跳到主要内容

单页主线:arun() 的端到端

本章讲清:你调一次 crawler.arun(url),内部到底发生了什么。读完你就拿到了整个项目的「骨架」,后面每一章都是在某根骨头上加肉。

1. 这一章解决什么

Crawl4AI 的所有功能——抓取、清洗、过滤、抽取——都挂在一条主线上。主线就是 AsyncWebCrawler.arun()。先看懂这根主线怎么流,再去钻每个环节,才不会迷路。

2. 入口与生命周期

AsyncWebCrawler 管的是一个浏览器实例的生命周期。两种用法(async_webcrawler.py:58-111 的类文档里写了):

# 推荐:上下文管理器自动开/关浏览器
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(url="https://example.com")

# 长期运行:手动管理
crawler = AsyncWebCrawler()
await crawler.start() # 启动浏览器
... # 多次 arun
await crawler.close() # 关闭

构造时它做三件事(async_webcrawler.py:115-174,__init__):建 logger、建 crawler strategy(默认 AsyncPlaywrightCrawlerStrategy)、并且arun 套一层深爬装饰器:

# async_webcrawler.py:170-171,__init__ 末尾
self._deep_handler = DeepCrawlDecorator(self)
self.arun = self._deep_handler(self.arun)

这一步很关键:arunDeepCrawlDecorator 包了一层。如果 config 里设了 deep_crawl_strategy,装饰器会拦截调用,转去多页爬取;否则放行到真正的单页 arun。深爬的事见 06-deep-and-adaptive.md

3. 主线全景

怎么读这张图: 自上而下是 arun() 内部的判断与流向;虚线框是可能提前返回的出口。

arun(url, config)

├─ 未 start? → 自动 start() async_webcrawler.py:247

├─ cache_context.should_read()?
│ └─ 命中 → (可选)CacheValidator 校验新鲜度 → 直接返回缓存

├─ 代理轮换:proxy_rotation_strategy 取下一个代理 :350-377

├─ 抓取循环 (最多 1 + max_retries 次):
│ for 每次重试:
│ for 每个代理:
│ crawler_strategy.crawl(url) → 原始 HTML :459
│ aprocess_html(...) → crawl_result :474
│ is_blocked(status, html)? → 被拦就换下个代理 :512
│ 没被拦 → _done=True, break

├─ 全失败且配了 fallback_fetch_function → 调它兜底 :554

├─ 仍被拦 → 标记 success=False, error_message :631

├─ 算 head_fingerprint(给缓存校验用) :649
├─ should_write() → acache_url(写 SQLite 缓存) :671

└─ return CrawlResultContainer(crawl_result)

4. 逐段拆解

4.1 缓存:读不读、写不写,由 CacheMode 决定

第一件事是判断缓存。CacheMode 是个枚举(cache_context.py:3-20):

模式读缓存写缓存用途
ENABLED默认,正常缓存
DISABLED完全不缓存
READ_ONLY只读
WRITE_ONLY只写(刷新缓存)
BYPASS本次跳过缓存

CacheContext 把 URL 类型和缓存模式打包,提供 should_read() / should_write()(cache_context.py:59 起)。注意 raw: 开头的 URL 是「调用方直接给的 HTML」,不可缓存。

命中缓存且开了 check_cache_freshness 时,还会用 CacheValidator 发 HEAD 请求比对 ETag/Last-Modified/head 指纹,判定 FRESH/STALE,STALE 就强制重爬(async_webcrawler.py:279-319)。这就是「智能缓存」。

4.2 抓取 + 反爬:两层 for 循环

没命中缓存就进入抓取。这里的设计精华是:重试 × 代理 两层循环(async_webcrawler.py:419-547)。

外层是重试次数 1 + max_retries,内层遍历代理列表 config._get_proxy_list()。每个组合里:

# async_webcrawler.py:459-512(精简)
async_response = await self.crawler_strategy.crawl(url, config=config) # 抓
html = sanitize_input_encode(async_response.html)
crawl_result = await self.aprocess_html(url=url, html=html, ...) # 处理
...
_blocked, _block_reason = is_blocked(async_response.status_code, html) # 反爬检测
if not _blocked:
_done = True; break # 成功,跳出

关键细节:raw: URL 跳过反爬(_is_raw_url,:403)——调用方自己给的 HTML,谈不上被网站拦。

4.3 兜底:fallback_fetch_function

