在一台机器上,Node 里的 cheerio 解析了一个 10 MB 的合成 HTML 文档,并提取标题和 href,用时 2,927.89 ms。而通过 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 负责解析,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 相对 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 ms 到 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 的端到端结果和小尺寸时的模式已经明显偏离。仅靠 5 个尺寸点,无法判断渐近复杂度,也无法确定究竟是运行时、解析器、选择器、内存分配还是垃圾回收哪一层导致了这个跃升。
它落在 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 是很多人图省事时会选的库,也正因为它慢,大家才总是建议把它换掉。面对 10 MB 文档时,cheerio 处在同一速度带的底部,而不是与那些 C 后端解析器同一速度带。
至于 Node 侧的替代方案,这篇文章并没有得出结论。没有测试更新的 Node 替代库,所以这个结果不能说明“换库不可能”,也不能说它们不够成熟。它只是展示了当前测得的 cheerio 路径,与列出的 Python 栈相比是怎样的表现。
这既是运行时比较,也是库比较。 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 所测的标题和 href 提取任务。
异常 HTML。 12 个文档,每个都只故意破坏一处:未闭合标签、嵌套错误的内联元素、带空格却没加引号的属性、多余的闭合标签、完全没有 <html>、重复属性、在标签中间被截断的文档、错误实体、未闭合的 <script>、虚假的 charset 声明、包含标记的注释,以及 600 层嵌套;另外再加上 2 个相同大小的正常控制样本,因为“它什么都没返回”只有在同尺寸的正常文档上也保持沉默时,才能说明它对异常 HTML 的表现。
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 到 10 MB 之间性能跃升的位置、XML 解析,以及更新的 Node 替代方案。


