虚拟卷轴

现代网站越来越多地使用虚拟滚动(也称为窗口渲染或视口渲染)来高效处理大型数据集。此技术仅渲染 DOM 中的可见项目,并在用户滚动时替换内容。常见示例包括 Twitter 的时间线、Instagram 的动态以及许多数据表。

Crawl4AI 的虚拟滚动功能会自动检测并处理这些情况,确保您捕获所有内容,而不仅仅是最初可见的内容。

了解虚拟滚动

问题

传统的无限滚动会将新内容附加到现有内容上。虚拟滚动会替换内容以保持性能:

Traditional Scroll:          Virtual Scroll:
┌─────────────┐             ┌─────────────┐
│ Item 1      │             │ Item 11     │  <- Items 1-10 removed
│ Item 2      │             │ Item 12     │  <- Only visible items
│ ...         │             │ Item 13     │     in DOM
│ Item 10     │             │ Item 14     │
│ Item 11 NEW │             │ Item 15     │
│ Item 12 NEW │             └─────────────┘
└─────────────┘             
DOM keeps growing           DOM size stays constant

如果没有适当的处理,爬虫只能捕获当前可见的项目,而错过其余内容。

三种滚动场景

Crawl4AI 的虚拟滚动可检测并处理三种情况:

  1. 无变化 - 滚动时内容不更新(静态页面或到达末尾)
  2. 附加内容 - 将新项目添加到现有项目(传统无限滚动)
  3. 内容替换 - 用新项目替换项目(真正的虚拟滚动)

只有场景 3 需要特殊处理,Virtual Scroll 可以自动执行。

基本用法

from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, VirtualScrollConfig

# Configure virtual scroll
virtual_config = VirtualScrollConfig(
    container_selector="#feed",      # CSS selector for scrollable container
    scroll_count=20,                 # Number of scrolls to perform
    scroll_by="container_height",    # How much to scroll each time
    wait_after_scroll=0.5           # Wait time (seconds) after each scroll
)

# Use in crawler configuration
config = CrawlerRunConfig(
    virtual_scroll_config=virtual_config
)

async with AsyncWebCrawler() as crawler:
    result = await crawler.arun(url="https://example.com", config=config)
    # result.html contains ALL items from the virtual scroll

配置参数

虚拟滚动配置

范围 类型 默认 描述
container_selector str 必需的 可滚动容器的 CSS 选择器
scroll_count int 10 执行的最大滚动次数
scroll_by 或者int "container_height" 每步滚动量
wait_after_scroll float 0.5 每次滚动后等待的秒数

按选项滚动

  • - 按容器的可见高度滚动
  • - 按视口高度滚动
  • (整数) - 按精确像素量滚动

现实世界的例子

类似 Twitter 的时间线

from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, VirtualScrollConfig, BrowserConfig

async def crawl_twitter_timeline():
    # Twitter replaces tweets as you scroll
    virtual_config = VirtualScrollConfig(
        container_selector="[data-testid='primaryColumn']",
        scroll_count=30,
        scroll_by="container_height",
        wait_after_scroll=1.0  # Twitter needs time to load
    )

    browser_config = BrowserConfig(headless=True)  # Set to False to watch it work
    config = CrawlerRunConfig(
        virtual_scroll_config=virtual_config
    )

    async with AsyncWebCrawler(config=browser_config) as crawler:
        result = await crawler.arun(
            url="https://twitter.com/search?q=AI",
            config=config
        )

        # Extract tweet count
        import re
        tweets = re.findall(r'data-testid="tweet"', result.html)
        print(f"Captured {len(tweets)} tweets")

Instagram网格

async def crawl_instagram_grid():
    # Instagram uses virtualized grid for performance
    virtual_config = VirtualScrollConfig(
        container_selector="article",  # Main feed container
        scroll_count=50,               # More scrolls for grid layout
        scroll_by=800,                 # Fixed pixel scrolling
        wait_after_scroll=0.8
    )

    config = CrawlerRunConfig(
        virtual_scroll_config=virtual_config,
        screenshot=True  # Capture final state
    )

    async with AsyncWebCrawler() as crawler:
        result = await crawler.arun(
            url="https://www.instagram.com/explore/tags/photography/",
            config=config
        )

        # Count posts
        posts = result.html.count('class="post"')
        print(f"Captured {posts} posts from virtualized grid")

混合内容(新闻提要)

一些网站混合了静态和虚拟化内容:

