在一台机器上,Node 里的 cheerio 用 2,927.89 ms 从一份 10 MB 的合成 HTML 文档中解析并提取了标题和 href。与此同时,运行在 CPython 上、并使用 C 语言后端解析器的 selectolax 完成同样的字段提取只用了 158 ms。排序后的标题文本和 href 哈希完全一致。这是一场跨运行时的端到端技术栈对比,不是单纯对某个解析算法下结论。
如果页面只有 10 KB,这个差距大概也就 2 倍,几乎没人会在意。真正的问题是:你的页面落在这条曲线的哪个位置。
什么是 cheerio
cheerio 是 Node 生态里使用 jQuery 风格语法的 HTML 解析器,而且它之所以成为这个生态中的默认答案是有原因的:30,449 个 GitHub stars、MIT 许可证,以及我开始测试的前一天仓库还有新的提交。测试版本:1.2.0。
官方参考:Cheerio 官方介绍。
import * as cheerio from "cheerio";
const $ = cheerio.load(html);
const titles = $("h3.title").map((_, e) => $(e).text()).get();
const hrefs = $("a").map((_, e) => $(e).attr("href")).get();
如果你写过 jQuery,上手这个 API 几乎没有门槛。也正是这种熟悉感,让它赢得了这个生态。
不过它的底层并不是单一解析器,而是一整套组件:解析使用 htmlparser2 和 parse5,DOM 树由 domhandler 和 domutils 处理,选择器依赖 cheerio-select,再加上 undici、encoding-sniffer 等等——11 个直接依赖,最终展开成 22 个顶层包,占用 9.0 MiB 磁盘空间。这些包提供了解析和编码相关能力,同时也带来了依赖体量。本次评测测试了异常 HTML 的输出,但没有测试编码正确性,也没有把两个解析后端分别隔离出来比较。
测量方法,以及为什么它可信
这套研究基线本来就已经有一套解析器基准测试:从 1 KB 到 10 MB 的五种页面尺寸、50 次迭代、3 轮独立运行,以及最关键的一点——内容哈希校验门槛,会把提取结果(排序后的标题和排序后的 href)与参考解析器进行比对。一个偷偷漏做工作的解析器不可能拿到漂亮的速度结果。
把 cheerio 加进去之前,必须先过两个检查。
参考结果是否仍然落在原来的位置? selectolax 在同一会话、同一测试样本上重新跑了一遍。它的内容哈希在 5/5 个尺寸上都复现成功,p50 结果与已发布数据相比落在 0.989× 到 1.079× 之间。所以,这就是那台生成原始表格的机器。
cheerio 是否提取出了相同的评分字段? 它在 Node 中按完全相同的规则计算内容哈希——对排序后的标题文本和排序后的 href 做 SHA-256——结果在 5/5 个尺寸上都与参考值一致。这证明了这些样本上排序字段的一致性,但并不能证明 DOM 形状、文档顺序、属性、文本规范化或错误恢复机制完全相同。
只有在这之后,时间数据才真正有意义。
| 页面大小 | selectolax | lxml | PyQuery | cheerio (Node) | cheerio vs selectolax |
|---|---|---|---|---|---|
| 1 KB | 0.0286 ms | 0.0508 | 0.0456 | 0.1147 ms | 4.0× |
| 10 KB | 0.1725 ms | 0.1802 | 0.1728 | 0.3490 ms | 2.0× |
| 100 KB | 1.4855 ms | 1.4145 | 1.4093 | 3.8399 ms | 2.6× |
| 1 MB | 14.97 ms | 15.03 | 14.96 | 59.37 ms | 4.0× |
| 10 MB | 158.10 ms | 165.25 | 162.86 | 2,927.89 ms | 18.5× |
p50 毫秒,三次运行的中位数。parser-bench.json。三个 Python 解析器是在同一个进程中运行的;cheerio 则运行在 Node 22 中,这既是运行时边界,也是库边界——见下文。
如何客观看这张表