所有代理都被拦/抛异常后,如果你配了 fallback_fetch_function(比如接一个第三方抓取 API),会调它最后一搏(async_webcrawler.py:554-609)。fallback 成功的结果是权威的,不再做反爬复检——因为真页面里也可能有 PerimeterX 之类的 JS 标记,复检会误报(:611-633 的注释解释了这点)。

4.4 反爬检测 is_blocked()

检测逻辑在 antibot_detector.py:is_blocked。它的哲学(文件顶部注释)是**「误报便宜、漏报致命」**:误报有 fallback 救,漏报用户直接拿到垃圾页。所以分层检测:

  • HTTP 403/503 带 HTML → 一律算被拦。
  • Tier 1 结构标记(Akamai Reference #、Cloudflare cf-error-code span 等)→ 任何页面大小都触发。
  • Tier 2 通用词 → 仅在短页或错误状态码下触发。
  • Tier 3 结构完整性 → 抓「静默拦截」和空壳页。

4.5 处理:aprocess_html 的三步

抓到 HTML 后,真正「把脏 HTML 变干净」的是 aprocess_html(async_webcrawler.py:715)。它是后面所有章节的入口,三步:

aprocess_html(url, html, config, ...)

├─ ① ScrapingStrategy.scrap(url, html, ...) :783
│ → cleaned_html + media(图/音/视/表) + links + metadata

├─ ② MarkdownGenerator.generate_markdown(...) :871
│ → raw_markdown / markdown_with_citations / fit_markdown
│ (按 content_source 选 raw_html / cleaned_html / fit_html 当输入)

└─ ③ ExtractionStrategy(可选) :895-949
→ 按 input_format 选内容 → chunk → run/arun → extracted_content(JSON)

一个值得注意的「短路」:prefetch 模式(:742-761)——如果只想快速发现链接、不要正文,aprocess_html 直接用 quick_extract_links 抽链接就返回,跳过全部清洗/Markdown,快 5-10 倍(README 提到的 prefetch)。

这三步分别是:

5. 产物:CrawlResult

主线最终返回一个 CrawlResultContainer 包着的 CrawlResult(models.py:130)。它是个 Pydantic 模型,常用字段:

字段是什么
html原始 HTML
cleaned_html清洗后的 HTML
markdownMarkdownGenerationResult:含 raw_markdown / markdown_with_citations / fit_markdown
extracted_content结构化抽取的 JSON 字符串(若开了抽取)
media / links / tables图片音视频、内外链、表格
screenshot / pdf / mhtml可选的页面快照
success / status_code / error_message成败信息

CrawlResultContainer 是个兼容层:对单结果它代理属性访问,所以 result.markdownresult.html 直接能用(async_webcrawler.py:242-244 的 docstring,models.py:290CrawlResultContainer)。

6. 批量:arun_many 与调度器

爬多个 URL 用 arun_many(async_webcrawler.py:973)。它不自己开循环,而是交给一个 dispatcher(默认 MemoryAdaptiveDispatcher,:1059)。dispatcher 负责并发:

  • max_session_permit 限制同时几个页面。
  • MemoryAdaptiveDispatcher 会盯着进程内存(psutil),超过 memory_threshold_percent(默认 90%)就暂缓发新任务(async_dispatcher.py:148-206),防止 OOM。
  • RateLimiter 控制每域名的请求间隔与退避重试。

特例:如果 config 设了 deep_crawl_strategy,arun_many 绕过 dispatcher,对每个 URL 直接调 arun(让深爬装饰器接管),因为深爬返回的是「一批结果」而 dispatcher 只认单个结果(:1026-1052)。

7. 代码地图

主题文件路径符号名
单页主线crawl4ai/async_webcrawler.pyAsyncWebCrawler.arun
处理三步crawl4ai/async_webcrawler.pyAsyncWebCrawler.aprocess_html
深爬装饰器crawl4ai/deep_crawling/base_strategy.pyDeepCrawlDecorator
缓存判定crawl4ai/cache_context.pyCacheModeCacheContext.should_read
缓存新鲜度crawl4ai/cache_validator.pyCacheValidator.validate
反爬检测crawl4ai/antibot_detector.pyis_blocked
批量调度crawl4ai/async_dispatcher.pyMemoryAdaptiveDispatcher.run_urls
结果模型crawl4ai/models.pyCrawlResultCrawlResultContainer