尧图精选

crawl4ai:AI驱动的网页数据结构化提取实战

🕒 发布时间:2026/9/8 5:18:46 📁 来源:尧图网络
爬虫这个事做了这么多年我最大的感受就是抓数据从来不是最难的抓完之后怎么把一坨 HTML 变成能直接进数据库、进 Excel、进报表的结构化数据才是最烦人的。尤其到了 AI 时代网页千奇百怪改动又频繁传统爬虫写选择器写到手软结果页面一改版全部报废。crawl4ai 这个开源项目我盯了挺久最近上手跑通后确实有点惊艳它把“抓取”和“理解”两件事合并到了一起输出直接给你 JSON配合 LLM 还能按你定义的 schema 自动提取字段非常适合做 AI 数据采集、RAG 知识库构建、竞品监控这些方向。这篇内容我尽量写得偏实操目标读者是爬虫工程师、AI 应用开发者和数据工程师。你不需要很深的爬虫基础但是至少得会 Python懂得基本的 HTML 结构后面代码拿过去能直接改着用。1. crawl4ai 是什么以及为什么需要它1.1 传统爬虫的痛点每次我跟团队里新人聊爬虫都会先问一个问题你花在写解析规则上的时间和花在抓取上的时间哪个多答案基本都是前者。传统爬虫的套路很简单用 requests 拿 HTML用正则或者 XPath/CSS 选择器抽字段最后再拼接成一个 dict。这套流程在页面结构稳定的情况下没问题但实际业务里页面会改版会加参数会搞反爬最典型的情况是电商平台一个商品列表页今天抓 title 是h3 a明天变成div.product-name你的解析代码就要重写一遍。还有一类页面是纯 JS 渲染的requests 拿回来的 HTML 里根本找不到完整数据你得再上 Selenium 或 Playwright处理等待、点击、滚动复杂度直接上一个台阶。等你终于把数据抓下来了清洗、去重、格式化又是一堆重复劳动。所以爬虫真正的痛点不在“爬”而在“把非结构化页面变成结构化数据”这个过程太脆弱、太昂贵。1.2 crawl4ai 的定位与特性crawl4ai 是 GitHub 上一个比较活跃的 Python 爬虫项目主打“AI-ready”的数据抓取。它不是一个单纯帮你发 HTTP 请求的工具而是把整个数据管线打包好了爬取、HTML 清理、正文提取、结构化字段抽取、JSON 输出。我实际跑下来觉得它几个特性是直击痛点的内置 Playwright能跑动态页面也能退化成普通 HTTP 模式速度快很多。爬完之后自动生成干净的 Markdown这个特别适合做 RAG 知识库喂给向量模型之前不需要再做太多清洗。支持两种提取方式一种是用 CSS 选择器做精准字段抽取字段名、类型、默认值都自己定义另一种是接 LLM用自然语言指令配合 schema 做语义抽取。输出统一是 JSON方便下游任务直接消费。最吸引我的是它对“LLM 提取”的封装做得不错不需要自己去拼 Prompt、解析大模型返回结果只要定义好 schema它会把页面正文或者 HTML 片段喂给模型然后帮我把结果解析成结构化 JSON。1.3 哪些场景适合用这类工具不是万能药但我认为在下面这些场景里非常值得试知识库 / 问答系统把网页文档、博客、帮助中心文章抓下来转成 Markdown 或 JSON再切分向量化。商品监控抓电商列表页的标题、价格、评分、库存做价格变动告警。新闻资讯聚合把不同站点的新闻标题、发布时间、正文统一成一种 schema。论文元数据采集从学术页面里抽标题、作者、摘要、DOI。企业数据中台作为前端采集层把各地业务系统页面数据统一入库。换句话说凡是“需要把网页内容变成可复用、可搜索、可分析的数据”的需求都是它的地盘。2. 环境准备与快速安装2.1 安装并不复杂但有两个坑crawl4ai 的安装看起来就是一条命令但实际上有两点比较容易踩坑。pip install crawl4ai playwright install chromium第一个坑是 Python 版本。它要求 Python 3.9 以上我建议直接用 3.11 或 3.12别在 3.8 上浪费折腾时间。第二个坑是 Playwright 的浏览器内核。crawl4ai 默认走 HTTP 请求不需要浏览器但一旦你碰到动态页面想开启渲染就得有 Chromium。上面第二条命令是必须的如果安装失败多半是网络问题多试几次或者确保你的系统装了 Playwright 依赖的系统库。在 Linux 服务器上部署时眼睛多看控制台提示缺什么库就补什么库常见的libnss3、libatk这些别漏掉。2.2 跑通第一条异步爬虫crawl4ai 的核心是基于异步的第一次用的时候你可能不习惯还要写asyncio.run。我完整跑过的第一个例子是这样的import asyncio from crawl4ai import AsyncWebCrawler async def main(): async with AsyncWebCrawler() as crawler: result await crawler.arun(urlhttps://example.com) print(状态码:, result.status_code) print(标题:, result.metadata.get(title)) print(Markdown 长度:, len(result.markdown)) print(result.markdown[:500]) if __name__ __main__: asyncio.run(main())印象最深的是result里不只是带了 HTML还自动生成了清掉导航、页脚、脚本标签后的 Markdown。这一步看着简单但对我处理内容型网站帮助特别大因为它相当于把最脏的清洗工作提前做掉了。2.3 理解返回结果里到底有什么很多教程直接让你打印result其实CrawlResult这个对象里的字段值得细看字段作用html原始 HTML适合调试cleaned_html清理后的 HTML去掉了脚本和样式markdown自动生成的 Markdown 正文metadata标题、描述、语言等页面元信息links页面中的所有链接分内链、外链media图片、视频等媒体资源信息screenshot页面截图截图保存成功后有路径pdf如果你开启了 PDF 抓取会返回 PDF 内容status_codeHTTP 状态码extracted_content结构化提取后的 JSON 内容后面重点讲需要注意arun默认不开启截图也不开启 LLM 提取都需要在参数里显式设置。我建议最开始只用默认参数跑通再逐步加能力不然各种代理、延迟、模型配置堆在一起出了问题很难定位。3. 网页到结构化数据的核心链路3.1 三步链路抓取、清理、提取crawl4ai 整个数据处理链路可以拆成三步第一步是抓取。它会用 Playwright 渲染页面等 JS 执行完拿到完整的 DOM。如果页面简单也可以让它用轻量级请求模式速度更快但拿不到 JS 动态填充的内容。第二步是清理。这一步很容易被忽略但它决定了下游数据质量。crawl4ai 会移除script、style、nav、footer这些对正文没用的节点再用算法把正文部分转成 Markdown。对内容型网站来说这一步比你自己写一堆 BeautifulSoup 逻辑要省事得多。第三步是提取。到了这一步你有两条路可以走一条是用 CSS 选择器精确抽字段一条是用 LLM 做语义抽取。前者适合页面结构稳定、字段明确的情况后者适合页面风格多变、字段语义复杂的情况。我实际项目中通常是先用 CSS 策略跑一遍拿到大部分字段只有碰到那些结构乱到没法写选择器的页面才叠一层 LLM 策略兜底。两个策略可以配着用不冲突。3.2 用 CSS 选择器做精准提取CSS 选择器策略对应的类是JsonCssExtractionStrategy。它的核心是定义一个 schema告诉爬虫从哪个区域开始找数据每个字段对应哪个选择器。from crawl4ai.extraction_strategy import JsonCssExtractionStrategy schema { name: Article List, baseSelector: div.article-item, fields: [ { name: title, selector: h2.article-title, type: text, }, { name: link, selector: a, type: attribute, attribute: href, }, { name: publish_date, selector: span.date, type: text, default: 2025-01-01, }, ], } strategy JsonCssExtractionStrategy(schema, verboseTrue)这里要注意几个细节。baseSelector决定遍历哪一个模块页面里所有匹配这个选择器的元素都会被当成一条记录。fields里的type可以有很多值text表示取文本内容attribute表示取属性值还有html、regex、nested这些更高级的用法。我建议刚开始先只试text和attribute等理解了数据流再考虑nested处理嵌套结构。另外一定要给字段设置合理的default因为不是每条记录都长得一模一样缺字段时有个默认值兜底后面入库不容易报错。3.3 用 LLM 做语义提取schema 建模LLM 策略是我用下来最惊喜的部分。以前用 ChatGPT API 做提取要自己拼 Prompt还要处理模型偶尔抽风返回的额外文本累得很。crawl4ai 里的LLMExtractionStrategy把这套流程封装了。from crawl4ai.extraction_strategy import LLMExtractionStrategy strategy LLMExtractionStrategy( provideropenai/gpt-4o-mini, api_token你的API_KEY, schemaproduct_schema, instruction从页面中提取商品信息保持字段完整没有的字段返回空字符串。, )这里的schema是输出格式定义设计逻辑和我们后面要说的结构化数据建模一模一样。它的作用不仅是告诉模型该返回什么字段更是约束模型“只输出合法的 JSON”降低幻觉。你可以把 LLM 策略理解成“你用大白话提需求模型按照你给的表格格式填内容”。你的 schema 设计得越清晰模型输出越稳定返回的 JSON 就越规范。3.4 无 Schema 的开放提取如果只是快速做个原型不关心字段是否固定也可以不传 schema让模型自由发挥。strategy LLMExtractionStrategy( provideropenai/gpt-4o-mini, api_token你的API_KEY, instruction提取文章的核心观点、作者、发布日期。, )这种用法简单适合前期验证“这个页面能不能抽出来数据”。但我不建议直接用在生产环境因为模型自由发挥的结果不稳定同一个页面你抓十次字段可能有细微差异。生产环境还是要用固定 schema 去约束字段名和类型。4. 实际案例抓取一个产品列表页4.1 目标页面分析与字段定义为了更直观我拿一个常见的商品列表页举例。假设页面结构大概是div classproduct-card h3 classproduct-name无线机械键盘/h3 span classprice399/span span classrating4.7/span span classstock有货/span /div现在我想把每个商品卡片抽成这样的结构{ name: 无线机械键盘, price: 399, rating: 4.7, stock_status: 有货 }在写代码之前应该先把字段类型定清楚。price我建议用floatrating用floatstock_status用text。这样下游做统计时不需要再转换一遍。4.2 JsonCssExtractionStrategy 完整配置按照字段定义schema 可以这么写schema { name: Product Card, baseSelector: div.product-card, fields: [ { name: name, selector: h3.product-name, type: text, }, { name: price, selector: span.price, type: text, transform: extract_number, }, { name: rating, selector: span.rating, type: text, transform: extract_number, }, { name: stock_status, selector: span.stock, type: text, }, ], }这里我特意加了transform: extract_number它会把文本里的数字抽出来比如399元会变成399这个功能在处理带单位、带符号的字段时很香。如果页面里价格是399.00也能自动转成399.0少写好几个正则。然后实例化策略from crawl4ai.extraction_strategy import JsonCssExtractionStrategy css_strategy JsonCssExtractionStrategy(schema, verboseTrue)4.3 代码实现与运行效果完整抓取和提取代码我写成下面这样import asyncio import json from crawl4ai import AsyncWebCrawler from crawl4ai.extraction_strategy import JsonCssExtractionStrategy async def main(): schema { name: Product Card, baseSelector: div.product-card, fields: [ {name: name, selector: h3.product-name, type: text}, {name: price, selector: span.price, type: text, transform: extract_number}, {name: rating, selector: span.rating, type: text, transform: extract_number}, {name: stock_status, selector: span.stock, type: text}, ], } strategy JsonCssExtractionStrategy(schema, verboseTrue) async with AsyncWebCrawler() as crawler: result await crawler.arun( urlhttps://example.com/products, extraction_strategystrategy, ) data json.loads(result.extracted_content) print(json.dumps(data, ensure_asciiFalse, indent2)) if __name__ __main__: asyncio.run(main())如果页面结构完全匹配result.extracted_content会是一个 JSON 数组每个元素对应一个商品。[ { name: 无线机械键盘, price: 399.0, rating: 4.7, stock_status: 有货 }, { name: 双模鼠标, price: 199.0, rating: 4.5, stock_status: 有货 } ]注意extracted_content是字符串不是对象所以一定要用json.loads解析一遍再使用。4.4 升级方案LLM 提取避免页面结构变化用 CSS 选择器抓固定页面虽然准确但只要页面换了类名立刻会失效。我做竞品监控的时候最烦这个今天抓div.product-card明天它改成div.card-item选择器就废了。这时候 LLM 策略就能兜底。它不看具体类名而是理解语义。同样的商品页面我可以把 schema 定义为product_schema { type: object, properties: { name: {type: string}, price: {type: number}, rating: {type: number}, stock_status: {type: string} }, required: [name, price] }调用方式from crawl4ai.extraction_strategy import LLMExtractionStrategy llm_strategy LLMExtractionStrategy( provideropenai/gpt-4o-mini, api_token你的API_KEY, schemaproduct_schema, instruction你是数据提取助手请从页面中提取所有商品的名称、价格、评分和库存状态缺少的字段用空字符串替代。, ) async with AsyncWebCrawler() as crawler: result await crawler.arun( urlhttps://example.com/products, extraction_strategyllm_strategy, ) data json.loads(result.extracted_content) print(data)我实测下来LLM 策略对类名的变化不敏感即使页面结构调整只要商品信息还在页面上它基本都能抽得出来。代价是速度慢、要花钱而且模型输出不是百分百稳定所以我的经验是核心页面用 CSS 策略边缘页面、易变化页面用 LLM 策略两边互补。5. 常见问题与排查技巧5.1 动态页面抓不到内容如果result.markdown是空的或者内容严重缺失八成是页面数据依赖 JavaScript 渲染而 crawl4ai 默认没有等 JS 执行完。解决办法是强制开启渲染或者增加等待条件result await crawler.arun( urlhttps://example.com, wait_forcss:.product-card, headlessTrue, )wait_for可以传 CSS 选择器也可以传一个 JS 表达式等页面出现对应节点后再开始提取。如果页面滚动才能加载更多内容可以加js_code参数传入一段滚动脚本。5.2 中文乱码问题爬中文网站时偶尔会遇到提取出来的文本是乱码。这个问题的根源通常不在 crawl4ai而在服务器返回的响应头里charset不对或者页面本身编码声明与实际内容不一致。目前的处理办法是手动指定页面编码。result await crawler.arun( urlhttps://example.com, headers{Accept-Language: zh-CN,zh;q0.9}, )如果还不行就把html保存下来自己用BeautifulSoup重新指定from_encodingutf-8后再处理。其实新版 crawl4ai 对 UTF-8 处理已经很好了绝大多数情况加一个Accept-Language请求头就能解决。5.3 LLM 提取结果不稳定我试过同一个页面连续跑五次模型输出字段顺序不一样偶尔还会多出一些解释性文字导致 JSON 解析失败。几个小技巧在instruction里明确写“只输出 JSON不要解释”。在schema里把必填字段都列出来能有效约束模型。调整模型参数比如temperature尽量低。如果 API 支持 JSON 模式优先开启。另外如果你不想每次都调用付费 APIcrawl4ai 也支持接本地模型比如 Ollama 跑llama3这类开源模型速度慢点但数据不出内网适合处理敏感内容。5.4 请求失败和频率控制爬取大量页面时请求失败是常态别指望它每次都成功。crawl4ai 提供了重试机制但生产环境我建议自己再做一层调度for url in url_list: try: result await crawler.arun(urlurl) except Exception as e: print(f抓取失败: {url}, {e}) continue同时要注意请求频率爬太快容易被封 IP。我是用asyncio.sleep(1)做最简单限速必要的时候再加代理池。5.5 输出 JSON 不合法extracted_content是字符串我在没仔细看的情况下直接json.loads踩过几次坑。尤其 LLM 策略输出偶尔会被 Markdown 代码块包起来比如json [{name: 测试}]如果出现这种情况解析前先做个清理 python import re text result.extracted_content text re.sub(r^json\s*|\s*$, , text.strip()) data json.loads(text)这件事提醒我不管工具封装得多好下游的防御性处理还是不能省。最后再分享一个小技巧前面说的都是怎么抓和怎么提实际生产里最后一公里也很重要。我每次跑完提取都会把结果直接写成带时间戳的 JSON 文件或者落到 SQLite 里而不是只打印在控制台上。这样就算后续分析代码写 bug 了原始结构化数据还在还能重新跑。另外如果你的页面既有稳定的列表区又偶尔出现动态卡片可以同时注册 CSS 和 LLM 两种策略先用 CSS 策略快速提一遍再用 LLM 策略对没提取成功的页面做二次处理。这套组合拳在我自己的监控项目里跑了好几个月改动成本比想象中小值得一试。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →