Cheerio 文档加载指南:load、loadBuffer、stringStream、decodeStream 与 fromURL 全解析
【免费下载链接】cheerioThe fast, flexible, and elegant library for parsing and manipulating HTML and XML.项目地址: https://gitcode.com/gh_mirrors/ch/cheerio
在能用 CSS 选择器查询、遍历或修改一个文档之前,Cheerio 必须先把它解析成内部的 DOM 树。本文以 Cheerio 官方文档 Loading Documents 为主体,完整讲解 Cheerio(v1.2.0,仓库根目录 package.json)提供的五种加载方式——load、loadBuffer、stringStream、decodeStream与fromURL——包括各自适用场景、编码处理机制、流式解析原理与网络请求细节,并结合仓库源码(src/index.ts、src/load.ts、src/options.ts)与测试用例(src/index.spec.ts)深入底层实现。读完后你将能针对「字符串、字节、流、URL」四种输入形态,以及「编码已知/未知」两种前提,做出正确的加载方法选型,并理解 fragment 模式、编码嗅探、重定向处理等关键行为。
为什么 Cheerio 需要显式的「加载」步骤
如果你从 jQuery 迁移过来,这一步是全新的:jQuery 直接操作页面内置的 DOM,而 Cheerio 没有宿主页面,它需要你把文档内容亲手传入。无论标记来自文件、网络响应还是内存字符串,都要先经加载方法解析,才能得到一个$查询函数。正如官方文档开篇所述,选择哪种方法,取决于「标记从哪里来」以及「你是否知道它的字符编码」。
Cheerio 的加载方法家族(全部定义在 src/index.ts 中)可以按下表快速定位:
| 方法 | 输入 | 适用场景 |
|---|---|---|
load | string | 已有一份字符串形式的标记 |
loadBuffer | Buffer | 有原始字节,且编码未知 |
stringStream | 已解码的文本流 | 正在流式处理,且编码已知 |
decodeStream | 原始字节流 | 正在流式处理,且编码未知 |
fromURL | URL | 希望 Cheerio 替你抓取页面 |
其中接受字节的方法(loadBuffer与decodeStream)会运行 HTML 编码嗅探算法,从而识别<meta charset>或字节序标记(BOM)。只要无法确定源是 UTF-8,就应优先使用这两种方法。
需要特别注意的是浏览器环境限制:在浏览器构建中只有load可用。loadBuffer、stringStream、decodeStream和fromURL都依赖 Node.js API,不会被打进浏览器包——这一点可以在 src/index-browser.mts 中得到印证,它只重新导出了 src/load-parse.ts 的load与类型定义,并未包含 src/index.ts 中的流式与网络加载实现。
load:从字符串加载文档
load接收一个包含文档的字符串,返回一个可用于遍历和操作的$函数:
import * as cheerio from 'cheerio'; const $ = cheerio.load('<h1>Hello, world!</h1>'); console.log($('h1').text()); // Output: Hello, world!从源码看,load的实际签名是(src/load-parse.ts):
load: ( content: string | AnyNode | AnyNode[] | Buffer, options?: CheerioOptions | null, isDocument?: boolean, ) => CheerioAPI即除了字符串,它还能接收已经解析好的 DOM 节点(AnyNode或其数组)以及Buffer;第二个参数是 Cheerio 选项,第三个参数isDocument控制是否按「完整文档」解析。底层由 src/load.ts 的getLoad工厂函数驱动:它解析输入得到初始根节点,然后构造一个绑定该文档的查询函数,并把_root、_options、fn等属性挂载到返回的initialize上,使其成为完整的CheerioAPI(接口定义见 src/load.ts)。
fragment 模式:跳过文档外壳
和浏览器一样,load在输入中缺失<html>、<head>、<body>时会自动补全。如果只想解析一段片段,把false作为第三个参数传入即可:
const $ = cheerio.load('<ul id="fruits">...</ul>', null, false); $.html(); //=> '<ul id="fruits">...</ul>'这与 配置指南 中讲解的 fragment 模式一致:当输入只是大页面的一小块(如一行表格<tr>、一个列表项<li>)时,通常应该用 fragment 模式,避免序列化时带上无意义的文档外壳。在源码中,isDocument参数会一路传给解析器——src/load-parse.ts 中getParse返回的解析函数会根据它选择调用parseWithParse5(content, options, isDocument, context)还是parseWithHtmlparser2。
底层双解析器机制
load背后实际是两套解析器的统一入口(src/load-parse.ts):
- parse5:HTML 解析的默认选择,严格遵循 HTML 标准,能产出与浏览器一致的 DOM 树;
- htmlparser2:XML 解析的默认选择,更快、更省内存、对畸形标记更宽容。
具体用哪套由选项决定:flattenOptions(src/options.ts)在xml或xmlMode为真时把内部标志_useHtmlParser2置为true,解析与渲染(src/load-parse.ts)随即切到 htmlparser2 管线。相关选项定义见 CheerioOptions,包括xml、xmlMode、baseURI、quirksMode、pseudos等。
loadBuffer:从字节加载文档
loadBuffer与load行为一致,但接收Buffer而非字符串。编码由 Cheerio 从字节本身推断,因此它特别适合处理编码不受你控制的文件与网络响应:
import * as cheerio from 'cheerio'; import * as fs from 'node:fs'; // 文件可能是 UTF-8、ISO-8859-1……由 Cheerio 自行判断 const $ = cheerio.loadBuffer(fs.readFileSync('document.html')); console.log($('title').text());其实现(src/index.ts)先调用encoding-sniffer包的decodeBuffer做字节嗅探与解码,再交给load解析:
export function loadBuffer(buffer: Buffer, options: DecodeStreamOptions = {}): CheerioAPI { const opts = flattenOptions(options); const str = decodeBuffer(buffer, { defaultEncoding: opts?.xmlMode ? 'utf8' : 'windows-1252', ...options.encoding, }); return load(str, opts); }两个值得注意的实现细节:
- 默认编码:HTML 模式下默认
windows-1252,XML 模式(xmlMode)下默认utf8。也就是说,嗅探失败时回退的编码因解析模式而异; - 编码覆盖:可通过
options.encoding传入encoding-sniffer的SnifferOptions覆盖默认行为,相关类型为DecodeStreamOptions(src/index.ts)。
测试用例(src/index.spec.ts)验证了两种典型场景:普通 UTF-8 字节,以及带 UTF-16 LE BOM 的字节(0xff 0xfe前缀),后者能被正确嗅探并解析,证明 BOM 检测确实生效。
stringStream:从已解码文本流加载
stringStream返回一个可写流,数据到达时边接收边解析。它适用于编码已知、可以先解码成文本再喂给 Cheerio 的场景:
import * as cheerio from 'cheerio'; import * as fs from 'node:fs'; const writeStream = cheerio.stringStream({}, (err, $) => { if (err) { // Handle error return; } console.log($('title').text()); }); fs.createReadStream('document.html', { encoding: 'utf8' }).pipe(writeStream);第一个参数是 Cheerio 的选项,第二个是流结束时的回调,接收错误对象与已加载的CheerioAPI。
底层实现_stringStream(src/index.ts)同样存在双管线分支:
- 默认使用 parse5 的
Parse5Stream,并通过finished(stream, ...)等待流结束后用load(stream.document, options)组装查询函数; - 当
_useHtmlParser2为真(即启用了xmlMode/xml)时,改用htmlparser2.createDocumentStream,包装成自定义Writable。
测试中可以看到该流只接受字符串(src/index.spec.ts):向它write一个Buffer会抛出Parser can work only with string streams.——因为按设计,解码工作由上游完成,Cheerio 在这里只负责解析已解码的文本。
decodeStream:从原始字节流加载
decodeStream是loadBuffer的流式对应物:它接收原始字节,在解析前运行编码嗅探算法。当需要流式处理一个编码未知的文档时,选它:
import * as cheerio from 'cheerio'; import * as fs from 'node:fs'; const writeStream = cheerio.decodeStream({}, (err, $) => { if (err) { // Handle error return; } console.log($('title').text()); }); // 注意:不传 encoding 选项——字节原样透传 fs.createReadStream('document.html').pipe(writeStream);实现(src/index.ts)清晰展示了它的两层结构:
export function decodeStream( options: DecodeStreamOptions, cb: (err: Error | null | undefined, $: CheerioAPI) => void, ): Writable { const { encoding = {}, ...cheerioOptions } = options; const opts = flattenOptions(cheerioOptions); // XML 模式下默认编码为 UTF-8 encoding.defaultEncoding ??= opts?.xmlMode ? 'utf8' : 'windows-1252'; const decodeStream = new DecodeStream(encoding); const loadStream = _stringStream(opts, cb); decodeStream.pipe(loadStream); return decodeStream; }DecodeStream来自encoding-sniffer包,负责按字节嗅探编码并解码;解码结果随即pipe给内部的_stringStream完成解析。也就是说,decodeStream= 编码嗅探层 +stringStream。默认编码规则与loadBuffer保持一致(HTML 为windows-1252,XML 为utf8)。测试(src/index.spec.ts)验证了 UTF-16 BOM 字节流可被正确解析,且xmlMode: true时切换 htmlparser2 管线。
fromURL:让 Cheerio 帮你抓取页面
fromURL是五个方法中唯一会替你发起网络请求的,它是异步的,需要await:
import * as cheerio from 'cheerio'; const $ = await cheerio.fromURL('https://example.com');它做的事情比裸fetch多得多,以下行为值得逐条掌握(实现见 src/index.ts):
- 重定向:自动跟随,最多 5 次。实现上通过
interceptors.redirect({ maxRedirections: 5 })组合进 undici 的Client; - 非 2xx 响应直接拒绝:抛出的错误是 undici 的
ResponseError,携带状态码(src/index.ts),而不是把错误页交给你解析; - 非标记内容直接拒绝:若
Content-Type既不是 HTML 也不是 XML,抛出RangeError(The content-type "..." is neither HTML nor XML.),避免把 PDF 当 HTML 解析。MIME 判断通过whatwg-mimetype的MIMEType完成(src/index.ts); - 自动选择 XML 模式:根据同一个
Content-Type推断——application/xml之类的响应会以xmlMode: true解析,无需你手动指定(src/index.ts); - 编码来源:优先取
Content-Type的charset参数,通过encoding.transportLayerEncodingLabel传给解码层;没有时才回退到字节嗅探; baseURI设置为最终 URL:即重定向之后的地址(src/index.ts 中取请求历史history?.at(-1))。这正是prop('href')与 extract 文档抽取 能拿到绝对链接的原因——测试(src/index.spec.ts)验证了重定向后prop('href')解析为基于最终地址的完整 URL,以及无重定向时相对链接按请求 URL 解析的行为。
定制请求:requestOptions
fromURL的第二个参数接受requestOptions来控制请求,它们会被传给 undici 的stream方法:
const $ = await cheerio.fromURL('https://example.com', { requestOptions: { method: 'GET', headers: { 'user-agent': 'my-scraper/1.0 (+https://example.com/bot)', }, }, });类型CheerioRequestOptions(src/index.ts)声明了requestOptions字段。使用时必须注意两条覆盖而非合并的规则:
method不会被默认:defaultRequestOptions(src/index.ts)虽然定义了method: 'GET',但它只在你完全不传requestOptions时生效;一旦你提供了requestOptions却没有method,请求会以method must be a string失败,所以请始终带上method;headers是全有或全无:省略它则保留 Cheerio 默认的Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8;提供了它就整体替换默认值,而不是追加。源码中streamOptions是{ headers: defaultRequestOptions.headers, path, ...requestOptions }的展开合并(src/index.ts),这正是「覆盖不合并」行为的来源。
性能与安全提示
fromURL依赖 undici,而 undici 仅在调用时才懒加载——源码注释(src/index.ts)明确说明:导入 undici 大约消耗 60ms 和 8MB 堆内存,因此不能因为引入fromURL就让所有使用者背上这个成本。从await import('undici')的写法可以看出这个优化。
安全方面,fromURL是 Cheerio 唯一会主动触网的 API(其余解析都不执行脚本、不请求网络),详见 安全指南。在把用户提供的 URL 交给fromURL之前务必校验——否则可能构成 SSRF。另外,Cheerio 解析开销与输入大小成正比,对不可信来源的标记应在上游限制大小。
如何选择正确的加载方法
综合以上分析,选型决策可以归结为两个问题:
- 输入是什么形态?字符串 →
load;Buffer→loadBuffer;已解码文本流 →stringStream;原始字节流 →decodeStream;URL →fromURL。 - 编码是否可知?不确定编码时,优先选择带编码嗅探能力的
loadBuffer和decodeStream,它们能识别<meta charset>与 BOM,并依据解析模式回退到windows-1252(HTML)或utf8(XML);编码确定时,load与stringStream更直接高效。
最后记住浏览器环境只提供load。凡涉及流式大文档或跨编码抓取,优先考虑loadBuffer/decodeStream的嗅探能力;需要完整 URL 解析时,用fromURL并善用其自动重定向、Content-Type校验与baseURI设置。各方法的测试用例集中在 src/index.spec.ts,可作为行为契约继续深入阅读。
【免费下载链接】cheerioThe fast, flexible, and elegant library for parsing and manipulating HTML and XML.项目地址: https://gitcode.com/gh_mirrors/ch/cheerio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考