ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

crawl4ai实战:用AI爬虫轻松将网页转为结构化JSON

crawl4ai实战:用AI爬虫轻松将网页转为结构化JSON 做数据采集这些年我最怕的不是网站反爬而是“爬下来了却要花三倍时间洗数据”。一个商品页标题在 h1 里价格在 meta 里库存状态又藏在某段 JS 变量中用 Requests BeautifulSoup 不是不能抓而是每次网站改版都要重新拆一遍 HTML维护成本高得离谱。后来在 GitHub 上刷到 crawl4ai试了一下午才意识到AI 爬虫不是花架子它确实把“爬网页”这件事从“拿 HTML”推进到了“拿结构化数据”。crawl4ai 的核心能力就是让开发者先定义清楚要什么字段再用策略CSS 选择器或大模型自动从页面里提取 JSON省掉中间一大轮手写解析和清洗。这篇我就从安装到落地完整记录一下怎么用它把网页变成结构化数据顺便把我踩过的坑全倒出来。1. crawl4ai 到底解决什么问题1.1 传统爬虫的三座大山早些年写爬虫技术上其实不复杂麻烦的是“脏活累活特别多”。我用 Python 爬过电商、新闻、招聘网站最常见的三个痛苦第一是页面结构改版。这周还能跑的 CSS 选择器下周网站前端加了字体图标、换了 class 命名代码直接报废。每次出问题都要打开 DevTools 重新定位元素改选择器、调父节点、再跑一遍看有没有漏字段。这种循环特别消耗耐心。第二是字段分散。一个商品详情页看上去是一张卡片实际数据来源有四五处标题在h1价格在meta propertyproduct:price库存状态写在script里的window.__INITIAL_STATE__对象中评价数又要去另一个接口拿。传统思路只能分散解析再汇总代码越长越难维护。第三是内容本身不够结构化。表格、动态加载的列表、登录后才出现的数据用普通 HTTP 请求根本拿不到真实 DOM必须依赖无头浏览器渲染完再抓。而浏览器一渲染解析逻辑又得重新设计。这三个问题放在一起就会得到一段很长、很脆的“胶水代码”。crawl4ai 恰恰把这三座大山一起处理了它能驱动真实浏览器能提取主体内容还能直接按 schema 输出 JSON相当于把爬虫后半程的脏活标准化了。1.2 爬取链路里的 AI 到底在哪很多人一听“AI 爬虫”脑子里浮现的是“机器人自动看懂网页”其实 crawl4ai 并没有那么玄学。它的整体架构可以拆成三截底层是抓取引擎。crawl4ai 基于异步机制内置了 HTTP 直连和 Playwright 浏览器渲染两种方式。前者快适合静态页面后者稳适合动态页面。这个设计很务实不是所有目标站点都需要开浏览器。中间是内容处理管线。拿到原始 HTML 之后它会把导航、脚本、CSS、广告等噪音去掉输出一份干净的 Markdown 或纯文本。这一步对后续 LLM 抽取特别关键因为喂给大模型的文本越干净抽取效果越好。最上面才是“AI”的部分——提取策略。crawl4ai 提供两类策略一类是传统但高效的JsonCssExtractionStrategy本质是结构化的 CSS 选择器批量抽取另一类是LLMExtractionStrategy把页面主体文本丢给大模型让模型根据你的字段定义返回 JSON。所以它的“AI 味”主要集中在这层下层还是实实在在的工程。打个比方传统爬虫是你在仓库里挨个翻箱子找到什么拿什么crawl4ai 是直接给搬运工一张收货单告诉他“我要哪些格子、每个格子里装什么”他翻完就给你装进对应的小盒子。你把小盒子接过来就是一行一行的结构化数据。1.3 和 Scrapy、Requests 拉开差距的关键用传统的 Requests BeautifulSoup代码自由度最高但每个项目都要重新搭一遍解析模板。Scrapy 是更重型的选择它能管理并发、管道、中间件但学习曲线陡而且对于动态页面还是要外挂 Playwright 或 Splash配置起来并不轻松。crawl4ai 的突围点在于“开箱即用的结构化提取”。它不像 Scrapy 那样逼你先搭一套爬虫框架而是给了一个异步 APIAsyncWebCrawler打开上下文arun跑一个 URL返回结果里直接带上 HTML、Markdown、Links、Metadata以及你定义好的结构化 JSON。它关心的是“拿到数据之后怎么办”而不是让你花一天时间搭项目骨架。方案上手成本动态页面支持结构化输出适合场景Requests BS4低弱手写解析一次性静态小任务Scrapy高需要外挂管道自建大规模长期爬虫项目crawl4ai中低内置 Playwright策略直接出 JSON需要快速把网页转结构化数据我个人的判断是如果你面对的只是三五个静态页面用什么差异都不大但如果你要经常和格式混乱、结构多变的网页打交道crawl4ai 的提取策略是真的能省时间。2. 从零上手先跑通一个最简单的例子2.1 安装与首次启动安装本身不复杂一个 pip 命令就能搞定pip install crawl4ai但光是装好包还不能立刻跑crawl4ai 依赖 Playwright首次使用一般要初始化浏览器内核。我在 0.4.x 版本上跑的命令是crawl4ai-setup这个命令会自动拉取 Chromium 之类的基础浏览器组件。第一次执行会卡在下载进度条上这是正常的等一会儿就行。如果网络环境不太好这里失败的概率不低解决办法是检查网络后重新执行已经下载好的部分会被复用。提示如果你只抓静态 HTML不涉及 JS 渲染理论上可以不启用完整浏览器直接用轻量模式。但新手阶段建议老实跑完 setup避免后续被动态页面卡住。装完之后可以用crawl4ai --doctor这类命令做一次健康检查确认浏览器环境没问题。这一步虽然不起眼但能省掉后面很多“明明代码没问题却跑不通”的排查时间。2.2 第一个异步爬取任务crawl4ai 的 API 设计是异步优先所以哪怕只抓一个页面也要写一个async函数。最简版本长这样import asyncio from crawl4ai import AsyncWebCrawler async def main(): async with AsyncWebCrawler() as crawler: result await crawler.arun(urlhttps://example.com) print(result.markdown[:500]) asyncio.run(main())第一次看到这段代码你可能会疑惑为什么不是crawler.fetch(url)这种同步写法因为异步上下文管理器能让底层连接池、浏览器实例在整个会话内复用做批量采集时性能提升非常明显。你只需要记住多个 URL 的抓取任务放在同一个async with块里做并发效率远高于一个个地开关资源。result是一个CrawlResult对象也就是一次抓取的完整“包裹”。它身上挂着这次抓到的所有东西下一步所有操作都从这里展开。2.3 返回结果里哪些字段最有用CrawlResult我实际用得最多的字段是这几个result.html原始 HTML 字符串适合排查问题时看页面到底长什么样。result.markdown清洗后的 Markdown 文本给 LLM 策略喂数据时首选。result.fit_markdown进一步裁剪后的“主体内容”去掉了导航、页脚之类的噪音。result.links解析出来的站内链接和站外链接。result.metadata标题、描述、关键词等元信息。result.structured_data按提取策略生成的结构化 JSON 字符串这是我们的最终目标。result.success布尔值标记这次抓取是否成功。我第一次跑通时直接打印了result.markdown[:200]看到干净文本的时候有点惊讶——一个带导航栏、侧边栏、广告位的页面能被压成那么干净的正文。这说明中间的内容处理管线确实下了功夫不只是普通 HTML-to-Markdown 转换。心得很多新手一上来就盯structured_data但抓不到数据时别死磕策略先打印result.markdown看内容在不在。如果干净文本里都没有目标字段说明页面没加载完或 URL 不对这时候调选择器是无效劳动。3. 把网页变成结构化数据的核心思路3.1 先定义 schema再去爬这是 crawl4ai 和传统爬虫在思路上的最大分水岭。传统做法是先拿到 HTML再看怎么解析crawl4ai 的做法是反向的——在你发出请求前先定义一份 schema说清楚“我想从页面里拿哪些字段、每个字段大概长什么样”。这有点像写数据库表结构你先把列定义好再来填数据。一个最普通的 schema 长这样schema { name: Product, baseSelector: div.product, fields: [ {name: title, selector: h2.title, type: text}, {name: price, selector: span.price, type: text}, {name: link, selector: a.thumb, type: attribute, attribute: href}, ] }schema[name]是这段结构的名字baseSelector是循环列表项的根节点fields是你要提取的字段清单。每个字段的type决定怎么取值text是拿文本内容attribute是拿某个属性的值后续还能用到嵌套对象、正则匹配等更高级的玩法。为什么要把 schema 放在前面最直接的好处是“抽取逻辑可复用”。页面改版了你只要修改选择器外层的数据落库代码逻辑完全不用动。而传统爬虫分散在代码各处的.find()调用改起来就像在一锅粥里挑豆子。3.2 用 CSS 策略实现“表格化”抽取对应 schema 的第一个落地工具是JsonCssExtractionStrategy。它可以被理解成一个面向列表页的“批量表格化提取器”你给我根节点和字段选择器我把根节点下所有满足条件的条目全部提出来转成一个 JSON 数组。实际用法是先把 schema 传给策略再把策略传给爬虫对象from crawl4ai.extraction_strategy import JsonCssExtractionStrategy extractor JsonCssExtractionStrategy(schema, verboseTrue) async with AsyncWebCrawler() as crawler: result await crawler.arun( urlhttps://example.com/products, extraction_strategyextractor )跑完之后result.structured_data里就是一个 JSON 字符串json.loads()一下就能拿到 Python 列表。这个策略的核心优势是“零成本、稳定、可预期”。没有大模型调用没有 token 费用响应速度接近毫秒级。只要目标页面的 DOM 结构稳定它是我首选方案没有之一。3.3 用大模型策略处理“非标”页面但现实世界里的网页并不总是规规矩矩的。我遇到过不少页面同一份列表里有的条目有价格有的却没有有的标题直接暴露在 HTML 里有的却是 JS 渲染出来的富文本。CSS 选择器在这种场景下会顾此失彼这时就该LLMExtractionStrategy登场。它的基本思路是把页面主体文本通常就是fit_markdown和一段指令一起丢给大模型让模型根据你定义的 schema 决定哪些内容对应哪些字段然后返回 JSON。我常用的参数是这样的from crawl4ai.extraction_strategy import LLMExtractionStrategy strategy LLMExtractionStrategy( provideropenai/gpt-4o-mini, api_token你的密钥, schemaproduct_schema, extraction_typeblock, instruction请从页面中提取所有商品的标题、价格和库存状态返回 JSON 数组。 )要注意这里的schema和 CSS 策略里的 schema 意义不太一样。CSS 策略里的 schema 是“选择器清单”LLM 策略里的 schema 更像是“字段定义说明书”用来告诉模型你要什么结构的输出。字段值哪里找模型自己理解。LLM 策略的最大好处是抗页面结构变化。前端随便改 class、挪位置只要页面语义上还说得通模型基本都能对上。坏处也很明显有 token 成本有网络延迟还有可能返回格式不稳定的 JSON。所以它最适合的场景是“页面杂乱、没有统一模板、数据量中等”的任务。3.4 两个策略的取舍用熟了之后你会发现两个策略不是竞争关系而是互补关系。我一般按下面这个逻辑选择判断条件JsonCssExtractionStrategyLLMExtractionStrategy页面结构固定模板、class 稳定结构混乱、字段位置不固定请求量大批量、每天上万页小批量、几十到几百页成本预算零成本按 token 计费对速度要求毫秒级响应秒级甚至更慢容错要求结构一变就出错对结构变化更鲁棒如果页面是后台管理系统、内部文档站这类结构稳定的场景无脑选 CSS 策略。如果爬的是资讯聚合站、用户生成内容或者你根本懒得折腾选择器就用 LLM 策略。心得我做过一个混合方案——先用 CSS 策略跑如果返回的structured_data是空数组再用 LLM 策略兜底。这样平时 90% 的流量都在零成本路径上跑偶尔遇到改版也不会完全断粮。4. 实例抓一个博客列表页并生成 JSON4.1 先画页面结构空谈概念不如直接来一个完整案例。假设我要抓一个博客站的文章列表页页面结构大概是下面这样article classpost h2a href/post/ai-scraperAI 爬虫实战/a/h2 time datetime2025-01-202025-01-20/time p classsummary本文记录 crawl4ai 落地细节.../p /article目标字段是四个文章标题、链接、发布日期、摘要。在写代码之前我先在浏览器里按 F12看了一遍真实 DOM确认选择器无误然后才开始写 schema。别嫌这一步麻烦省掉的都是返工时间。4.2 编写 schema 与执行代码完整代码如下import asyncio import json from crawl4ai import AsyncWebCrawler from crawl4ai.extraction_strategy import JsonCssExtractionStrategy schema { name: ArticleList, baseSelector: article.post, fields: [ {name: title, selector: h2 a, type: text}, {name: link, selector: h2 a, type: attribute, attribute: href}, {name: date, selector: time, type: text}, {name: summary, selector: p.summary, type: text}, ] } async def main(): strategy JsonCssExtractionStrategy(schema, verboseTrue) async with AsyncWebCrawler() as crawler: result await crawler.arun( urlhttps://example.com/blog, extraction_strategystrategy, bypass_cacheTrue ) if not result.success: print(抓取失败状态码, result.status_code) return data json.loads(result.structured_data) print(json.dumps(data, ensure_asciiFalse, indent2)) asyncio.run(main())运行之后预期的输出大概是[ { title: AI 爬虫实战, link: /post/ai-scraper, date: 2025-01-20, summary: 本文记录 crawl4ai 落地细节... }, { title: Python 结构化数据建模入门, link: /post/python-structure, date: 2025-01-18, summary: 从数据清洗到 Schema 设计的完整路径... } ]核心逻辑不复杂arun负责抓页面extraction_strategy负责按 schema 提取structured_data负责把结果以 JSON 字符串形式带回来。只要页面选择器没写错整条链路一气呵成。4.3 输出示例与效果baseSelector的逻辑是按“分组”来走的。页面里有几个article.post结果就是几个对象每个对象里的字段再按各自的selector去匹配。这就像做透视图按行分组按列取属性最后形成一张表。如果某个字段在单个条目里出现多次匹配type: text默认取第一个匹配项的文本type: attribute则取第一个节点的属性值。想取全部文本或全部属性时需要用更精细的配置。我实际用下来几百个页面的列表抓取结构化输出的成功率在结构稳定时接近百分之百。偶尔出现空字段大多是源页面自己数据缺失不是爬虫逻辑问题。4.4 调试和选择器优化前面提到过调试的第一步是看result.markdown。但要是 CSP 策略严格、页面对无头浏览器不友好Markdown 里可能也没有你要的内容。这时候我会做三件事打印result.html搜目标文本在不在原始 HTML 里。如果 HTML 里有内容检查 schema 的选择器是否太具体比如body div#app section div:nth-child(3) article.post这种 DevTools 一键复制的选择器宁可改成article.post。打开verboseTrue看日志里有没有解析警告。提示直接从 DevTools 复制的选择器往往带一堆:nth-child和#id前缀看着精确实际脆弱。我更喜欢用类名做主定位类名不够再加aria-label或>from crawl4ai import CrawlerRunConfig config CrawlerRunConfig( wait_for#content, delay_before_return_html2, page_timeout30000 ) async def main(): async with AsyncWebCrawler() as crawler: result await crawler.arun( urlhttps://example-spa.com/page, configconfig )其中wait_for是等待页面里出现指定选择器属于最常用的等待手段。delay_before_return_html是额外等多少秒再取 HTML适合那些加载完选择器之后还有其他异步刷新的页面。我踩过一个大坑是明明等到了选择器也等了 2 秒数据还是不全。后来发现目标页面是先渲染首屏再懒加载列表下方的数据。解决办法是把wait_for指向列表尾部的一个“加载完毕”标记节点或者干脆等待某个我们没有拿到但必然出现的元素。心得动态页面的核心不是“会等”而是“知道在等什么”。不要上来就sleep(5)硬等学会找一个标志性节点用wait_for精准等待。这样既快又稳。5.2 批量采集与缓存控制crawl4ai 的缓存机制设计得不错。默认情况下相同 URL 会在一定时间内复用上一次抓取结果这能大幅降低重复抓取的网络开销。但调试时这个功能会让人很困惑——你改了 schema跑出来的还是旧数据。处理方式有两个一是在arun里传bypass_cacheTrue这是调试期最常用的参数二是通过更细粒度的CacheMode控制缓存策略比如只命中内存缓存不命中磁盘缓存。批量抓取时我一般用asyncio.gather做并发async def fetch_one(url, strategy): async with AsyncWebCrawler() as crawler: result await crawler.arun(urlurl, extraction_strategystrategy) return json.loads(result.structured_data) async def main(): urls [https://example.com/blog?page1, https://example.com/blog?page2] results await asyncio.gather(*[fetch_one(url, strategy) for url in urls])需要注意每个AsyncWebCrawler实例对应一套浏览器资源如果你在列表推导里一直创建新实例资源开销会很大。更合理的做法是共用一个实例或者用asyncio.Semaphore限制并发数。我习惯把并发控制在 5 到 10 之间既够快又不会给目标服务器造成太大压力。5.3 数据落库从 JSON 到 DataFrame结构化数据拿到手后下一步通常是落库或对接下游分析。最省事的方案是转成 Pandas然后再决定是导出 CSV、Excel还是直接塞进数据库import pandas as pd data [ {title: AI 爬虫实战, date: 2025-01-20}, {title: Python 结构化数据建模入门, date: 2025-01-18} ] df pd.DataFrame(data) df.to_csv(articles.csv, indexFalse)从structured_data到 DataFrame 只有一行json.loads的距离中间不需要任何手工清洗。对于单个字段我偶尔还会在 schema 里做正则处理比如把日期里的时区尾巴截掉或者从链接里抽出 ID。这些都能在 schema 字段配置里完成没必要回表后再处理。如果项目比较正式我建议再套一层 Pydantic 做数据校验。爬虫数据并不总是干净的让校验环节拦截异常字段比落库之后才发现脏数据要省心得多。5.4 采集合规与频率控制这一点必须单独拎出来说。crawl4ai 只是一个工具工具本身不分好坏但使用方式要讲究。我在做任何采集任务之前都会先看目标站点的 robots.txt再确认服务条款里是否明确禁止自动化访问。公开数据、非登录数据、低频抓取是底线。频率控制上delay_before_return_html只能控制单页等待控制整体请求频率需要自己做节流。我通常的做法是import asyncio async def fetch_with_interval(url, interval1.5): async with AsyncWebCrawler() as crawler: result await crawler.arun(urlurl) await asyncio.sleep(interval) # 给服务器留点余量 return result给目标服务器留余量既是合规要求也是更稳的长期策略。宁可抓得慢一点也别因为请求过猛导致 IP 被临时限制到时候整个业务都可能停摆。6. 常见问题与排查技巧实录6.1 高频报错速查表我在使用过程中遇到过的、以及身边朋友问过的典型问题整理成一张速查表现象常见原因解决思路structured_data返回空数组选择器没匹配到元素先看markdown和html确认内容在不在长时间不返回结果页面等待条件不满足检查wait_for选择器是否始终不存在拿到的是上个版本数据缓存未清除调试时加bypass_cacheTrueLLM 返回 JSON 格式不稳定指令不够明确、字段描述含糊简化 schema把extraction_type调成block抓取大量页面时内存上涨浏览器实例未复用统一管理AsyncWebCrawler实例限制并发部分字段提取为 None源页面字段缺失或选择器过严用正则回退或设置缺省值这六类问题覆盖了我在实际项目里九成以上的报错。其中“结构化数据为空”是新手最容易撞上的根因基本都在选择器。不要急着怀疑 crawl4ai先用result.html反向验证。6.2 抓不到数据时的排查路径如果遇到抓取结果一直不对我会按下面的顺序排查基本不跑偏第一步检查基本连接。打印result.status_code和result.success。如果是403或429说明被对方限制降低频率或者调整请求头如果是200继续往下看。第二步检查内容完整度。打印result.markdown看看里面有没有目标字段的文本。如果 Markdown 是干净的但缺内容大概率是 JS 渲染没完成如果 Markdown 是乱的说明页面本身结构特殊需要进一步清理。第三步检查选择器。目标文本在 HTML 里但structured_data为空这时把 schema 里的baseSelector单独拿出来在 DevTools 的 Console 里跑一下document.querySelectorAll(...)立竿见影地看出有没有匹配到节点。第四步考虑页面懒加载。如果文本一开始没有要滚动才出现就得回到第三节提到的wait_for策略。这套路径帮我解决过很多次“看起来代码没问题”的尴尬时刻。尤其是第三步简直是把练习时长从半小时压缩到了两分钟。6.3 性能优化和内存控制最后一个经验贴士关于资源和速度的平衡。首先尽量复用AsyncWebCrawler实例避免反复创建和销毁浏览器进程。一次async with生命周期内要抓几十个页面是很常见的设计。其次根据页面是否动态选择抓取模式。纯静态页面没必要每次开完整浏览器开浏览器的时间往往比请求本身还长。crawl4ai 支持轻量模式能大幅提升纯静态页面的抓取速度。最后善用缓存。对内容更新频率低的站点缓存机制配合定时任务可以做到首次全量抓取之后只抓变更页面。这样既省带宽也降低服务器压力。提示如果目标页面数量非常大不要把所有 URL 一次性塞进gather。先跑一个 20 页的小批量验证 schema 稳定再放开全量。一个错误的baseSelector在全量任务里会静默产生大量空数据到时候回头清理脏数据才是真的浪费时间。最后再分享一个小体会。crawl4ai 真正让我觉得“值回票价”的不是它某个明星功能而是它把“定义 schema、抽取字段、输出 JSON”这个流程变成了顺手的事。我第一次调试好一个列表页、看到structured_data稳稳输出 JSON 的时候说实话挺有成就感的。后来做类似项目我都会把 schema 单独抽成一个 JSON 文件存起来页面改版时只改选择器外围代码一点不动。这个习惯帮我节省了大量重复劳动。如果你也想上手我建议不要先背 API而是找一个真实的、结构稍微乱一点的页面带着一个具体目标去拆解和定义字段。等你完成第一份结构化数据整个工具的设计逻辑自然就通了。
返回列表