首页
/ Crawl4AI 页面交互实战指南:JS 执行、等待条件、会话复用与虚拟滚动

Crawl4AI 页面交互实战指南:JS 执行、等待条件、会话复用与虚拟滚动

2026-09-06 15:45:35作者:韦蓉瑛

Crawl4AI 通过 CrawlerRunConfig 中的 js_codejs_code_before_waitwait_forsession_idjs_only 等参数,提供了一套完整的动态页面交互能力:点击“Load More”、填表提交、等待元素或数据出现、跨步骤复用同一会话。读完本文,你将掌握 Crawl4AI 页面交互管线的执行顺序与各参数含义,能编写“滚动 + 点击 + 等待 + 会话复用”的多步采集脚本,并能处理 Web Components 的 Shadow DOM 与 Twitter/Instagram 式虚拟滚动站点。

1. JavaScript 执行:js_code 与执行时机

CrawlerRunConfig.js_code 接受单个 JS 字符串或 JS 片段列表,它在 wait_fordelay_before_return_html 完成之后才执行——也就是说,你的代码跑在一个已经完全加载好的页面上。

一个典型用法是滚动到页面底部,然后可选地点击“Load More”按钮:

import asyncio
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig

async def main():
    # 单条 JS 命令
    config = CrawlerRunConfig(
        js_code="window.scrollTo(0, document.body.scrollHeight);"
    )

    async with AsyncWebCrawler() as crawler:
        result = await crawler.arun(
            url="https://news.ycombinator.com",  # 示例站点
            config=config
        )
        print("Crawled length:", len(result.cleaned_html))

    # 多条命令
    js_commands = [
        "window.scrollTo(0, document.body.scrollHeight);",
        # Hacker News 的 'More' 链接
        "document.querySelector('a.morelink')?.click();",
    ]
    config = CrawlerRunConfig(js_code=js_commands)

    async with AsyncWebCrawler() as crawler:
        result = await crawler.arun(
            url="https://news.ycombinator.com",
            config=config
        )
        print("After scroll+click, length:", len(result.cleaned_html))

if __name__ == "__main__":
    asyncio.run(main())

与页面交互相关的四个关键参数:

  • js_code:在 wait_fordelay_before_return_html 之后、在完全加载的页面上执行的 JavaScript。
  • js_code_before_wait:在 wait_for 之前执行的 JavaScript。适合用于触发某项加载行为,然后让 wait_for 去检查结果。
  • js_only:后续调用设为 True 时,表示不发起新的完整导航,而是在已有会话上继续执行 JS。
  • session_id:指定一个 ID 后,多次 arun() 调用将复用同一个页面。

从源码结构看,这些参数在 async_configs.py 中定义并校验,js_codejs_code_before_wait 均支持 strList[str]js_only 默认为 False

1.1 执行顺序:你的 JS 在管线中的确切位置

理解 JavaScript 相对于其他管线步骤的执行时机非常重要,Crawl4AI 的页面处理管线顺序如下:

1. Page navigation (page.goto)
2. js_code_before_wait     ← 触发加载 / 点击选项卡
3. wait_for                ← 等待内容出现
4. delay_before_return_html ← 额外的安全余量
5. js_code                 ← 在完全加载的页面上执行
6. flatten_shadow_dom      ← (若启用)
7. page.content()          ← HTML 捕获

这一顺序在 async_crawler_strategy.py 中得到了直接印证,源码用“Phase”注释划分了各阶段:先执行 js_code_before_wait(Phase 1,注释写明“for triggering loading that wait_for checks”),再执行 wait_for 等待(Phase 2),随后执行 delay_before_return_html 休眠与 js_code(Phase 3),最后才是 iframe 处理、弹窗移除与 HTML 捕获(Phase 4/5)。

如果你需要用 JS 触发某个动作、然后等待其结果,请组合 js_code_before_wait + wait_for

config = CrawlerRunConfig(
    # 先点击选项卡
    js_code_before_wait="document.querySelector('#specs-tab')?.click();",
    # 然后等待选项卡内容出现
    wait_for="css:#specs-panel .content",
)

