爬虫写了几年,requests 用得比筷子还顺手,但这两年明显感觉有点跟不上趟了。以前抓网页,最烦的是解析,正则写半天,xpath 调半天,好不容易跑通了,网站改个版又废了。现在大模型能把自然语言变成结构化指令,爬虫这个领域也跟着变天。crawl4ai 就是在这个背景下冒出来的一个库,主打让大模型直接参与网页到结构化数据的转换过程。我上手试了一段时间,说实话,这东西的思路和传统爬虫完全不是一个路子,用好了确实省事,但坑也不少。
这篇文章不打算写成官方文档的翻译版。我就按照自己实际摸索下来的顺序,聊聊 crawl4ai 到底解决了什么问题、核心组件怎么用、结构化输出怎么调参,以及我在实测中踩过的那些坑。
1. crawl4ai 解决的问题,其实不是爬虫本身
先说结论:crawl4ai 不是让你告别爬虫,它是把爬虫链路里最费人的两个环节——页面解析和数据结构化——交给大模型来做。
1.1 传统爬虫的结构化数据流程有多痛
传统流程大概是这样的:
- 用 requests 或 httpx 发请求,拿到 HTML。
- 分析网页结构,用 XPath、CSS 选择器或者正则表达式提取标题、正文、链接。
- 把提取结果塞进自建的字典或模型类,清洗、去重、落库。
- 网站改版,重新分析结构,然后回第 2 步。
这套流程的问题不在于"不会写",而在于"太脆"。尤其当你面对的是十几个不同来源的网站,每个网站都得单独写一套解析规则,每套规则还动不动就失效。更别提那些内容挂在 JS 异步渲染里的页面,requests 拿到的 HTML 根本什么都没有,又得上 Selenium 或 Playwright。整个链条下来,60% 的时间花在写选择器和调试解析上,真正跟数据打交道的精力反而少。
1.2 crawl4ai 改变了链路中的哪一环
crawl4ai 的核心思路是把"解析"这件事从手工选择器切换成大模型能力。它做了几件关键的事:
- 内置 Playwright 解析 JS 渲染后的页面,不需要你额外写浏览器控制代码。
- 提供两种内容过滤策略(启发式修剪和基于关键词的 BM25 过滤),先把无关导航页脚广告去掉,再喂给大模型。
- 支持把 LLM 的 JSON 输出直接映射到你自己定义的 Pydantic 模型或 JSON Schema 上。
这意味着什么?意味着你只需要告诉模型"我要这个页面的标题、作者、发布日期,输出成 JSON",它就能返回结构化的字段。不用写选择器,不用等页面结构稳定,不用在 chrome devtools 里反复右键 copy selector。
当然,这只是理想状态。实际用下来,crawl4ai 并不能完全消灭写代码,但它确实把人力从"维护解析规则"里解放了出来,转向"设计 schema 和调 prompt",这个转向我认为是值得的。
2. crawl4ai 的核心组件和运行流程拆解
从代码层面看,crawl4ai 的逻辑链条相当清晰,主要围绕浏览器配置、爬取运行配置、内容过滤器和 LLM 配置四类对象展开。
2.1 BrowserConfig:管好看不见的浏览器
crawl4ai 把 Playwright 的浏览器封装成了一个 BrowserConfig 对象。你可以用它控制浏览器是 headless 还是有头、是否启用代理、是否忽略 HTTPS 错误、设置 user agent 等等。这里有一个比较实用的参数是headless,调试阶段可以关了看真实渲染效果,跑量的时候再开回去。
from crawl4ai import BrowserConfig browser_config = BrowserConfig( headless=True, ignore_https_errors=True, user_agent_mode="random", java_script_enabled=True, )注意,java_script_enabled默认就是 True,不用手动关,除非你明确只爬静态页面想加速。很多第一次接触的朋友不知道这一点,以为需要额外开启 JS 渲染,实际上 crawl4ai 的爬取引擎本身就是围绕 JS 渲染设计的,你不需要特意去"启用",它本来就是开着的。
2.2 CrawlerRunConfig:爬取行为的总闸
CrawlerRunConfig 是整个爬取动作的核心配置,指定了如何加载目标 URL,如何过滤内容,是否执行 JavaScript,以及如何把结果交给 LLM。它的设计思路是"一次配置、多次复用",很适合在生产环境里针对不同网站维护多套配置。
from crawl4ai import CrawlerRunConfig run_config = CrawlerRunConfig( css_selector="article.main-content", word_count_threshold=10, extraction_strategy="LLM", )字段比较多,但重点理解extraction_strategy。它的取值决定了走"普通爬取提取"路线,还是"LLM 提取"路线。如果你想省时省力拿纯文本,可以走普通爬取加内容过滤;如果你要的是字段化数据,就交给 LLM 提取策略。
2.3 LLMConfig:决定消息最终给你什么格式
这是 crawl4ai 最出彩的一层。你可以在 LLMConfig 里指定模型(比如 OpenAI 兼容接口、deepseek、本地 Ollama 等)、温度、最大令牌数等参数。然后配合 instruction 告诉大模型输出规则。比如你想从一篇新闻页提取标题、作者、时间、正文,就写:
from crawl4ai import LLMConfig llm_config = LLMConfig( provider="openai/gpt-4o-mini", api_token="你的密钥", temperature=0.2, max_tokens=2048, )温度调低一点,是为了让模型输出更稳定,这对结构化提取非常重要。做数据提取不是写文案,温度 0.2 以下才是合理区间。
2.4 把四类配置拼装起来执行的完整流程
理解了上面几个对象之后,实际调用的全貌就很清楚了。这里给一个入门级的同步示例:
import asyncio from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, LLMConfig async def main(): browser_config = BrowserConfig(headless=True) run_config = CrawlerRunConfig( extraction_strategy="LLM", llm_config=LLMConfig( provider="openai/gpt-4o-mini", api_token="你的密钥", ), instruction=""" 请从页面中提取以下信息,以 JSON 格式返回: { "title": "文章标题", "author": "作者", "published_at": "发布日期", "content": "正文内容" } 只需要返回 JSON,不要额外解释。 """, ) async with AsyncWebCrawler(config=browser_config) as crawler: result = await crawler.arun(url="https://example.com/news/123", config=run_config) print(result.extracted_content) asyncio.run(main())result.extracted_content里就是模型返回的结构化内容,通常是 JSON 字符串,可以直接再用json.loads解析成 Python 对象。
这整套流程跑通之后,你会发现"爬取页面—渲染 JS—过滤噪声—LLM 抽取—结构化输出"首次被串成了一条完整链路,而且每一步都有明确的配置对象在支撑。
3. 把网页变成结构化数据的完整实操
看官方文档和自己动手完全是两回事。这里我拿一个真实的演示站点跑一遍,把具体步骤、参数怎么调、输出长什么样都摆出来。官方文档里用的例子是 Arxiv(https://arxiv.org/abs/2411.15243),这是一个 arXiv 论文详情页,页面结构规整,非常适合拿来验证流程。
3.1 写一个最简的可运行脚本
我建议你直接复制下面这段,先跑通再慢慢加参数:
import asyncio, json from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, LLMConfig async def extract_arxiv(): browser_cfg = BrowserConfig(headless=True) llm_cfg = LLMConfig( provider="openai/gpt-4o-mini", api_token="你的密钥", temperature=0.1, ) run_cfg = CrawlerRunConfig( extraction_strategy="LLM", llm_config=llm_cfg, instruction=""" 从页面中提取论文信息,返回 JSON 格式,字段如下: { "title": "论文标题", "authors": ["作者1", "作者2"], "abstract": "摘要", "subjects": ["学科分类1", "学科分类2"], "submitted_date": "提交日期" } 只返回 JSON。 """, ) async with AsyncWebCrawler(config=browser_cfg) as crawler: result = await crawler.arun(url="https://arxiv.org/abs/2411.15243", config=run_cfg) print("抓取状态:", result.success) print("原始HTML长度:", len(result.html)) print("LLM提取结果:", result.extracted_content) asyncio.run(extract_arxiv())我实测的结果是,success为 True,extracted_content返回了合法的 JSON 字符串,里面包含了论文标题、作者列表、摘要等字段。这是 crawl4ai 最典型的使用姿势——告诉模型要什么、给什么格式,它自己从页面内容里抽。
3.2 用中文 prompt 和自定义 schema 提取,效果一样好
之前有人说中文 prompt 大模型理解得不如英文,实测下来在 gpt-4o-mini 上效果差别不大。我更习惯直接把 schema 写进 instruction 里,同时指定输出模式为STRICT,这样模型会严格按照 JSON Schema 输出,不搞自由主义。
run_cfg = CrawlerRunConfig( extraction_strategy="LLM", llm_config=llm_cfg, instruction=""" 从页面提取以下信息,严格按照给定 schema 输出 JSON: { "标题": "论文标题,字符串", "作者列表": ["字符串数组"], "摘要": "论文摘要,字符串", "学科分类": ["字符串数组"], "投稿时间": "日期字符串" } 输出字段名必须保持一致,不要添加多余内容。 """, )如果你用的模型对中文键名支持不够好,也可以保留英文字段名,只把 prompt 描述写成中文。这是我在多次测试里常用的折中方案,灵活度和准确率都更稳。
3.3 结果解析与模型类型定义的衔接
拿到extracted_content之后,很多人会问:这跟 Pydantic 模型怎么衔接?官方没有强迫你用 Pydantic,你可以直接用json.loads转成字典。但如果你喜欢类型安全,建议自己定义一个 Pydantic 模型,然后做一次解析校验:
from pydantic import BaseModel from typing import List import json class ArxivPaper(BaseModel): title: str authors: List[str] abstract: str subjects: List[str] submitted_date: str # 假设 result.extracted_content 已经拿到 data = json.loads(result.extracted_content) paper = ArxivPaper(**data) print(paper.title, len(paper.authors))这样就把大模型的自由文本输出锁死在了你自己的数据结构里,后面无论写数据库还是做接口,都是非常确定的形态。
注意:如果模型偶尔输出了解释性文字(比如"好的,我为你提取了以下信息"),会导致 json.loads 失败。建议在 instruction 里强调"只返回 JSON",再配合
STRICT模式,基本能避免这个问题。
4. 三个最影响结果质量的参数细节
工具链搭起来只需要十分钟,但要让结果稳定、质量高,有几个参数和细节需要专门调优。这一节我把现场容易忽略的点都摊开讲。
4.1 word_count_threshold:过滤短文本噪声
word_count_threshold是内容过滤器里的一个重要阈值,表示少于多少个单词的文本块会被丢弃。默认一般是 2~3,但如果页面里充斥大量短文本(比如按钮文字、标签、浮动提示),可以适当调高到 5~10,让最终喂给 LLM 的正文更干净。
run_cfg = CrawlerRunConfig( word_count_threshold=8, extraction_strategy="LLM", llm_config=llm_cfg, )这个参数调得好,能直接减少大模型被噪声干扰的概率,提升字段提取准确率。
4.2 temperature 和 max_tokens 的取舍
结构化提取场景里,temperature 一定要低,我推荐 0.1~0.2。高温度会带来"创造性输出",但我们要的是从页面里找答案,不是要它发挥。
max_tokens 则取决于你要提取的内容长度。摘要或者短文章,2048 足够;如果是长文正文,建议 4096 以上。设置得太小,JSON 会被截断,解析必炸。
4.3 使用内容过滤器和 CSS 选择器缩小问题域
LLM 不是万能的,页面越大、噪声越多,错误率越高。所以正确做法是先帮模型做减法:
- 用
css_selector只选正文区域,如article.main-content; - 用
PruningContentFilter或BM25ContentFilter自动删掉导航、页脚、广告; - 再让模型基于清洗后的内容做提取。
一个组合示例:
from crawl4ai import CrawlerRunConfig from crawl4ai.content_filter_strategy import PruningContentFilter, BM25ContentFilter run_cfg = CrawlerRunConfig( css_selector="main", content_filter=PruningContentFilter(threshold=0.45, threshold_type="dynamic"), extraction_strategy="LLM", llm_config=llm_cfg, )这样做的本质是缩小问题域。模型拿到的如果是干净正文,提取准确率天然就高。我实测同一个页面,不做过滤直接让 LLM 抽,和做了过滤再抽,字段完整度差距非常明显。
5. 实测对比:传统方法与 crawl4ai 各来一遍
拿同一个 arXiv 页面,我分别用 requests + BeautifulSoup 的传统方法和 crawl4ai + LLM 的方式跑了一遍,把差异摊开,你看完就明白什么时候该用什么。
5.1 传统方式:requests + BeautifulSoup
传统方式的代码其实很简单,但脆弱性体现在选择器上:
import requests from bs4 import BeautifulSoup resp = requests.get("https://arxiv.org/abs/2411.15243") soup = BeautifulSoup(resp.text, "html.parser") title = soup.select_one("h1.title").text.strip() authors = [a.text for a in soup.select("div.authors a")] abstract = soup.select_one("blockquote.abstract").text.strip()跑这个页面没问题,但换个网站就得重新分析结构。而 arXiv 页面结构相对稳定,所以传统方式在单一来源、长期稳定的场景里依然有优势——不消耗 token,速度快,延迟低。
5.2 crawl4ai 方式:费 token 但省心
crawl4ai 方式写好了通用提取 prompt 和 schema 之后,换一个完全不同的站点,只要页面内容能覆盖目标字段,基本不用改代码。这在多源聚合、页面经常改版、不规律页面的场景下优势极大。
两种方式的取舍我整理成了下面的表:
| 维度 | 传统解析(requests + BS) | crawl4ai + LLM |
|---|---|---|
| 开发速度 | 需要逐站写选择器,慢 | 写一份 schema 通用,快 |
| 抗改版能力 | 差,改版即崩 | 强,页面结构变化影响小 |
| 运行成本 | 几乎为零 | 每次请求消耗 LLM token |
| 部署复杂度 | 低 | 需要管理浏览器和 LLM 服务 |
| 适合场景 | 单站固定结构、大规模长期爬取 | 多源异构、字段不固定、页面常变 |
这个对比应该能帮你做选型判断。量特别大、目标站点非常稳定的时候,我建议还是走传统方式加缓存,省钱。量不大但来源杂乱,或者需要提取的字段不在 DOM 里能直接映射的位置,那 crawl4ai 完胜。
5.3 异步批量运行的实测记录
crawl4ai 也支持异步批量爬取。我拿了一个论文列表页里的 5 个链接做并发抓取,简单封装了一下:
import asyncio from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, LLMConfig async def crawl_batch(): urls = [ "https://arxiv.org/abs/2411.15243", "https://arxiv.org/abs/2408.12345", "https://arxiv.org/abs/2406.67890", ] browser_cfg = BrowserConfig(headless=True) llm_cfg = LLMConfig(provider="openai/gpt-4o-mini", api_token="你的密钥", temperature=0.1) run_cfg = CrawlerRunConfig( extraction_strategy="LLM", llm_config=llm_cfg, instruction="提取论文标题、作者、摘要,只输出 JSON。", ) async with AsyncWebCrawler(config=browser_cfg) as crawler: results = await crawler.arun_many(urls=urls, config=run_cfg) for i, r in enumerate(results): print(i, r.success, r.status_code, r.extracted_content[:80]) asyncio.run(crawl_batch())实测下来,3 个页面的并发抓取在 20 秒左右完成,每页耗时大概 6~7 秒。相比传统方式当然是慢的,但换来的是不用维护任何选择器。如果你跑的是上千级别的量,可以加一个简单的信号量限制并发数,避免被目标网站或者大模型接口限流:
semaphore = asyncio.Semaphore(5) async def limited_run(url): async with semaphore: async with AsyncWebCrawler(config=browser_cfg) as crawler: r = await crawler.arun(url=url, config=run_cfg) return r6. 避坑清单与执行策略
这部分是实打实踩出来的经验。直接给结论,不绕弯子。
6.1 懒加载内容会丢,需要先滚动
很多现代网页的内容不是一次性渲染完的,需要滚动到可视区域才触发加载。crawl4ai 提供了js_code和wait_for参数来应对。可以在js_code里注入滚动脚本:
run_cfg = CrawlerRunConfig( js_code="window.scrollTo(0, document.body.scrollHeight);", wait_for="css:.comment-list", )如果是瀑布流页面,滚动一次不够,就得循环滚几次。这个操作等价于 Selenium 里的自动下拉,忘了它会导致正文缺失。
6.2 页面有 cookie 弹窗或登录墙,最简单的是直接换入口
crawl4ai 提供了cookie参数和set_cookie操作,但说实话,遇到强 cookie 校验的时候,手动配置会很繁琐。我的经验是:先看是否有移动版页面或者 API 接口,很多时候绕过前端弹窗比硬拼页面渲染省事得多。你可以在CrawlerRunConfig里注入 cookie 字符串:
run_cfg = CrawlerRunConfig( cookie="name=value; sessionid=xxxx", )但如果是复杂的登录态保持,建议你还是先在浏览器里把 cookie 导出,再写进配置里。
6.3 LLM 输出偶尔会飘,必须有重试和校验
大模型的输出本质是概率性的,即使同一个页面跑十次,也不能保证每次结果都完全一致。我的做法是:
- 对
extracted_content做json.loads校验; - 用 Pydantic 模型做字段必填校验;
- 失败就重试一次,最多重试两次,避免无限循环烧钱;
- 重试时换一个更低 temperature 的配置。
import json, time from pydantic import ValidationError def parse_with_retry(result, model, tries=2): for i in range(tries): try: data = json.loads(result.extracted_content) return model(**data) except (json.JSONDecodeError, ValidationError) as e: if i == tries - 1: raise time.sleep(1)这套兜底逻辑能显著提升整体任务的成功率,不会因为某一次的模型输出异常导致全流程崩溃。
6.4 并发和限流:控制好自己,别惹毛对方和大模型
批量抓取时,限制 Playwright 浏览器的并发数、随机化每个请求的间隔时间,是基本的爬虫礼仪。crawl4ai 的arun_many很方便,但别一上来就开 50 个并发。我推荐一开始跑 3~5 个,稳定后再往上加。另外,LLM 接口也有速率限制,并发太高会直接收到 429,那时候就不是页面抓不到的问题,而是整个任务卡死。
6.5 其他容易踩的坑
- CSS 选择器不生效时,先确认页面是不是在 iframe 里,Playwright 不一定能直接穿透所有 iframe 结构。
- arXiv 这类页面反爬比较宽松,但很多国内站点会校验 TLS 指纹和 header 顺序,单纯换个 UA 没用,必要时开启 stealth 模式。
- 如果目标网站有大量图片,crawl4ai 会把图片链接也保留下来,你要在 instruction 里明确"忽略图片链接"或者后续自己做过滤。
7. 从爬虫到 Agent:两条进阶路线
crawl4ai 用熟练之后,你会发现它的定位远不止"更好用的爬虫"。它更大的价值在于给 AI Agent 提供"读取网页并理解网页"的能力。这里给两个我认为很实用的进阶方向。
7.1 给 Agent 提供网页阅读能力
传统 RAG 方案里,Agent 读网页靠的是 URL 链接 + 抓取 + 文本切块。但网页里 80% 都是导航、版权、广告信息,直接抓下来塞给 Agent 会导致检索质量下降。现在你可以用 crawl4ai 的过滤策略先把正文洗干净,再进 RAG,效果完全不一样。
更进一步的玩法是"按需提取"——Agent 在回答用户问题时,如果发现自己的知识库不够,可以直接调 crawl4ai 抓取相关网页,并指挥 LLM 提取特定字段。这种"检索即提取"的模式,其实已经在往 Agentic Search 的方向走了。
7.2 与爬虫管理平台结合,批量采集不再依赖逐站配置
如果你维护过类似 Scrapy 的爬虫项目,一定经历过"新站点接入要写一套 pipeline"的痛。用 crawl4ai,可以做一个通用抽取服务:
- 前端传入 URL 和目标字段 schema;
- 后端用 crawl4ai 抓取页面;
- 用同一个 LLM 配置按 schema 提取;
- 输出标准化 JSON 落库。
这样一来,业务方不需要写任何爬虫代码,只要描述"我想要什么",系统就能返回对应字段。这就是"大模型逆向爬虫"味道最浓的落地场景。
结合最近社区里的讨论,crawl4ai 的 0.7 版本之后还加入了更多云服务支持和策略扩展点,性能和兼容性一直在迭代。我曾经在某个遍历多个站点抓行业资讯的项目里,用同一套代码跑了三类结构完全不同的页面,只改 prompt 里的字段描述,就全部跑通了,这在以前手动写解析规则的时代想都不敢想。如果你正卡在多源采集和数据结构化的老问题上,这套思路值得认真试一试。