Crawlee @crawlee/cheerio 深度解析:从 CHANGELOG 看 CheerioCrawler 的能力演进与底层实现
2026/9/12 11:40:22 网站建设 项目流程

Crawlee @crawlee/cheerio 深度解析:从 CHANGELOG 看 CheerioCrawler 的能力演进与底层实现

【免费下载链接】crawleeCrawlee—A web scraping and browser automation library for Node.js to build reliable crawlers. In JavaScript and TypeScript. Extract data for AI, LLMs, RAG, or GPTs. Download HTML, PDF, JPG, PNG, and other files from websites. Works with Puppeteer, Playwright, Cheerio, JSDOM, and raw HTTP. Both headful and headless mode. With proxy rotation.项目地址: https://gitcode.com/GitHub_Trending/cr/crawlee

@crawlee/cheerio是 Crawlee 生态中基于纯 HTTP 请求与 Cheerio 为时间主线,梳理 CheerioCrawler 从 v3.3 到 v3.18 的关键功能演进,并对照 cheerio-crawler.ts 与 cheerio-parser.ts 源码,讲清每个特性背后的实现原理。读完你将掌握:CheerioCrawler 的解析管线、递归抓取深度控制、链接入队限制、robots.txt 尊重、类型安全路由等核心能力的用法与底层机制,以及版本升级时的关注点。

一、包概览:CheerioCrawler 在 Crawlee 中的定位

从 package.json 可以看到,@crawlee/cheerio当前工作区版本为4.0.0,要求node >= 22.0.0,直接依赖cheerio ^1.0.0htmlparser2 ^10.0.0,并复用@crawlee/http@crawlee/utils@crawlee/types等内部包。

CheerioCrawler的核心定位(见 cheerio-crawler.ts 的类注释)可以概括为三点:

  1. 纯 HTTP 请求抓取:每个 URL 通过普通 HTTP 请求下载,不启动浏览器,因此对带宽和 CPU 的消耗远低于 Puppeteer/Playwright 系爬虫;
  2. Cheerio 解析:页面 HTML 被解析成类似 jQuery 的 DOM 接口,requestHandler内用$(selector)语法抽取数据;
  3. 静态列表 + 动态队列双源:URL 既可来自静态RequestList,也可来自支持递归入队的RequestQueue,二者还可以组合成RequestManagerTandem

从源码结构看,CheerioCrawler本身是一个"薄封装"——它继承自@crawlee/http包的DOMCrawler,构造函数只是把默认解析器替换为cheerioParser()(cheerio-crawler.ts):

constructor(options?: CheerioCrawlerOptions<...>) { super({ ...options, parser: cheerioParser() }); }

这意味着 CheerioCrawler 的所有请求调度、并发控制、重试、会话管理能力都来自更底层的HttpCrawler/DOMCrawler,而 cheerio 包的职责被收敛在解析器这一层。理解这一点,就能明白为什么 CHANGELOG 中很多修复标注为core域——它们虽然记录在 cheerio 包的变更日志里,实际改动在公共内核。

createCheerioRouter则是Router.create<CheerioCrawlingContext>()的类型化快捷方式(cheerio-crawler.ts),用于按request.label分发不同的处理逻辑。

二、解析管线:cheerioParser 与 XML 支持

cheerioParser的实现位于 cheerio-parser.ts,它是理解本包行为的关键:

export function cheerioParser(): DOMParser<CheerioParseResult> { return { placeholderMembers: { $: true, body: true }, parse(context: InternalHttpCrawlingContext) { const isXml = context.contentType.type.includes('xml'); const body = Buffer.isBuffer(context.body) ? context.body.toString(context.contentType.encoding) : context.body; const dom = parseDocument(body, { decodeEntities: true, xmlMode: isXml }); const $ = cheerio.load(dom, { xml: { decodeEntities: true, xmlMode: isXml }, } as CheerioOptions); return { $, body }; }, extractLinks: ({ $ }, selector, baseUrl) => extractUrlsFromCheerio($, selector, baseUrl), select: ({ $ }, selector) => $(selector).get(), toCheerio: ({ $ }) => $, }; }