2. 等待条件:CSS 与 JavaScript 两种模式

wait_for 支持两种模式,其解析逻辑在 smart_wait 方法中实现:以 js: 开头的按 JavaScript 函数处理,以 css: 开头的按 CSS 选择器处理,两者都不带前缀时则先尝试按 JS 函数求值、失败后回退到 CSS 选择器匹配。

2.1 基于 CSS 的等待

只想等待某个特定元素出现时,直接用 css: 前缀。例如等待 Hacker News 至少出现 30 条内容:

import asyncio
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig

async def main():
    config = CrawlerRunConfig(
        # 等待 Hacker News 上至少出现 30 条
        wait_for="css:.athing:nth-child(30)"
    )
    async with AsyncWebCrawler() as crawler:
        result = await crawler.arun(
            url="https://news.ycombinator.com",
            config=config
        )
        print("We have at least 30 items loaded!")
        # 粗略检查
        print("Total items in HTML:", result.cleaned_html.count("athing"))

if __name__ == "__main__":
    asyncio.run(main())

关键参数wait_for="css:..." 让爬虫一直等待,直到该 CSS 选择器对应的元素出现在页面上。

2.2 基于 JavaScript 的等待

对于更复杂的条件(例如等待内容数量超过阈值),使用 js: 前缀:

wait_condition = """() => {
    const items = document.querySelectorAll('.athing');
    return items.length > 50;  // 等待至少 51 条
}"""

config = CrawlerRunConfig(wait_for=f"js:{wait_condition}")

底层机制:Crawl4AI 会持续轮询该 JS 函数,直到它返回 true 或超时。从源码看,等待超时的取值逻辑是:若显式设置了 wait_for_timeout 则用它,否则回退到 page_timeout(见 async_crawler_strategy.py);等待失败会抛出 RuntimeError("Wait condition failed: ..."),因此编写 js: 条件时应确保条件确实可满足,避免无谓的超时。

3. 处理动态内容:多步操作模式

许多现代站点需要多个步骤:滚动、点击“Load More”或通过 JavaScript 刷新数据。以下是典型模式。

3.1 “Load More”示例(Hacker News 的 More 链接)

import asyncio
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig

async def main():
    # 第 1 步:加载 Hacker News 初始页面
    config = CrawlerRunConfig(
        wait_for="css:.athing:nth-child(30)"  # 等待 30 条
    )
    async with AsyncWebCrawler() as crawler:
        result = await crawler.arun(
            url="https://news.ycombinator.com",
            config=config
        )
        print("Initial items loaded.")

        # 第 2 步:滚动并点击 “More” 链接
        load_more_js = [
            "window.scrollTo(0, document.body.scrollHeight);",
            # 页面底部的 “More” 链接
            "document.querySelector('a.morelink')?.click();"
        ]

        next_page_conf = CrawlerRunConfig(
            js_code=load_more_js,
            wait_for="""js:() => {
                return document.querySelectorAll('.athing').length > 30;
            }""",
            # 标记不做重新导航,而是在同一会话中运行 JS:
            js_only=True,
            session_id="hn_session"
        )

        # 复用同一个爬虫会话
        result2 = await crawler.arun(
            url="https://news.ycombinator.com",  # 同一 URL,但延续会话
            config=next_page_conf
        )
        total_items = result2.cleaned_html.count("athing")
        print("Items after load-more:", total_items)

if __name__ == "__main__":
    asyncio.run(main())

关键参数

  • session_id="hn_session":让多次 arun() 调用共享同一个页面。
  • js_only=True:不执行完整重载,只在已打开的页面上应用 JS。
  • js:wait_for:等待条目数量增长到 30 以上。

js_only 的行为在源码中非常清晰:async_crawler_strategy.py 中,只有当 not config.js_only 时才会调用 page.goto()before_goto/after_goto 钩子;否则直接进入等待 body 可见、执行 JS、等待条件的流程,状态码固定记为 200。这正是“部分刷新”语义的实现基础。