1 KB 这一行基本是噪声。 在三个 Python 解析器之间,这个尺寸下的差异达到 77.6%,而且单次运行结果重叠得很厉害——比如 selectolax 的三次结果在 0.0267 到 0.0404 ms 之间。28 微秒这个量级里,计时器分辨率和调度开销占主导地位。我不会拿 1 KB 的结果给任何工具排序,包括 cheerio。
表格中间部分没什么戏剧性。 在 10 KB 到 1 MB 之间,差距大致在 2 倍到 4 倍。对于一个只抓几百个页面的爬虫来说,这意味着每页 45 毫秒而不是 15 毫秒,你根本感觉不到。
10 MB 这一行不是噪声。 cheerio 的三次结果分别是 2,839、2,928 和 2,954 ms——很稳定,而且明显和其他尺寸拉开了距离。10 MB 的端到端结果与较小尺寸时的模式明显不同。但仅凭五个尺寸点,不能证明其渐近复杂度,也不能判断到底是运行时、解析器、选择器、内存分配还是垃圾回收层导致了这个跃升。
它落在 BeautifulSoup 的区间里。 公开基准还测了同一个 10 MB 样本上的另外四个解析器,把 cheerio 的 2,927.89 ms 放到它们旁边,是本文最有价值的一部分:
| 解析器(10 MB) | p50 |
|---|---|
| selectolax (lexbor) | 159.93 ms |
| lxml | 172.93 ms |
| parsel | 231.85 ms |
| selectolax (modest) | 247.95 ms |
| BeautifulSoup + lxml | 2,261.56 ms |
| BeautifulSoup + html.parser | 2,788.75 ms |
| cheerio | 2,927.89 ms |
上面四行 Python 数据来自 bench_parse.json 的已发布结果;cheerio 的数据来自本次运行。参考解析器在两次运行之间复现结果落在 0.989×–1.079× 之间,因此把低于约 8% 的差异视为误差范围内更合理——例如 cheerio 与 BeautifulSoup 的 html.parser 后端相差 5%,算在这个范围内;但 cheerio 和 selectolax 相差 18 倍,显然不在此列。
BeautifulSoup 是很多人在想要省事、也接受它比较慢时会优先选择的库——在 Python 性能讨论里,它常常是“你应该换掉它”的那个对象。在 10 MB 文档上,cheerio 也落在同一性能带里,而不是那些经常被归到一起的 C 后端解析器那一档。
至于 Node 侧的替代方案,这篇文章并没有继续展开。更现代的 Node 备选项没有被测试,所以这里不能说明“换库不可能”,也不能说它们不够成熟。本文只展示了在这些 Python 技术栈对比下,cheerio 的实测表现。
这也是一场运行时对比,而不只是库对比。 cheerio 的毫秒数来自 Node 的 JIT 和垃圾回收器;其他结果来自 CPython 调用 C 后端解析器。内容哈希证明完成了相同的工作,而这些数字也正是开发者在选择技术栈时真实会遇到的成本——但没人应该把它解读成“cheerio 的算法比 selectolax 差 18 倍”。这只是本机、各自原生运行时里实际发生的结果。
环境与依赖情况
| 库 | 包数量 | 磁盘占用 | 许可证 | Stars | 最近提交 |
|---|---|---|---|---|---|
| cheerio | 22(npm) | 9.0 MiB | MIT | 30,449 | 2026-08-11 |
| PyQuery | 3(pip) | 20.1 MiB | BSD | 2,380 | 2026-07-27 |
官方参考:Cheerio 配置文档。
metadata-snapshot.json,采集于写作当天。
npm install cheerio 用时不到两秒,下载量为 9.0 MiB。在同一台机器上,另一次单独的转换运行中,冷启动导入耗时为 0.056 秒。
11 个直接依赖对于一个解析器来说不算少,如果你要审计依赖树,这一点很值得注意:htmlparser2、parse5、parse5-htmlparser2-tree-adapter、parse5-parser-stream、domhandler、domutils、dom-serializer、cheerio-select、encoding-sniffer、undici 和 whatwg-mimetype。里面其实包含了两套完整的解析器实现,因为 cheerio 可以根据需要使用其中任意一种。
三万多个 stars,而且在测试前一天还有提交,这在这个类别里已经是很健康的维护信号了。
内存占用,以及损坏 HTML 会带来什么影响
内存占用和异常 HTML 的处理方式会影响部署和故障处理,所以这里把它们单独测量。
更完整的压力测试背景在这篇 十个库的内存与畸形 HTML 对比 中。
峰值常驻内存 通过 /usr/bin/time -l 测得,每个单元格都使用一个全新的进程——导入基线表示库加载后空闲时的成本,峰值则把文档本身算进去。
| 库 | 运行时 | 导入基线 | 226 KB 峰值 | 10 MB 峰值 |
|---|---|---|---|---|
| html2text | python3.14 | 18.7 | 19.9 | 71.2 |
| pyquery | python3.14 | 30.3 | 33.9 | 172.5 |
| resiliparse | python3.14 | 20.5 | 25.1 | 225.1 |
| markdownify | python3.14 | 23.9 | 28.9 | 278.5 |
| goose3 | python3.14 | 44.1 | 52.4 | 398.5 |
| cheerio | node22 | 66.8 | 76.5 | 398.5 |
| justext | python3.14 | 30.3 | 36.6 | 431.2 |
| newspaper4k | python3.14 | 52.6 | 61.8 | 668.5 |
| trafilatura | python3.14 | 52.5 | 64.8 | 927.1 |
| turndown | node22 | 47.8 | 68.4 | 2947.1 |
memory-results.json。Python 和 Node 的基线彼此不能直接比较;解释器本身已经包含在两边之内。
在这个混合上下文表里,cheerio 的导入基线最高,达到 66.8 MiB,这其中包含 Node 运行时和依赖。它在 10 MB 样本上的进程峰值是 398.5 MiB。其他行里既有解析器,也有转换器和文章提取器,它们的主要工作不同,所以更适合拿来做进程占用背景,而不是横向性能排名。同一运行时下的 turndown 峰值更高,但它做的是转换,而不是 cheerio 这里测的“提取标题和链接”的契约。
畸形 HTML。 测试集包含 12 个文档,每个都只故意破坏一处:未闭合标签、嵌套错位的行内元素、带空格却未加引号的属性、多余闭合标签、根本没有 <html>、重复属性、在标签中间被截断的文档、错误实体、未闭合的 <script>、错误的字符集声明、包含标记的注释,以及 600 层嵌套;另外再加上 两个同尺寸的正常控制样本,因为“它什么都没返回”只有在该库对正常文档也沉默时,才能说明它对畸形输入的处理能力。
cheerio 在 14 个样本中 0 个报错、0 个返回空结果,并在畸形样本中恢复出了 11/22 个评分哨兵位(malformed-results.json)。对于解析器来说,评分器会检查 11 个可评分的畸形文档中的标题和链接哨兵;它不对段落哨兵评分,而且未闭合 <script> 的样本被排除在外。在这一部分没有同契约基线的情况下,11/22 不能算质量排名。我们能得出的结论是:cheerio 在所有 14 个畸形与控制输入上都返回了非空输出且没有报错,并且恢复了一半的评分标记。
优缺点
优点。 熟悉的 jQuery 风格语法。MIT 许可证。30,449 stars 和测试前一天还有提交,这些都说明它维护状态不错。它包含两个解析后端以及与编码相关的包,不过本次没有把后端恢复能力和编码准确性单独拆开测试。对所有样本尺寸来说,它提取后排序得到的标题 + href 哈希都与参考值一致。
缺点。 在 10 MB 文档上比 selectolax 慢 18.5 倍,在 1 MB 上也慢 4 倍。11 个直接依赖,其中还包括两套完整解析器实现。仅支持 Node。而且文档里也没有告诉你它在哪个尺寸下会不再是显而易见的选择。
谁适合用,谁不适合用
适合使用 cheerio 的情况:你在 Node 环境里,jQuery 式 API 很重要,而且你的代表性页面规模接近本次测试的 1 MB 以内范围。1 MB 是本次测试里最大的“常规”点,之后就是 10 MB 的明显跃升;本文并没有给出二者之间的分界点,也没有声称整个互联网有多少页面低于这个阈值。
在正式使用前先做基准测试,如果你要处理非常大的 HTML 文档,例如生成报表、目录导出或长列表页。XML sitemap 的行为没有测试。对于 10 MB 的 HTML 样本来说,每个文档 2.9 秒是会不断叠加的实打实成本。
如果你用的是 Python,这份对比告诉你的又是另一件事:selectolax、lxml 和 PyQuery 从 10 KB 开始几乎打平(差异在 0.5% 到 5.4% 之间,而且运行结果区间重叠),所以可以优先按 API 选择,而不是按速度。cheerio 跟它们三者之间的差距才是值得关注的数字,而不是它们彼此之间的细微差别。
托管 API 的位置
cheerio 处理的是你已经拿到手的 HTML。它不会去抓取页面、不会执行 JavaScript,也不会处理反爬层——而在很多真实目标站点上,后半段才是更难的部分。
托管式抓取 / 渲染 / 提取服务,包括我们自己的 Thunderbit,处在不同的责任边界上。本次没有对 Thunderbit 做基准测试。真正的区别在于:是对已有 HTML 做选择器解析,还是把采集、渲染和提取都外包出去;这篇文章并没有提供相同指标下的质量、延迟或成本对比。
更公平的说法是:如果你已经拿到了 HTML,也知道自己要什么选择器,cheerio 就是免费且顺手的。如果你要大规模抓页面,或者更愿意描述数据而不是 DOM,那就是另一类采购决策了。
想了解更广泛的选择,可以看看我们的 网页抓取 API 盘点 和 开源爬虫总览。如果解析后的内容最终要喂给模型,用 Python 将 HTML 转成 Markdown 这篇会说明哪些细节会在转换中丢失。
该不该用 cheerio?
如果你在 Node 里,而且 API 形态很重要,代表性文档又大致落在本次测试的小尺寸到 1 MB 区间,那么答案是:可以。
API 的熟悉度和当前的维护信号,都是合理的选型因素。但这项基准测试并不能证明某种支持答复一定存在,也不能证明测试中的页面尺寸分布与你的生产数据完全一致。
真正要记住的是 10 MB 这个数字。也就是说,在 1 MB 和 10 MB 之间的某个地方,cheerio 的成本不再跟随其他库,而是开始倍增——4 倍变成 18.5 倍。如果你的数据集中确实有这么大的文档,在决定之前一定要先测,因为库本身不会提醒你。
试试 Thunderbit 进行网页数据提取 Get Started Free
常见问题
拿 cheerio 跟 Python 解析器比,公平吗? 这是技术栈对比,不是算法对比。四个工具在 5/5 个页面尺寸上,都按哈希规则提取出了完全一致的排序后标题文本和 href。这并不能证明所有解析行为都等价。cheerio 的耗时包含 Node 的运行时表现,而其他结果包含 CPython 调用 C 后端解析器的成本;这个对比描述的是这些端到端选择。
为什么 1 KB 那一行不做排名? 因为在 28 微秒这个量级,测量已经被噪声主导了。三次运行里,Python 解析器的差异达到 77.6%,单次结果彼此还会重叠。这个尺寸下任何排序都只是偶然产物。从 10 KB 往上,结果才足够稳定。
10 MB 处为什么会突然跳升? 这项测试并没有说明原因。它能确认的是,这个跳升是真实存在的,不是噪声:cheerio 三次结果分别是 2,839、2,928 和 2,954 ms,明显与其他结果区分开;而 1 MB 时候的差距还只是 4 倍。要找原因,就得单独给 cheerio 的各个解析后端做性能剖析,而这超出了本次范围。
它到底有多少依赖?
11 个直接依赖,展开后是 22 个顶层包,占用 9.0 MiB 磁盘空间。其中有两个是完整解析器实现——htmlparser2 和 parse5——因为 cheerio 可以按需使用任意一个。这就是它同时支持宽容解析和符合规范解析的代价,如果你要审计依赖树,这点很值得知道。
这里没测什么?
当前草稿确实测了一个 226 KB 文档和一个 10 MB 文档的进程峰值内存,也测了一个包含 14 个输入的畸形 + 控制样本集合,其中 cheerio 没有报错、全部返回非空输出,并恢复了 11/22 个评分哨兵。但它没有测试 parse5-parser-stream 的流式处理、编码正确性、各后端的恢复行为、1 MB 到 10 MB 之间性能跃升的具体位置、XML 解析,也没有测试更新的 Node 替代方案。原始相对资源链接在发布时还要求保持相同的公开目录结构;否则就需要稳定的公开 URL 或仓库提交引用。