几个值得注意的实现细节:

  • HTML/XML 双模式:解析器根据响应Content-Type是否包含xml自动决定xmlMode。这一行为正是 v3.3.0 的修复**CheerioCrawler:** pass isXml down to response parser(PR #1807)的成果——在修复前 XML 内容可能被按 HTML 语义解析而损坏。仓库中的 xml.test.ts 专门验证了$('item').first().find('link').text()能从 XML 文档中正确取值。
  • body 编码处理:若响应体是 Buffer,按contentType.encoding转成字符串,保证中文等非 UTF-8 页面不乱码。
  • 链接抽取复用extractLinks直接调用@crawlee/utils内部的extractUrlsFromCheerio,入队逻辑与解析逻辑解耦。

2.1 MIME 类型白名单与 additionalMimeTypes

CheerioCrawler 默认只处理text/htmlapplication/xhtml+xmltext/xmlapplication/xmlapplication/json这五类 MIME 内容(cheerio-crawler.ts),其余类型直接跳过。如果需要抓取其他类型,通过additionalMimeTypes扩展白名单。

在底层 http-crawler.ts 中,该选项经 zod schema 校验后调用extendSupportedMimeTypes(第 841-852 行)动态扩充可处理的类型集合。注意不同 MIME 的解析行为不同:HTML/XML 走 DOM 解析,JSON 场景下$的可用性有限,抽取逻辑需要按内容类型分支处理。

2.2 context.body 不再解码 HTML 实体

v3.13.0 的修复**cheerio:** don't decode HTML entities in context.body(PR #2838)解决了一个数据一致性问题:此前context.body中的 HTML 实体(如&amp;)可能已被解码,导致开发者拿到的原始 HTML 与页面上传的字节不一致。修复后,实体解码只发生在 Cheerio 的 DOM 解析层decodeEntities: true作用于parseDocumentcheerio.load),而body字段保持原始字符串。也就是说:用$取文本时你会得到解码后的可读内容,而用body做原文保存/哈希时会得到未解码的原始 HTML——两者各司其职,不应混用。

三、能力演进主线:按 CHANGELOG 逐项拆解

以下特性均出自 CHANGELOG.md,多数虽标注为core域修复,但直接塑造了 CheerioCrawler 的日常使用方式。

3.1 递归抓取深度控制:maxCrawlDepth

v3.14.0 引入maxCrawlDepthcrawler 选项(PR #3045),用于限制递归抓取的层数。配合enqueueLinks使用:当爬虫从startUrls出发,第一层页面入队的链接属于 depth 1,依此类推;一旦请求的深度超过maxCrawlDepth,该请求会被跳过。这在以下场景非常实用:

  • 只想抓取"首页 → 列表页 → 详情页"三层结构,避免爬进用户主页、标签页等无关分支;
  • 控制抓取规模与预算,防止递归失控导致请求量爆炸。

3.2 链接入队上限与标签

CHANGELOG 中有两条与链接入队直接相关的修复:

  • v3.13.8:不再入队超出爬虫处理能力的链接(PR #2990,对应 issue #2728)。此前enqueueLinks可能一次性把页面上的所有链接塞进队列,超出爬虫在合理时间内能处理的数量,导致队列积压、内存上涨甚至超预算。修复后入队数量会结合爬虫的容量上限进行约束,从源头保护请求队列。
  • v3.4.0:入队时尊重<base>标签(PR #1936)。HTML 的<base>元素会改写页面内相对链接的解析基准,此前实现可能忽略它,导致相对 URL 解析到错误的绝对地址。修复后enqueueLinks在解析相对链接前会先考虑<base>声明的 URL。

3.3 请求跳过与 robots.txt 尊重

  • v3.13.2 新增onSkippedRequest选项(PR #2916):当某个请求因故被跳过(如超过maxCrawlDepth、被 robots.txt 拦截、超出入队限制等)时,回调会被触发。这是观测抓取覆盖率、排查"为什么这个页面没被抓到"的重要钩子,可配合统计埋点或日志使用。
  • v3.13.1 新增respectRobotsTxtFile爬虫选项(PR #2910):开启后爬虫在入队前检查目标站点的robots.txt规则,遵循Disallow指令。同版本还有一个命名层面的破坏性变更:RobotsFile更名为RobotsTxtFile(PR #2913),如果你在代码里引用过旧的RobotsFile类型或配置项,升级到 3.13.1+ 时需要同步改名。

3.4 类型安全路由:per-label userData 与 schema 校验

v3.18.0 是本包近期最重要的一次类型能力升级,包含两项特性:

  • type-safe router labels via per-label userData map(PR #3747):为createCheerioRouter的路由标签(label)关联独立的userData类型映射。以往所有请求共享一个userData类型,不同 label 下数据结构不同时只能as断言;现在每个 label 可以声明自己的 userData 结构,requestHandler内按 label 分发后,request.userData会自动获得对应类型推断。
  • opt-in schema validation of request userData per router label(PR #3851):在类型安全之上,进一步允许为每个 label 的userData声明运行时校验 schema(zod)。请求进入对应路由处理器前先校验 userData 形状,非法数据提前暴露,而不是等到抽取阶段才发现字段缺失。

这两项配合createCheerioRouter(cheerio-crawler.ts)使用效果最佳。需要说明的是,schema 校验是 opt-in 的——不传 schemas 时行为与旧版本一致,不会对既有项目产生运行时破坏。

3.5 上下文助手:parseWithCheerio 与 waitForSelector

  • v3.3.1 为 cheerio crawler 增加parseWithCheerio上下文助手:当requestHandler需要在一个页面上下文里对额外获取到的 HTML 片段(例如 AJAX 接口返回的 HTML 字符串)重新做 Cheerio 解析时,不必手动 import cheerio 再配置,直接用上下文的parseWithCheerio(html)即可得到可用的$
  • v3.10.3 在 adaptive crawler 中新增waitForSelector上下文助手 +parseWithCheerio(PR #2522):adaptive 爬虫在浏览器渲染模式下先waitForSelector等待关键元素出现,再决定是否切回 Cheerio 解析,兼顾了"JavaScript 渲染页"与"静态页"两种场景的吞吐。

3.6 基础设施演进:Request Queue v2 与包体积

  • v3.5.5 引入 Request Queue v2(PR #1975):新版请求队列重新设计了持久化与去重机制,是后续所有队列相关能力(含入队上限控制)的地基。CheerioCrawler 的递归抓取依赖队列动态供给 URL,因此该升级直接影响长跑型爬虫的稳定性。
  • v3.16.0 从发布包中移除tsbuildinfo文件(PR #3243):缩小了 npm 包体积、缩短安装时间,属于对所有用户的透明优化。
  • v3.5.3 固定所有内部依赖版本(PR #2041):monorepo 内部包之间的依赖被 pin 死,避免发布链路中"隐式升级"引发的兼容性问题——这也是该包 CHANGELOG 大量条目只写 "Version bump only" 的原因之一,它们仅是跟随内核发版。

四、版本节奏与升级关注点

从 CHANGELOG 的整体结构可以看出@crawlee/cheerio的发布节奏:大部分版本只是跟随内核的版本号递增("Version bump only for package @crawlee/cheerio"),只有少数版本携带 cheerio 包自身的实质性变更。升级时建议重点核对以下几个时间节点:

版本类型变更内容升级动作
3.18.0Featureper-label userData 类型映射 + 可选 schema 校验可选,按需启用
3.14.0FeaturemaxCrawlDepth可选,限制递归深度
3.13.8Fix入队数量受爬虫容量约束行为收紧,注意队列规模变化
3.13.2FeatureonSkippedRequest可选,排查跳过原因
3.13.1Breaking-ishRobotsFileRobotsTxtFile更名;新增respectRobotsTxtFile需改名,开启后行为受 robots.txt 约束
3.13.0Fixcontext.body不再解码 HTML 实体行为变更,依赖body原文的代码需复核
3.4.0Fix入队尊重<base>标签相对链接解析基准可能变化

其中3.13.0 与 3.13.1 是最需要关注的行为级变更:前者改变了context.body的内容形态,后者引入了 robots.txt 对入队行为的约束(仅在你主动开启时生效)。

五、快速上手:一个可运行的完整示例

综合以上能力,一个覆盖"递归抓取 + 深度限制 + 路由分发 + 数据落盘"的 CheerioCrawler 典型用法如下(思路参照 README.md 与仓库 docs/examples/cheerio_crawler.ts):

import { CheerioCrawler, createCheerioRouter, Dataset } from 'crawlee'; const router = createCheerioRouter(); router.addHandler('list', async ({ request, $, enqueueLinks }) => { // 从列表页抽取详情页链接并递归入队 await enqueueLinks({ selector: 'a.item-link', label: 'detail', // 配合 maxCrawlDepth 限制抓取深度 }); }); router.addHandler('detail', async ({ request, $, log }) => { const title = $('h1').text().trim(); const price = $('.price').text().trim(); await Dataset.pushData({ url: request.url, title, price }); log.info(`Saved ${title}`); }); const crawler = new CheerioCrawler({ requestHandler: router, maxCrawlDepth: 3, // 3.14.0+,限制递归深度 additionalMimeTypes: ['application/ld+json'], // 按需扩展 MIME // respectRobotsTxtFile: true, // 3.13.1+,开启 robots.txt 尊重 // onSkippedRequest: ({ request, reason }) => console.log(request.url, reason), // 3.13.2+ }); await crawler.run(['https://example.com/catalog']);

使用要点提醒:

  • maxCrawlDepthonSkippedRequestrespectRobotsTxtFileadditionalMimeTypes这些选项分别对应 3.14.0 / 3.13.2 / 3.13.1 及更早版本引入的能力,低版本上不存在这些选项;
  • 开启respectRobotsTxtFile前先确认目标站点 robots.txt 的Disallow范围,避免意外丢站;
  • 若站点内容由 JavaScript 渲染(HTML 里取不到数据),应改用PlaywrightCrawler/PuppeteerCrawler,或使用 adaptive crawler 的waitForSelector+parseWithCheerio混合策略(v3.10.3+);
  • 若需同时从只读的静态列表和可写的动态队列取 URL,可用requestLoader.toTandem()组合成RequestManagerTandem传入requestManager,这与 CHANGELOG 中 Request Queue v2 的基础设施演进一脉相承。

六、总结

@crawlee/cheerio的 CHANGELOG 记录的不只是版本号,更是一条清晰的能力演进曲线:从 v3.3 的 XML 解析修复、v3.4 的<base>支持,到 v3.13 的 robots.txt 与body语义修正,再到 v3.14 的深度控制、v3.18 的类型安全路由。结合 cheerio-parser.ts 与 cheerio-crawler.ts 源码可以看出:这个包的核心是"薄爬虫壳 + 强解析器",绝大多数调度能力由@crawlee/http内核提供,而 cheerio 包专注于把 HTML/XML 高效、准确地变成可抽取的 DOM。对使用者而言,抓住 3.13.x 与 3.18.x 两个关键节点,就能平稳跟进其行为变化与类型红利。

【免费下载链接】crawleeCrawlee—A web scraping and browser automation library for Node.js to build reliable crawlers. In JavaScript and TypeScript. Extract data for AI, LLMs, RAG, or GPTs. Download HTML, PDF, JPG, PNG, and other files from websites. Works with Puppeteer, Playwright, Cheerio, JSDOM, and raw HTTP. Both headful and headless mode. With proxy rotation.项目地址: https://gitcode.com/GitHub_Trending/cr/crawlee

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询