3.2 表单交互

如果站点带有搜索或登录表单,可以用 js_code 填字段并提交。例如假设 GitHub 有一个本地搜索表单:

js_form_interaction = """
document.querySelector('#your-search').value = 'TypeScript commits';
document.querySelector('form').submit();
"""

config = CrawlerRunConfig(
    js_code=js_form_interaction,
    wait_for="css:.commit"
)
result = await crawler.arun(url="https://github.com/search", config=config)

注意:实际使用时需将 ID 或 class 替换为目标站点表单的真实选择器。由于 js_codewait_for 之后执行,若表单提交会触发导航,请改用 js_code_before_wait 提交表单,再用 wait_for 等待结果页——这一点由第 1.1 节的执行顺序直接决定。

4. 时间控制参数

  1. page_timeout(毫秒):页面加载或脚本执行的整体时间上限,同时会被 page.goto()wait_for 用作超时(未单独设置 wait_for_timeout 时)。
  2. delay_before_return_html(秒):捕获最终 HTML 之前额外等待一小段时间。
  3. mean_delaymax_range:当用 arun_many() 处理多个 URL 时,这两个参数会在每次请求之间加入随机停顿。

示例:

config = CrawlerRunConfig(
    page_timeout=60000,  # 60 秒上限
    delay_before_return_html=2.5
)

5. 多步交互完整示例:GitHub TypeScript Commits 翻页

下面是一个精简脚本,在 GitHub 的 TypeScript commits 页面上执行多次“下一页”点击,通过复用同一会话逐步累积新提交。代码涵盖了实际会依赖的 CrawlerRunConfig 参数:

import asyncio
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, CacheMode

async def multi_page_commits():
    browser_cfg = BrowserConfig(
        headless=False,  # 可视窗口便于演示
        verbose=True
    )
    session_id = "github_ts_commits"

    base_wait = """js:() => {
        const commits = document.querySelectorAll('li.Box-sc-g0xbh4-0 h4');
        return commits.length > 0;
    }"""

    # 第 1 步:加载初始 commits
    config1 = CrawlerRunConfig(
        wait_for=base_wait,
        session_id=session_id,
        cache_mode=CacheMode.BYPASS,
        # 首次加载不使用 js_only
    )

    async with AsyncWebCrawler(config=browser_cfg) as crawler:
        result = await crawler.arun(
            url="https://github.com/microsoft/TypeScript/commits/main",
            config=config1
        )
        print("Initial commits loaded. Count:", result.cleaned_html.count("commit"))

        # 第 2 步:后续页面运行 JS,若存在则点击 “Next Page”
        js_next_page = """
        const selector = 'a[data-testid="pagination-next-button"]';
        const button = document.querySelector(selector);
        if (button) button.click();
        """

        # 等待新 commits 出现
        wait_for_more = """js:() => {
            const commits = document.querySelectorAll('li.Box-sc-g0xbh4-0 h4');
            if (!window.firstCommit && commits.length>0) {
                window.firstCommit = commits[0].textContent;
                return false;
            }
            // 顶部 commit 变化,说明已有新 commits
            const topNow = commits[0]?.textContent.trim();
            return topNow && topNow !== window.firstCommit;
        }"""

        for page in range(2):  # 再翻 2 页 “Next”
            config_next = CrawlerRunConfig(
                session_id=session_id,
                js_code=js_next_page,
                wait_for=wait_for_more,
                js_only=True,       # 从已打开的标签页继续
                cache_mode=CacheMode.BYPASS
            )
            result2 = await crawler.arun(
                url="https://github.com/microsoft/TypeScript/commits/main",
                config=config_next
            )
            print(f"Page {page+2} commits count:", result2.cleaned_html.count("commit"))

        # 可选:结束会话
        await crawler.crawler_strategy.kill_session(session_id)

async def main():
    await multi_page_commits()

if __name__ == "__main__":
    asyncio.run(main())

要点

  • session_id:保持同一页面处于打开状态;
  • js_code + wait_for + js_only=True:做“部分刷新”,等待新提交出现;
  • cache_mode=CacheMode.BYPASS:确保每一步都拿到新鲜数据,避免缓存命中导致翻页判断失效。

