Crawl4AI Hooks 与认证机制实战:AsyncWebCrawler 八大钩子点的用法与源码实现解析
本篇技术文章围绕 Crawl4AI 的 Hooks & Auth 主题展开,系统讲解 AsyncWebCrawler 提供的 8 个钩子(Hook)触发点、各自适用的时机与典型用法,并给出官方文档中的完整可运行示例。读完本文,你将能够把登录认证、自定义请求头、路由拦截、懒加载滚动等操作挂到爬取管线的正确位置,并能从源码层面理解每个钩子的实际触发位置与参数约定。
一、钩子系统概览:8 个触发点与各自职责
Crawl4AI 的 hooks(钩子) 机制允许你在爬取管线的特定节点插入自定义逻辑。官方文档 docs/md_v2/advanced/hooks-auth.md 列出的 8 个钩子点为:
| 钩子 | 触发时机 | 典型用途 |
|---|---|---|
on_browser_created |
浏览器实例创建后 | 轻量级初始化(此时没有 page/context) |
on_page_context_created |
新的 context 与 page 创建后 | 认证登录、路由拦截、Cookie 注入 |
before_goto |
导航到目标页面前 | 注入自定义请求头、记录目标 URL |
after_goto |
导航完成之后 | 验证页面内容、等待关键元素 |
on_user_agent_updated |
User-Agent 发生变更时 | 隐身模式、UA 切换的副作用处理 |
on_execution_started |
自定义 JavaScript 开始执行时 | 监控/记录 JS 执行 |
before_retrieve_html |
抓取最终 HTML 快照前 | 最后一次滚动、触发懒加载 |
before_return_html |
把 HTML 返回给 CrawlResult 前 |
记录 HTML 长度、做最后的微调 |
文档特别强调了一个关键约束:避免在 on_browser_created 中做重任务——因为此时还没有 page context。如果目标是登录,应当放在 on_page_context_created 中执行。
使用警告(原文档要点):不要在错误的钩子里操作页面对象,否则可能使管线崩溃或产生错误结果。常见错误包括在
on_browser_created中创建/关闭页面,或在错误的时机覆盖、删除页面元素。钩子应保持聚焦于小任务(如路由过滤、自定义请求头),让主流程(爬取、数据提取)正常推进。
二、源码级机制:set_hook 与 execute_hook
钩子的注册与执行都集中在 AsyncCrawlerStrategy 中。
1. 钩子注册表。 策略对象在初始化时创建一个包含 9 个键的 self.hooks 字典,全部初始为 None(async_crawler_strategy.py):
self.hooks = {
"on_browser_created": None,
"on_page_context_created": None,
"on_user_agent_updated": None,
"on_execution_started": None,
"on_execution_ended": None, # 源码中存在,但官方文档未单独介绍
"before_goto": None,
"after_goto": None,
"before_return_html": None,
"before_retrieve_html": None,
}
可以看到源码中实际还预留了 on_execution_ended 钩子(与 on_execution_started 成对出现,详见下文执行链),文档未将其列入 8 项,但从源码结构看它是可用的补充触发点。
2. 注册方法 set_hook。 位于 async_crawler_strategy.py:
def set_hook(self, hook_type: str, hook: Callable):
if hook_type in self.hooks:
self.hooks[hook_type] = hook
else:
raise ValueError(f"Invalid hook type: {hook_type}")
要点:传入未定义的钩子名会直接抛 ValueError,因此拼写必须与上表一致。set_hook 的 docstring 还明确了参数约定:除 on_browser_created 接收 browser 外,其余钩子统一接收 page、context 和 **kwargs。
3. 执行方法 execute_hook。 位于 async_crawler_strategy.py:
async def execute_hook(self, hook_type: str, *args, **kwargs):
hook = self.hooks.get(hook_type)
if hook:
if asyncio.iscoroutinefunction(hook):
return await hook(*args, **kwargs)
else:
return hook(*args, **kwargs)
return args[0] if args else None
这里有两点值得注意:
- 同步与异步钩子都受支持:
execute_hook用asyncio.iscoroutinefunction判断,因此钩子既可以是async def,也可以是普通同步函数; - 未注册钩子时安全透传:返回第一个位置参数(通常是
page或browser),保证主流程不中断。但该方法不做异常捕获——如果钩子内部抛出未处理异常,异常会直接向上传播,导致本次爬取失败,这印证了文档"Error Handling:钩子失败可能导致整体爬取失败"的提醒。
三、每个钩子的实际触发位置(调用链溯源)
在 AsyncCrawlerStrategy 源码中逐一检索 execute_hook(...) 调用,可以确认文档所述 8 个触发点在代码中的真实位置:
on_browser_created— 在start()中触发(async_crawler_strategy.py):浏览器管理器启动后立即执行,传参为browser与context。由于start()只在crawler.start()时调用一次,该钩子天然只触发一次。on_page_context_created— 在页面与上下文创建完成后、导航之前触发(async_crawler_strategy.py):await self.execute_hook("on_page_context_created", page, context=context, config=config)。注意此时config会作为kwargs传入,钩子可以感知本次运行的CrawlerRunConfig。before_goto— 在真正执行page.goto()之前触发(async_crawler_strategy.py):await self.execute_hook("before_goto", page, context=context, url=url, config=config)。若config.js_only=True,则跳过导航与before_goto。after_goto— 导航(含重定向链处理)完成后触发(async_crawler_strategy.py),并把response对象一并传入,这就是文档示例中after_goto(page, context, url, response, **kwargs)能拿到响应的来源。before_retrieve_html— 在取出 HTML 前触发(async_crawler_strategy.py)。on_execution_started— 当配置了自定义 JS(js_code等)即将执行时触发(async_crawler_strategy.py):
await self.execute_hook("on_execution_started", page, context=context, config=config)
await self.execute_hook("on_execution_ended", page, context=context, config=config, result=execution_result)
before_return_html— 在最终 HTML 快照形成后、返回给调用方之前触发,传参为page、html、context、config(async_crawler_strategy.py),因此钩子签名中才会出现html: str参数。on_user_agent_updated— 该键在新版AsyncCrawlerStrategy的注册表中保留,set_hook仍可成功注册;但从源码结构看,新版异步策略中不再存在主动调用它的执行点,实际触发仅保留在旧版同步爬虫 legacy/crawler_strategy.py(self.driver = self.execute_hook("on_user_agent_updated", self.driver))中。因此若你的工作流依赖 UA 变更回调,建议以新版钩子体系中的其他触发点(如before_goto中显式set_extra_http_headers/设置 UA)为主。
四、完整示例:注册全部 8 个钩子
以下示例完整继承自官方文档(与仓库中的 docs/examples/hooks_example.py 示例互为对照),演示了每个钩子的定义、典型操作与注册方式:
import asyncio
import json
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, CacheMode
from playwright.async_api import Page, BrowserContext
async def main():
print("🔗 Hooks Example: Demonstrating recommended usage")
# 1) Configure the browser
browser_config = BrowserConfig(
headless=True,
verbose=True
)
# 2) Configure the crawler run
crawler_run_config = CrawlerRunConfig(
js_code="window.scrollTo(0, document.body.scrollHeight);",
wait_for="body",
cache_mode=CacheMode.BYPASS
)
# 3) Create the crawler instance
crawler = AsyncWebCrawler(config=browser_config)
#
# Define Hook Functions
#
async def on_browser_created(browser, **kwargs):
# Called once the browser instance is created (but no pages or contexts yet)
print("[HOOK] on_browser_created - Browser created successfully!")
# Typically, do minimal setup here if needed
return browser
async def on_page_context_created(page: Page, context: BrowserContext, **kwargs):
# Called right after a new page + context are created (ideal for auth or route config).
print("[HOOK] on_page_context_created - Setting up page & context.")
# Example 1: Route filtering (e.g., block images)
async def route_filter(route):
if route.request.resource_type == "image":
print(f"[HOOK] Blocking image request: {route.request.url}")
await route.abort()
else:
await route.continue_()
await context.route("**", route_filter)
# Example 2: (Optional) Simulate a login scenario
# (We do NOT create or close pages here, just do quick steps if needed)
# e.g., await page.goto("https://example.com/login")
# e.g., await page.fill("input[name='username']", "testuser")
# e.g., await page.fill("input[name='password']", "password123")
# e.g., await page.click("button[type='submit']")
# e.g., await page.wait_for_selector("#welcome")
# e.g., await context.add_cookies([...])
# Then continue
# Example 3: Adjust the viewport
await page.set_viewport_size({"width": 1080, "height": 600})
return page
async def before_goto(page: Page, context: BrowserContext, url: str, **kwargs):
# Called before navigating to each URL.
print(f"[HOOK] before_goto - About to navigate: {url}")
# e.g., inject custom headers
await page.set_extra_http_headers({
"Custom-Header": "my-value"
})
return page
async def after_goto(page: Page, context: BrowserContext,
url: str, response, **kwargs):
# Called after navigation completes.
print(f"[HOOK] after_goto - Successfully loaded: {url}")
# e.g., wait for a certain element if we want to verify
try:
await page.wait_for_selector('.content', timeout=1000)
print("[HOOK] Found .content element!")
except:
print("[HOOK] .content not found, continuing anyway.")
return page
async def on_user_agent_updated(page: Page, context: BrowserContext,
user_agent: str, **kwargs):
# Called whenever the user agent updates.
print(f"[HOOK] on_user_agent_updated - New user agent: {user_agent}")
return page
async def on_execution_started(page: Page, context: BrowserContext, **kwargs):
# Called after custom JavaScript execution begins.
print("[HOOK] on_execution_started - JS code is running!")
return page
async def before_retrieve_html(page: Page, context: BrowserContext, **kwargs):
# Called before final HTML retrieval.
print("[HOOK] before_retrieve_html - We can do final actions")
# Example: Scroll again
await page.evaluate("window.scrollTo(0, document.body.scrollHeight);")
return page
async def before_return_html(page: Page, context: BrowserContext, html: str, **kwargs):
# Called just before returning the HTML in the result.
print(f"[HOOK] before_return_html - HTML length: {len(html)}")
return page
#
# Attach Hooks
#
crawler.crawler_strategy.set_hook("on_browser_created", on_browser_created)
crawler.crawler_strategy.set_hook("on_page_context_created", on_page_context_created)
crawler.crawler_strategy.set_hook("before_goto", before_goto)
crawler.crawler_strategy.set_hook("after_goto", after_goto)
crawler.crawler_strategy.set_hook("on_user_agent_updated", on_user_agent_updated)
crawler.crawler_strategy.set_hook("on_execution_started", on_execution_started)
crawler.crawler_strategy.set_hook("before_retrieve_html", before_retrieve_html)
crawler.crawler_strategy.set_hook("before_return_html", before_return_html)
await crawler.start()
# 4) Run the crawler on an example page
url = "https://example.com"
result = await crawler.arun(url, config=crawler_run_config)
if result.success:
print("\nCrawled URL:", result.url)
print("HTML length:", len(result.html))
else:
print("Error:", result.error_message)
await crawler.close()
if __name__ == "__main__":
asyncio.run(main())
示例中几个值得展开的细节:
- 路由拦截放在
on_page_context_created内通过context.route("**", route_filter)实现,拦截规则挂载在 context 级别,因此该上下文中的后续所有请求(包括arun()的主导航)都会被过滤; - 登录流程以注释形式给出模板:
goto 登录页 → fill 表单 → click 提交 → wait_for_selector 验证 → add_cookies 固化凭据。注意文档的告诫——在这里不要创建或关闭 page,只做快速步骤,让主爬取流程接管后续导航; - 示例中
CrawlerRunConfig的js_code参数触发on_execution_started,两者形成组合:before_retrieve_html再补一次滚动,覆盖懒加载内容。
五、Hook 生命周期小结:每个钩子能做什么、不能做什么
官方文档对 8 个钩子的时机约束做了精炼总结,这里完整继承并加以说明:
on_browser_created:浏览器已就绪,但没有任何 page 或 context。只做轻量初始化——不要在这里打开或关闭页面(那是on_page_context_created的职责)。on_page_context_created:适合做认证与路由拦截。此时你手里已经有一个可用的 page + context,但尚未导航到目标 URL。before_goto:导航前的最后一刻。典型用途是设置自定义请求头或记录目标 URL(见源码,url作为 kwargs 传入,async_crawler_strategy.py)。after_goto:页面导航完成。适合验证内容或等待关键元素(response对象可用,可做状态码判断)。on_user_agent_updated:User-Agent 变化时触发(隐身模式或不同 UA 策略场景;结合第二节的源码分析了解其在新版策略中的现状)。on_execution_started:只要配置了js_code或执行自定义脚本,JS 即将启动时触发。before_retrieve_html:最终 HTML 快照之前的最后机会,常用来做最后一次滚动或懒加载触发。before_return_html:返回 HTML 给CrawlResult前的最后一个钩子,适合记录 HTML 长度或做轻微修改(此时能拿到html: str)。
六、认证(Auth)应该放在哪里
文档给出的推荐方案是:当需要以下操作时,使用 on_page_context_created:
- 导航到登录页或填充表单;
- 设置 cookies 或 localStorage token;
- 拦截资源路由以避免广告/图片等资源浪费。
之所以选这个钩子,是因为它保证新创建的 context 在 arun() 导航到主 URL 之前已完全处于你的控制之下——源码中该钩子正是在 page 创建后、page.goto() 之前触发的(async_crawler_strategy.py 与 async_crawler_strategy.py 之间的调用顺序可以印证)。
对于更复杂的认证场景,文档建议两条进阶路径:
- 基于身份的爬取(Identity-Based Crawling):把初始登录放在一个独立的、定义清晰的过程中完成,再把得到的 session 喂给主爬取流程,而不是把复杂认证硬塞进早期钩子。详见 docs/md_v2/advanced/identity-based-crawling.md;
- 会话复用:如果希望多次
arun()调用复用同一个会话,在CrawlerRunConfig中传入session_id=,钩子用法保持不变。相关文档见 docs/md_v2/advanced/session-management.md,示例见 docs/examples/session_id_example.py。
七、工程化注意事项
官方文档列出的四点"Additional Considerations",逐条结合源码说明如下:
- 会话管理(Session Management):多次
arun()复用单会话时传session_id=。从源码结构看,on_page_context_created在每个新上下文创建时都会触发(async_crawler_strategy.py),而会话复用场景下 context 只创建一次,登录步骤因此只需要执行一次——这是把认证放在该钩子的另一重好处。 - 性能(Performance):钩子若做重任务会拖慢爬取,保持精简。
before_goto/after_goto/before_retrieve_html等钩子位于每次 URL 的热路径上,一个 URL 就会走一遍完整钩子链,成本会被放大。 - 错误处理(Error Handling):钩子失败可能导致整体爬取失败。
execute_hook源码(async_crawler_strategy.py)不吞异常,因此应在钩子内部自行try/except或优雅降级。 - 并发(Concurrency):使用
arun_many()时,每个 URL 都会并行触发这些钩子,确保钩子实现是 async-safe 的(不要共享可变的全局状态)。
结语
Hooks 为 Crawl4AI 提供细粒度的管线控制能力,覆盖四个层次:
- Browser 创建(仅限轻量任务);
- Page / Context 创建(认证、路由拦截);
- Navigation 阶段(自定义请求头、日志、验证);
- 最终 HTML 获取前的收尾(滚动、长度记录、微调)。
遵循推荐用法:登录与重任务放 on_page_context_created,自定义请求头/日志放 before_goto / after_goto,滚动与最后检查放 before_retrieve_html / before_return_html。注册入口是 crawler.crawler_strategy.set_hook(hook_type, hook),实现与触发点均可在 crawl4ai/async_crawler_strategy.py 中查证,完整可运行示例见本文第四节与 docs/examples/hooks_example.py。
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 StartedRust0622
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