数据截至 (上游 commit 7e801521428e)
Crawl4AI — 架构与原理
30 秒导读: Crawl4AI 是一个开源异步网页爬虫,专门为「喂给大模型」而生。你给它一个 URL,它用真实浏览器把页面抓下来,清掉导航/广告/脚本,转成带标题、表格、引用编号的干净 Markdown;需要的话还能按你给的 CSS schema 或一句自然语言抽出结构化字段。全程不需要任何第三方 API key(除非你主动开 LLM 抽取)。
1. 这是什么(零基础也能懂)
一句话定义: 把「网页」变成「LLM 能直接读的干净 Markdown + 结构化数据」的爬虫库。
解决什么问题 / 给谁用。 假设你在做 RAG(检索增强生成)或者一个会上网的 agent,你需要把几百个网页的正文喂给模型。直接拿原始 HTML 喂?里面全是 <script>、导航栏、广告、cookie 弹窗、嵌套 <div>——既烧 token 又干扰模型。Crawl4AI 就是站在「拿到网页」和「喂给模型」之间的那一层:把脏 HTML 洗成干净文本。
它的典型使用者:
- 做 RAG 数 据管道的工程师——批量把文档站点转成 Markdown 入库。
- 写联网 agent 的人——让 agent 实时读网页。
- 做数据抓取的人——从商品页、列表页抽结构化字段。
它能做什么(功能):
- 用真实浏览器(Playwright)抓取,能跑 JS、点按钮、滚动懒加载页面。
- 把 HTML 清洗成干净 Markdown,链接转成引用编号
⟨1⟩+ 文末 References。 - 生成
fit_markdown——只留「正文」、扔掉边角料的精简版。 - 三种结构化抽取:CSS/XPath schema(纯 Python,免费)、正则、LLM。
- 多页爬取:深度爬(BFS/DFS/最佳优先)和「答够了就停」的自适应爬。
- 缓存、并发调度、反爬重试、代理轮换、会话复用。
用起来什么样。 最小的真实例子(来自 README):
import asyncio
from crawl4ai import AsyncWebCrawler
async def main():
async with AsyncWebCrawler() as crawler: # 启动浏览器
result = await crawler.arun(url="https://example.com")
print(result.markdown) # 干净 Markdown
asyncio.run(main())
或者命令行一行: crwl https://example.com -o markdown。
一句话直觉/类比。 把 Crawl4AI 想成网页世界的「榨汁机」:丢进去一整颗带皮带核的水果(原始 HTML),出来一杯只剩果汁的东西(Markdown)。而且每一道工序——怎么抓、怎么削皮、怎么过滤、怎么装瓶——都是可换的零件(strategy),你不满意默认件就换一个。
2. 顶层全景(它大概怎么转)
Crawl4AI 的核心是一条单页处理 流水线:AsyncWebCrawler.arun(url) 是入口,它把工作派给一串可插拔策略,最后吐出一个 CrawlResult。
怎么读这张图: 从左到右是一次 arun() 的数据流向;每个方框是一个可替换的策略对象,下面标了它在哪个文件。
URL
│
▼
┌──────────────┐ 命中缓存? ┌─────────────┐
│ AsyncWebCrawler├───────────►│ SQLite 缓存 │ async_database.py
│ .arun() │◄───────────┤ (~/.crawl4ai)│
└──────┬───────┘ 未命中 └─────────────┘
│ 抓取
▼
┌────────────────────┐
│ CrawlerStrategy │ async_crawler_strategy.py
│ (Playwright 浏览器 │ → 跑 JS / 滚动 / 截图
│ 或 HTTP 轻量抓取) │ → 原始 HTML
└────────┬───────────┘
│ 反爬检测 + 重试 + 代理轮换
▼
┌────────────────────┐
│ aprocess_html() │ async_webcrawler.py ← 处理主线
│ ① ScrapingStrategy│ 清洗 DOM → cleaned_html + media/links/tables
│ ② MarkdownGen │ HTML → Markdown(+引用化 + fit_markdown)
│ ③ ExtractionStrat │ 可选:结构化抽取(schema/正则/LLM)
└────────┬───────────┘
│
▼
CrawlResult (html / cleaned_html / markdown / extracted_content / media / links …)
部件一句话职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
AsyncWebCrawler | 总编排:缓存判定 → 抓取 → 反爬重试 → 调 aprocess_html | async_webcrawler.py |
AsyncPlaywrightCrawlerStrategy | 用真浏览器抓页面(跑 JS、滚动、截图) | async_crawler_strategy.py |
AsyncHTTPCrawlerStrategy | 不开浏览器的轻量 HTTP 抓取 | async_crawler_strategy.py |
LXMLWebScrapingStrategy | 清洗 DOM,产出 cleaned_html + 媒体/链接/表格 | content_scraping_strategy.py |
DefaultMarkdownGenerator | HTML→Markdown,链接转引用,生成 fit_markdown | markdown_generation_strategy.py |
PruningContentFilter / BM25ContentFilter | 决定哪些块进 fit_markdown | content_filter_strategy.py |
JsonCss/Regex/LLMExtractionStrategy | 抽结构化数据 | extraction_strategy.py |
DeepCrawlStrategy / AdaptiveCrawler | 多页:深爬 / 信息够了就停 | deep_crawling/、adaptive_crawler.py |
MemoryAdaptiveDispatcher | arun_many 的并发调度 + 内存自适应 | async_dispatcher.py |
主线走一遍(高层,不进代码):
- 你调
crawler.arun(url, config)。 - 看缓存:
config.cache_mode决定要不要读缓存。命中就直接返回。 - 没命中就抓:交给 crawler strategy,得到原始 HTML(顺带截图/PDF/网络日志)。
- 反爬检查:
is_blocked()看是不是被 Cloudflare/Akamai 拦了;被拦就换代理重试,最后还能调fallback_fetch_function。 - 处理(
aprocess_html):清洗 → Markdown → (可选)抽取。 - 写缓存,返回
CrawlResult。
3. 阅读地图(建议顺序)
这个项目很大(核心模块单文件动辄两三千行)。本文按流水线顺序拆成 6 章,建议从头读:
- 01-pipeline.md — 先看主线:
arun()怎么把缓存、抓取、反爬、处理串起来。读完你就有了整张地图。 - 02-fetch-browser.md — 抓取层:Playwright 怎么跑 JS、
smart_wait怎么等页面、虚拟滚动怎么处理无限列表。 - 03-clean-and-markdown.md — 清洗 + Markdown 化:DOM 怎么被洗干净、链接怎么变成
⟨1⟩引用。 - 04-content-filters.md — fit_markdown 的两条路:Pruning 启发式打分裁剪 vs BM25 查询相关裁剪。这是「LLM 友好」的核心。
- 05-extraction.md — 结构化抽取:免费的 CSS schema、纯正则、以及 LLM 抽取的取舍。
- 06-deep-and-adaptive.md — 多页:深爬策略 + 自适应爬取「信息够了就停」的统计学。
如果你只想用、不想钻原理:读完第 1 章 + 第 4 章就够建一个 RAG 管道了。
4. 巧妙之处(先记住这几条)
- 一切皆策略。 抓取、清洗、Markdown、过滤、抽取、深爬——每一步都是抽象基类 + 可替换实现。
CrawlerRunConfig把它们组装起来。这让「换个清洗算法」不用改主流程。 fit_markdown是杀手锏。 普通 Markdown 还是太多;fit_markdown用启发式或 BM25 只留正文,直接喂模型省 token。见第 4 章。- 反爬是「检测 + 救援」两段式。
is_blocked()宁可误报(便宜,有 fallback 兜底)也不漏报(漏了用户拿到垃圾),见antibot_detector.py顶部注释。 - 自适应爬取把「爬够了吗」量化成数学。 coverage / consistency / saturation 三个指标算出一个 confidence,够阈值就停,见第 6 章。
5. 边界与局限(诚实)
- 重依赖浏览器。 默认走 Playwright + Chromium, 装起来不轻(要
crawl4ai-setup下载浏览器)。要轻量可用AsyncHTTPCrawlerStrategy,但就跑不了 JS 了。 - 反爬是军备竞赛。 内置检测能识别 Cloudflare/Akamai/PerimeterX 等的拦截页,但「识别被拦」不等于「能绕过」;绕过靠 undetected 浏览器适配器 + 代理,且不保证成功。
- LLM 抽取/过滤要花钱、要联网。 schema/正则路径是纯 Python 免费;一旦用
LLMExtractionStrategy或LLMContentFilter就走 litellm 调外部模型。 - 自适应爬取的统计策略偏简单。 coverage 用的是查询词的 TF/DF,不是语义理解(语义版需要 embedding 模型,见第 6 章)。
6. 横向对比(同 shelf 的 RAG/检索兄弟)
Crawl4AI 在 rag-retrieval 区里占的是**「数据入口/ingestion」这一格:它不做向量检索、不做 rerank,而是把网页洗成可入库的干净文本**。下游才接 embedding、向量库、检索。换句话说,别的 RAG 库回答「我有干净文档了,怎么检索」;Crawl4AI 回答「文档还在脏网页里,怎么洗出来」。
7. 代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 总编排 / 主线 | crawl4ai/async_webcrawler.py | AsyncWebCrawler.arun、AsyncWebCrawler.aprocess_html |
| 浏览器抓取 | crawl4ai/async_crawler_strategy.py | AsyncPlaywrightCrawlerStrategy._crawl_web、smart_wait |
| HTTP 轻量抓取 | crawl4ai/async_crawler_strategy.py | AsyncHTTPCrawlerStrategy.crawl |
| DOM 清洗 | crawl4ai/content_scraping_strategy.py | LXMLWebScrapingStrategy.scrap、process_element |
| Markdown 生成 | crawl4ai/markdown_generation_strategy.py | DefaultMarkdownGenerator.generate_markdown、convert_links_to_citations |
| 内容过滤 | crawl4ai/content_filter_strategy.py | PruningContentFilter._prune_tree、BM25ContentFilter.filter_content |
| 结构化抽取 | crawl4ai/extraction_strategy.py | JsonElementExtractionStrategy.extract、LLMExtractionStrategy.run、RegexExtractionStrategy |
| 深度爬取 | crawl4ai/deep_crawling/bfs_strategy.py | BFSDeepCrawlStrategy.link_discovery |
| 自适应爬取 | crawl4ai/adaptive_crawler.py | StatisticalStrategy.calculate_confidence、CrawlState |
| 并发调度 | crawl4ai/async_dispatcher.py | MemoryAdaptiveDispatcher.run_urls |
| 反爬检测 | crawl4ai/antibot_detector.py | is_blocked |
| 缓存模式 | crawl4ai/cache_context.py | CacheMode、CacheContext |
| 结果模型 | crawl4ai/models.py | CrawlResult、MarkdownGenerationResult |