会话的创建与销毁由 kill_session 管理,它会委托给 browser_manager.kill_session(session_id) 真正关闭对应会话;在长任务脚本结束时显式调用它,可以及时释放浏览器资源。

6. 将交互与结构化抽取结合

动态内容加载完成后,可以直接挂接 extraction_strategy(如 JsonCssExtractionStrategyLLMExtractionStrategy)。例如在上面翻完页后抽取提交标题:

from crawl4ai import JsonCssExtractionStrategy

schema = {
    "name": "Commits",
    "baseSelector": "li.Box-sc-g0xbh4-0",
    "fields": [
        {"name": "title", "selector": "h4.markdown-title", "type": "text"}
    ]
}
config = CrawlerRunConfig(
    session_id="ts_commits_session",
    js_code=js_next_page,
    wait_for=wait_for_more,
    extraction_strategy=JsonCssExtractionStrategy(schema)
)

完成后,从 result.extracted_content 中读取 JSON 结果即可。

7. Shadow DOM 拍平(Web Components)

Web Components(Stencil、Lit、Shoelace 等)构建的站点会把内容渲染在 Shadow DOM 中——一棵对普通页面序列化不可见的封装子树。设置 flatten_shadow_dom=True 即可将其抽取出来:

config = CrawlerRunConfig(
    flatten_shadow_dom=True,
    wait_until="load",
    delay_before_return_html=3.0,  # 给组件 hydrate 留出时间
)

从源码结构看,这一步发生在 HTML 捕获阶段(Phase 5):async_crawler_strategy.pyconfig.flatten_shadow_dom 为真时加载 js_snippet/flatten_shadow_dom.js 并注入页面。该脚本采用手工递归序列化:遇到带 shadowRoot 的宿主元素就切换到 shadow 感知模式,通过 assignedNodes 解析 <slot> 投影,跳过 shadow 作用域的 <style>,最终生成扁平 HTML——源码注释明确说明其目标是“proper slot resolution… produces clean HTML with no regex hacks”。完整示例可参考 shadow_dom_crawling.py 与内容选择文档 content-selection.md 中的 Shadow DOM 章节。

8. 页面交互相关 CrawlerRunConfig 参数速查

以下是 CrawlerRunConfig 中与交互相关的关键参数(完整列表见 配置参数参考):

