Crawl4AI 页面交互实战指南:JS 执行、等待条件、会话复用与虚拟滚动
Crawl4AI 通过 CrawlerRunConfig 中的 js_code、js_code_before_wait、wait_for、session_id 与 js_only 等参数,提供了一套完整的动态页面交互能力:点击“Load More”、填表提交、等待元素或数据出现、跨步骤复用同一会话。读完本文,你将掌握 Crawl4AI 页面交互管线的执行顺序与各参数含义,能编写“滚动 + 点击 + 等待 + 会话复用”的多步采集脚本,并能处理 Web Components 的 Shadow DOM 与 Twitter/Instagram 式虚拟滚动站点。
1. JavaScript 执行:js_code 与执行时机
CrawlerRunConfig.js_code 接受单个 JS 字符串或 JS 片段列表,它在 wait_for 和 delay_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_for和delay_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_code、js_code_before_wait 均支持 str 或 List[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_code 在 wait_for 之后执行,若表单提交会触发导航,请改用 js_code_before_wait 提交表单,再用 wait_for 等待结果页——这一点由第 1.1 节的执行顺序直接决定。
4. 时间控制参数
page_timeout(毫秒):页面加载或脚本执行的整体时间上限,同时会被page.goto()与wait_for用作超时(未单独设置wait_for_timeout时)。delay_before_return_html(秒):捕获最终 HTML 之前额外等待一小段时间。mean_delay与max_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(如 JsonCssExtractionStrategy 或 LLMExtractionStrategy)。例如在上面翻完页后抽取提交标题:
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.py 在 config.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.py 的 process_iframes 调用) |
remove_overlay_elements |
自动移除某些弹出浮层 |
remove_consent_popups |
移除已知 CMP 供应商(OneTrust、Cookiebot、Didomi 等)的 GDPR/Cookie 同意弹窗 |
simulate_user, override_navigator, magic |
反爬或“拟人化”交互 |
源码中这些开关的执行位置也验证了管线顺序:process_iframes、remove_consent_popups、remove_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.py:container_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 的页面交互能力让你可以:
- 执行 JavaScript 完成滚动、点击或表单填写(
js_code/js_code_before_wait); - 等待 CSS 或自定义 JS 条件成立后再捕获数据(
wait_for,底层由smart_wait解析js:/css:前缀并轮询); - 处理多步流程(如“Load More”),借助
js_only=True+session_id做部分刷新或持久会话,并用CacheMode.BYPASS保证每步拿到新数据; - 拍平 Shadow DOM,从 Web Components 站点提取隐藏内容(
flatten_shadow_dom+ 专用 init 脚本); - 将交互结果与结构化抽取(
extraction_strategy)或虚拟滚动(VirtualScrollConfig)结合,覆盖现代交互站点的各种加载模式。
实践中建议的调试顺序是:先用 headless=False 与 verbose=True 的 BrowserConfig 观察页面,用最小 wait_for 条件确认目标元素何时出现,再逐步加入 js_code/js_code_before_wait 与 session_id 复用;每一步的管线位置都可以对照 async_crawler_strategy.py 中的 Phase 注释核对。需要更深入的 Hook 机制、用户模拟或完整参数清单时,参考 配置参数 API 文档 与各高级主题文档即可。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00