async def crawl_mixed_feed():
    # Featured articles stay, regular articles virtualize
    virtual_config = VirtualScrollConfig(
        container_selector=".main-feed",
        scroll_count=25,
        scroll_by="container_height",
        wait_after_scroll=0.5
    )

    config = CrawlerRunConfig(
        virtual_scroll_config=virtual_config
    )

    async with AsyncWebCrawler() as crawler:
        result = await crawler.arun(
            url="https://news.example.com",
            config=config
        )

        # Featured articles remain throughout
        featured = result.html.count('class="featured-article"')
        regular = result.html.count('class="regular-article"')

        print(f"Featured (static): {featured}")
        print(f"Regular (virtualized): {regular}")

虚拟滚动与 scan_full_page

这两个功能都可处理动态内容,但用途不同:

特征 虚拟卷轴 扫描全页
目的 捕获滚动过程中替换的内容 加载滚动过程中附加的内容
用例 Twitter、Instagram、虚拟桌子 传统的无限滚动、延迟加载的图像
DOM 行为 替换元素 添加元素
内存使用情况 高效(合并内容) 可以长得很大
配置 需要容器选择器 整页显示

何时使用哪个?

在以下情况下使用虚拟滚动: - 滚动时内容消失(Twitter 时间线) - DOM 元素数量保持相对恒定 - 您需要虚拟列表中的所有项目 - 基于容器的滚动(非整页)

在以下情况下使用 scan_full_page: - 滚动时内容累积 - 图像延迟加载 - 简单的“加载更多”行为 - 整页滚动

结合萃取

虚拟滚动与提取策略无缝协作:

from crawl4ai import LLMExtractionStrategy, LLMConfig

# Define extraction schema
schema = {
    "type": "array",
    "items": {
        "type": "object", 
        "properties": {
            "author": {"type": "string"},
            "content": {"type": "string"},
            "timestamp": {"type": "string"}
        }
    }
}

# Configure both virtual scroll and extraction
config = CrawlerRunConfig(
    virtual_scroll_config=VirtualScrollConfig(
        container_selector="#timeline",
        scroll_count=20
    ),
    extraction_strategy=LLMExtractionStrategy(
        llm_config=LLMConfig(provider="openai/gpt-4o-mini"),
        schema=schema
    )
)

async with AsyncWebCrawler() as crawler:
    result = await crawler.arun(url="...", config=config)

    # Extracted data from ALL scrolled content
    import json
    posts = json.loads(result.extracted_content)
    print(f"Extracted {len(posts)} posts from virtual scroll")

性能提示

  1. 容器选择:选择器要具体。使用正确的容器可以提高性能。
  2. 滚动计数:从保守开始,然后根据需要增加:
    # Start with fewer scrolls
    virtual_config = VirtualScrollConfig(
        container_selector="#feed",
        scroll_count=10  # Test with 10, increase if needed
    )
    
  3. 等待时间:根据网站速度调整:
    # Fast sites
    wait_after_scroll=0.2
    
    # Slower sites or heavy content
    wait_after_scroll=1.5
    
  4. 调试模式:设置headless=False观看滚动:
    browser_config = BrowserConfig(headless=False)
    async with AsyncWebCrawler(config=browser_config) as crawler:
        # Watch the scrolling happen
    

内部运作方式

  1. 检测阶段:滚动并比较 HTML 以检测行为
  2. 捕获阶段:对于替换的内容,在每个位置存储 HTML 块
  3. 合并阶段:合并所有块,根据文本内容删除重复项
  4. 结果:包含所有唯一项目的完整 HTML

重复数据删除使用规范化文本(小写,无空格/符号)来确保准确合并而不会出现误报。

错误处理

虚拟滚动可以优雅地处理错误:

# If container not found or scrolling fails
result = await crawler.arun(url="...", config=config)

if result.success:
    # Virtual scroll worked or wasn't needed
    print(f"Captured {len(result.html)} characters")
else:
    # Crawl failed entirely
    print(f"Error: {result.error_message}")

如果未找到容器,则爬行将继续正常进行,无需虚拟滚动。

完整示例

请参阅我们的综合示例,其中演示了: - 类似 Twitter 的 feed - Instagram 网格 - 传统的无限滚动 - 混合内容场景 - 性能比较

# Run the examples
cd docs/examples
python virtual_scroll_example.py

该示例包括一个具有不同滚动行为的本地测试服务器以供实验。


> Feedback