参数 作用
js_code wait_for + delay_before_return_html 之后、完全加载的页面上执行的 JavaScript
js_code_before_wait wait_for 之前执行的 JavaScript,用于触发后续等待要检查的加载行为
js_only True 时不做新的页面导航,只在已有会话中执行 JS
wait_for 等待条件表达式:CSS("css:...")或 JS("js:..."
session_id 跨多次调用复用同一页面
cache_mode 是否读取/写入缓存,或 CacheMode.BYPASS 绕过
flatten_shadow_dom 捕获前将 Shadow DOM 内容拍平进 light DOM
process_iframes 将 iframe 内容内联进主文档(见 async_crawler_strategy.pyprocess_iframes 调用)
remove_overlay_elements 自动移除某些弹出浮层
remove_consent_popups 移除已知 CMP 供应商(OneTrust、Cookiebot、Didomi 等)的 GDPR/Cookie 同意弹窗
simulate_user, override_navigator, magic 反爬或“拟人化”交互

源码中这些开关的执行位置也验证了管线顺序:process_iframesremove_consent_popupsremove_overlay_elements 均在 js_code 之后、HTML 捕获之前依次执行(见 async_crawler_strategy.py);而 simulate_user/magic 触发的是鼠标移动与滚轮信号模拟(见 async_crawler_strategy.py),注释说明刻意避免键盘事件和固定位置点击,以免误触按钮导致意外导航。

9. 虚拟滚动:VirtualScrollConfig

对于使用虚拟滚动的站点(滚动时内容被替换而非追加,如 Twitter 或 Instagram),Crawl4AI 提供了专门的 VirtualScrollConfig

from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, VirtualScrollConfig

async def crawl_twitter_timeline():
    # 为 Twitter 式信息流配置虚拟滚动
    virtual_config = VirtualScrollConfig(
        container_selector="[data-testid='primaryColumn']",  # Twitter 主列
        scroll_count=30,                # 滚动 30 次
        scroll_by="container_height",   # 每次按容器高度滚动
        wait_after_scroll=1.0          # 每次滚动后等待 1 秒
    )

    config = CrawlerRunConfig(
        virtual_scroll_config=virtual_config
    )

    async with AsyncWebCrawler() as crawler:
        result = await crawler.arun(
            url="https://twitter.com/search?q=AI",
            config=config
        )
        # result.html 现在包含虚拟滚动的全部推文

VirtualScrollConfig 的完整定义见 async_configs.pycontainer_selector 为必填的滚动容器 CSS 选择器;scroll_count 默认 10;scroll_by 默认为 "container_height",也可取 "page_height" 或指定固定像素值;wait_after_scroll 默认 0.5 秒。CrawlerRunConfig 也接受 dict 形式的 virtual_scroll_config,内部会通过 VirtualScrollConfig.from_dict 自动转换(见 async_configs.py)。

9.1 Virtual Scroll 与 JS 滚动的对比

特性 Virtual Scroll JS Code 滚动
适用场景 滚动时内容被替换 内容追加或普通滚动
配置方式 VirtualScrollConfig 对象 js_code 中写滚动命令
自动合并 是——合并所有唯一内容 否——只捕获最终状态
最佳适用 Twitter、Instagram、虚拟表格 传统页面、Load More 按钮

从源码结构看,其内部实现在 _handle_virtual_scroll:它在 wait_for 之后运行(注释写明“after wait_for so container exists”),在页面内注入一段 JS 循环——每次滚动 scrollAmount(按 scroll_by 解析为容器高度、视口高度或固定像素),等待 wait_after_scroll 后比较容器 HTML:若无变化则继续滚动;若是追加(currentHTML.startsWith(previousHTML))则内容已在页中无需处理;若是替换,则把上一轮 HTML 捕获为一个 chunk,滚动 scroll_count 次后合并所有 chunk 写回页面。这套“捕获 + 合并”机制正是它能拿到被虚拟列表回收掉的离屏内容的原因。仓库中的 tests/test_virtual_scroll.py 用一个 1000 条、每屏 10 条且采用内容替换策略的虚拟滚动测试页验证了全部条目可被捕获,docs/examples/virtual_scroll_example.py 则提供了 Twitter 类信息流的完整示例;更多配置选项参见 Virtual Scroll 文档

10. 小结:构建可靠的动态页面采集流程

Crawl4AI 的页面交互能力让你可以:

  1. 执行 JavaScript 完成滚动、点击或表单填写(js_code / js_code_before_wait);
  2. 等待 CSS 或自定义 JS 条件成立后再捕获数据(wait_for,底层由 smart_wait 解析 js:/css: 前缀并轮询);
  3. 处理多步流程(如“Load More”),借助 js_only=True + session_id 做部分刷新或持久会话,并用 CacheMode.BYPASS 保证每步拿到新数据;
  4. 拍平 Shadow DOM,从 Web Components 站点提取隐藏内容(flatten_shadow_dom + 专用 init 脚本);
  5. 将交互结果与结构化抽取extraction_strategy)或虚拟滚动VirtualScrollConfig)结合,覆盖现代交互站点的各种加载模式。

实践中建议的调试顺序是:先用 headless=Falseverbose=TrueBrowserConfig 观察页面,用最小 wait_for 条件确认目标元素何时出现,再逐步加入 js_code/js_code_before_waitsession_id 复用;每一步的管线位置都可以对照 async_crawler_strategy.py 中的 Phase 注释核对。需要更深入的 Hook 机制、用户模拟或完整参数清单时,参考 配置参数 API 文档 与各高级主题文档即可。

登录后查看全文
热门项目推荐
相关项目推荐