几乎每篇关于“最快 Python HTML 解析器”的文章都会提到 selectolax,而且结论往往都停在“比 BeautifulSoup 快很多”。这部分没错。但很少有人继续往下说:如果把 selectolax 和 lxml 放在一起比,会发生什么——因为到了这里,“最快”后面就要加个星号了。
所以我认真做了一次基准测试:把 selectolax(两个后端都测了)与 lxml、基于 html.parser 和基于 lxml 的 BeautifulSoup,以及 parsel 放在一起,在 1 KB 到 10 MB 的五种页面规模上分别跑分;每个结果取三次独立进程运行的中位数。结果是:selectolax 远远甩开 BeautifulSoup,并且和原生 lxml 打成平手——但在纯解析环节又输给了 lxml。下面所有数据都只是单机结果,且仅供参考(macOS arm64,Python 3.14.2);脚本已经提交到仓库了,所以如果你要引用我,最好先在你自己的机器上跑一遍。
selectolax 到底是什么,不是什么
selectolax 是一个 Python 封装,底层连接了两个 C 引擎——Modest 和 Lexbor——用于解析 HTML5,并通过 CSS 选择器查询内容。它不是爬虫,不是浏览器,也不是那种“点一下按钮就能抓”的“scraper”。你在已经拿到 HTML 之后,把内容交给它解析即可。维护者自己的简介是一句话:“A fast HTML5 parser with CSS selectors, written in Cython, using Modest and Lexbor engines.”
它有两个后端,而且这两者的差异比文档里说得更重要:
LexborHTMLParser(Lexbor 引擎)——按照 README 的说法,2024 年起应该优先使用它。HTMLParser(Modest 引擎)——最早的版本;同一份 README 也提到它底层的 C 库“已经不再维护”。
在谈速度之前,还有几个事实值得先知道。以我截图时的 仓库状态(2026-07-10)来看,selectolax 有 1,653 个 star,最新版本是 v0.4.10(2026 年 5 月),而 PyPI 显示它支持 Python >=3.9,<3.15。安装这件事几乎是整篇评测里最不戏剧化的部分:pip install selectolax 直接拉下了一个 2.3 MB 的预编译 cp314 wheel,在 Python 3.14 上秒装可用——没有浏览器下载,没有 doctor 步骤,也不需要编译。纯解析器相对浏览器方案最大的优势之一,就是它装上就能跑。
还有一个许可证细节不能埋着不说:Python 封装本身是 MIT,但 wheel 里打包了已编译好的引擎,而这些引擎各自有许可证——Modest 是 LGPL-2.1,Lexbor 是 Apache-2.0。所以“selectolax 是 MIT”这句话对 Python 代码本身没错,但对你最终实际分发的二进制来说并不完整。如果你的法务团队会关注再分发组件,这一点必须标出来。
速度问题,用真实数字来回答
我测试的任务是:解析 HTML 字符串,提取所有 <h3 class="title"> 文本,再提取所有 <a> 的 href。延迟单位是毫秒,取三次独立进程运行的中位数;对于 C 后端解析器,在大多数页面规模下,跨运行波动都控制在约 5% 以内。在任何一个单元格计时之前,我都先把每个解析器的输出压成内容哈希,确保如果某个解析器偷偷少做了工作,也能被立刻发现并排除——在这些页面上,所有六个结果在每个规模下都一致,所以这确实是同口径比较。完整数据在已提交的 bench_parse.json 里。

| 页面大小 | selectolax(Lexbor) | selectolax(Modest) | lxml | parsel | BS (lxml) | BS (html.parser) |
|---|---|---|---|---|---|---|
| 1 KB | 0.027 | 0.029 | 0.036 | 0.044 | 0.281 | 0.323 |
| 10 KB | 0.160 | 0.171 | 0.166 | 0.228 | 1.705 | 2.049 |
| 100 KB | 1.464 | 1.565 | 1.423 | 2.020 | 16.5 | 20.6 |
| 1 MB | 14.901 | 16.9 | 14.177 | 20.9 | 181.9 | 232.6 |
| 10 MB | 159.9 | 247.9 | 172.9 | 231.9 | 2261.6 | 2788.7 |
对比 BeautifulSoup:大约快 12-17 倍,坊间说法还低估了它
把这些数字换算成比例后,selectolax-Lexbor 在 1 KB 页面上大约比 BeautifulSoup(html.parser) 快 12 倍,到 10 MB 时接近 17 倍;相较于 BeautifulSoup(lxml) 也快了大约 10-14 倍。网上常流传的“selectolax 比 BeautifulSoup 快 4-5 倍”这个说法,对 html.parser 场景来说明显偏保守;只有拿 lxml 版 BeautifulSoup 来比时,才大致接近。真实倍数取决于你说的是哪一种 BeautifulSoup,以及每页提取多少内容。
这也和 README 里自己的 benchmark 对得上,后者暗示它相对 BeautifulSoup(html.parser) 有 25.5 倍优势。两个数字都没错。README 的任务(针对较小的首页抓标题、链接、脚本和 meta)提取量更少,因此会更突出 BeautifulSoup 在每次解析时的固定开销。放到有限范围内来看,可以这样总结:selectolax 在现实中的“解析 + 提取”场景里,大致比 BeautifulSoup 快 10-15 倍;页面越小、提取越轻,这个倍数越高。
如果你当前的瓶颈就是一堆 BeautifulSoup 代码在啃网页,那这类迁移是很划算的。这个结论没什么争议。接下来那个就有争议了。
对比 lxml:打平,而 lxml 在大家常忽略的环节里更快
回头看 100 KB 和 1 MB 这两行。Lexbor 和 lxml 的差距都在约 5% 以内,单次运行的区间还会重叠;按我的口径,这就算平手——没有赢家,也谈不上“更快”。selectolax 真正拉开差距的地方是 10 MB 页面(159.9 ms 对 172.9 ms,差距 8.1%,且区间不重叠)。所以在完整任务上,selectolax 和 lxml 基本一致,只在超大文档上略占上风。

然后我把树构建和 CSS 查询拆开测了一下,结果就反过来了——这也是很多评测文里漏掉的关键。对于 纯解析、不做任何查询 的情况,在这台机器上 lxml 一直比 selectolax-Lexbor 快约 33-34%——例如 10 MB 页面上是 77.9 ms 对 116.6 ms。在完整任务里二者又会收敛,我的工作假设(注意,这不是我通过归因实验严格证明的)是:在这些页面上,CSS 查询只占总时间的一小部分,所以 lxml 在解析阶段建立的优势会被稀释,直到总耗时差不多。
这条结论是整篇评测里最容易被挑战的,我也想提前说清楚原因。它和大众认知相反,而我找到的另一篇只测纯解析的公开 benchmark——aows.jpt.sh——给出的结论正好相反,显示 selectolax 大约快 4 倍。所以我把结果圈定了范围:它只在一个平台上成立(macOS arm64、Python 3.14、预编译 cp314 wheel;Linux x86_64 或源码构建都没测),而且我在四种页面规模上重复验证过,结果始终一致;我还用两种不同的 lxml API 重新确认过,排除了 API 造成的假象。两种 lxml API 在每个规模上都比 selectolax-Lexbor 快。我不是要把“lxml 解析更快”说成铁律——我是在呈现我这次基准跑出来的结果,并附上了脚本。你也应该在自己的机器上跑一遍。
再补一刀:在一个扁平页面上一次性查询 100,000 个 <a> 时,lxml 和 selectolax-Modest 打平(33.30 ms 对 34.19 ms,区间重叠),而 selectolax-Lexbor 则比两者慢约 15%。这三种 C 引擎共同的优势是,在批量选择时都比 parsel 或 BeautifulSoup 快 5-7 倍——因为后两者是“每个节点一个 Python 对象”的模式,开销确实大。所以“selectolax 是批量 CSS 选择里最快的”这个说法也站不住:Modest 只是和 lxml 打平,Lexbor 甚至输给了它。
我愿意真正承担的结论是:selectolax 对 lxml 的优势,不在于整体任务速度。它只在最大页面上赢。它的价值更多体现在其他方面——API 体验、对脏输入的处理,以及现代 CSS 支持,这些才是这篇评测后半部分要讲的内容。
内存和冷启动:看 RSS 排名,不要看你的 profiler
内存这一节,我得纠正我自己前面的数字,而且这正是重点。以 10 MB 页面、关闭 tracemalloc 的情况下测得的 RSS 增量来看,BeautifulSoup 的内存占用大约是 selectolax 或 lxml 的 1.5-1.8 倍——最低约 1.51 倍(BS-lxml 为 218.4 MB,而 Lexbor 为 144.6 MB),最高约 1.75 倍。selectolax 和 lxml 都属于较轻量那一档,其中 lxml 的 RSS 最低。

我之前曾经写成“约 3 倍”,这个数字错了,而且错得很有启发性:当时我是在 tracemalloc 开着的情况下测的,而 tracemalloc 对每次分配的记录开销,会把高分配解析器的表观 RSS 大约翻倍。所以这里给所有做解析器内存 benchmark 的人一个提醒:请在关闭 profiler 的情况下,用 RSS 排名。 用 tracemalloc 峰值去给解析器排序,会特别容易把 C 后端排错——它会让 selectolax-Lexbor 看起来比 Modest 更重,但按真实 RSS 来看两者其实很接近。BeautifulSoup 在这里确实最重,但并没有那种被污染仪器显示出来的 3 倍差距。
冷启动影响不算大,但是真实存在:selectolax 的导入时间大约 14 ms,和 lxml 差不多,比 bs4 或 parsel 快大约 2.3 倍。如果你在做 CLI 工具或 serverless 函数,而且 import 时间会出现在每次调用里,这个差距就值得注意。
CSS 选择器覆盖:能力很强,但确实有几个真漏洞
CSS 覆盖我做了 41 个测试项,每个选择器都对照一个已知正确答案的 fixture 检查,同时还做了一轮故意“找茬”的测试,专门去试着把 Lexbor 引擎搞崩。每个用例都单独放到子进程里跑,因为其中一个会直接把整个解释器带走。结果如下:

| 引擎 | 通过 | 错误结果 | 不支持 | 进程中止 |
|---|---|---|---|---|
| soupsieve | 41 | 0 | 0 | 0 |
| selectolax Lexbor | 39 | 0 | 2 | 0 |
| lxml (cssselect) | 37 | 1 | 3 | 0 |
| parsel (cssselect) | 37 | 1 | 3 | 0 |
| selectolax Modest | 35 | 3 | 2 | 1 |
一旦把这些“敌对选择器”也算进去,Lexbor 就不是绝对赢家了——soupsieve 才是,41/41 全过,而 Lexbor 是 39/41。Lexbor 漏掉的两个是 :lang(en) 和 :dir(rtl),它们会直接返回解析错误。除此之外它都表现完美,包括 :has()、:is()、:where() 以及大小写不敏感属性。
不过 Lexbor 真正的亮点是在和 cssselect 体系对比时体现出来的。README 里的明星选择器——div > :nth-child(2n+1):not(:has(a))——在两个 selectolax 后端和 soupsieve 上都能返回正确结果,但在 lxml 和 parsel 上会返回错误结果,而且不会报错。也就是说,照抄这个选择器到 Scrapy 或 parsel 的爬虫里,你会悄悄拿到错误数据。这里把语境说准确一点:cssselect 从 1.2.0 版本(2022 年)开始就已经解析 :has() 了,我测试的是 1.4.0,所以问题不是“完全不支持”,而是“支持了但组合表达式算错了”。这个具体组合的“静默错误结果”并不在 cssselect 的 issue 跟踪里;那里记录的是 :has() 的限制会抛出错误。Lexbor 还能处理大小写不敏感属性标志 [data-role="LEAD" i],而 cssselect 则直接拒绝。
但真正决定迁移成败的还有两个缺口。selectolax 完全不支持 XPath——两个后端都没有 xpath()。同时它也没有 ::text / ::attr() 这类伪元素,因为那是 parsel/Scrapy 的扩展,不是标准 CSS。如果你现有爬虫大量依赖 XPath,那你碰到的不是“换个库”这么简单,而是得重写选择器,这是迁移到 selectolax 的最大门槛。反过来看,Lexbor 还提供了一个 :lexbor-contains("text" i) 伪类,可用于大小写不敏感的文本匹配;这一点无论 lxml、parsel 还是标准 CSS 都没有,而且它确实按文档工作。
对乱七八糟的 HTML 的鲁棒性:这正是 selectolax 该拿分的地方
真正的网页抓取,意味着你得把垃圾输入丢给解析器,还指望它别崩。 我跑了 18 个对抗性输入,这一类场景里,selectolax 相比 lxml 的优势最明显。
把空字符串或只有空白字符的内容交给 lxml.html.fromstring,它会抛 ParserError("Document is empty")。而 selectolax 的两个后端都会直接返回一个合法的空树。 如果你的爬虫是在一串 URL 上循环,里面有些响应是空的,那你就少了一层 try/except 包装。selectolax 还能处理 100,000 个元素而不发生栈溢出。
最尖锐的分歧出现在深层嵌套上。面对 1,000 层和 5,000 层嵌套的 <div>,lxml 会悄悄丢掉最深层内容,而 selectolax 会保留它。 libxml2 大约把解析深度限制在 256 层左右,并且不会报错,而是直接截断树,所以最深层的文本就根本拿不到。两个 selectolax 后端都会返回完整树。这和后面会提到的 <template> 陷阱正好相反:在那里是 Lexbor 丢了其他引擎保留的内容;这里则是 lxml 丢了 selectolax 保留的内容。
当然也不是每个项目都赢。Modest 后端在遇到 :dir() 时,会 直接用 SIGABRT 终止整个 Python 解释器——不是抛一个你能捕获的异常,而是硬杀进程。对于仍然在用旧后端的人来说,这就是一个非常真实的稳定性问题,也正是那种平时看不见、直到凌晨三点把生产任务打挂时才会暴露的坑。
上线前必须知道的两个“静默丢数据”陷阱
这两个都不是新发现——上游文档里其实都写过——但它们都会无声地让你丢数据,而且 README 里并没有把风险说透。
Lexbor 会漏掉 <template> 里的 <a>
我在一个真实的 MDN 页面上测试时,selectolax-Lexbor 只找到了 497 个链接,而 lxml、两个 BeautifulSoup 后端,甚至 selectolax 自己的 Modest 后端都找到了 508 个。 少掉的 11 个,是一个语言切换器和一个讨论链接,它们都藏在 <template> 元素里(这个页面用了 Lit Web Components)。

根本原因是合理的:按照 HTML5 规范,<template> 的内容会被解析进一个独立、惰性的片段里,而不是正常 DOM;Lexbor 严格遵守了这个规则——tree.css("a") 不会下钻到 template 内容里。lxml、两个 BeautifulSoup 后端,以及 Modest 都会把 template 内容平铺进主树,所以能找到那些链接。这是一个已经公开的 issue(selectolax#146,引擎根因在 lexbor#170),而且两种解读都说得通——从规范角度看,Lexbor 甚至更“正确”。但问题在于:使用推荐后端的开发者会在没有任何报错的情况下悄悄漏掉这部分数据。反过来说,其他解析器会暴露浏览器本来不会渲染的惰性 template 内容,因此它们也可能把用户根本看不到的“幻影数据”给你。这个特定场景下,比较稳妥的绕法就是改用 Modest 后端,或者直接换别的库。
非 UTF-8 字节会悄悄把 .text() 搞坏
如果你喂给 selectolax 的是非 UTF-8 的 bytes,解析本身会成功——坏掉的问题会在后面才冒出来,而且比直接崩溃更糟。比如 "<p>café éè</p>".encode("latin-1"):Lexbor 的 .text() 会返回替换字符,Modest 的 .text() 会悄悄把出问题的字节丢掉,而且两个引擎只有在你去访问 .html 时才会抛 UnicodeDecodeError。也就是说,这个绑定是在读取结果时才按严格 UTF-8 解码,而不是在解析时就处理。这个问题和一个 已知的 selectolax issue 有关,核心是编码/解码的严格性。
修复方法只有一行,而且最好形成肌肉记忆:先自己把 bytes 解码成字符串——LexborHTMLParser(resp.content.decode("latin-1"))——这样两个引擎都会正确返回 'café éè'。实际使用里,最好永远把 str 传给 selectolax,不要直接喂非 UTF-8 的原始 bytes。README 里没有把这一点写得足够清楚。
生产维度(单次观测,所以只能看趋势)
下面这些结果我只测了一次,没有像前面那样做三次重复,所以我会把它们标记成信号,而不是最终定论。
线程扩展是最有意思的点。把一个 1 MB 页面解析 48 次,并发放在四个线程里跑,selectolax 的墙钟时间加速大约 3.5-3.9 倍——这正是一个库在 C 解析阶段释放 GIL 时会呈现出的经验特征;而 BeautifulSoup(lxml) 在线程下反而变慢了好几倍,这说明工作在 GIL 下被串行化了。lxml 介于两者之间,结论不够确定。对于 Python 正在迈向的 free-threading 时代来说,selectolax 这种能跨线程并行解析、而 BeautifulSoup 做不到的特性,确实是一个真实但仍需进一步验证的优势。这里是单线程数、单页面规模的结果,而且对其机制的判断只是推测,我并没有用 C 代码插桩去严格证明。
再看内存泄漏:在 1 MB 页面上重复进行 2,000 次“解析-提取-释放”循环,三个解析器都没有出现那种线性攀升的 RSS 曲线——它们都稳定在一个有限的工作集范围内。我之所以相信这个结果,是因为我用同一套仪器跑了一个已知会泄漏的对照对象,它按照预期一路涨到了 +198 MB,说明这套仪器确实能看到泄漏,只是没有在这些解析器里发现。并且,一个在所属树离开作用域后仍被保留的节点句柄,依然可以正常使用,没有段错误。再次强调,这些都只是单次观察,不是长时间 soak 测试。
selectolax 适合什么,不适合什么
上面所有内容都只围绕一件事:把你已经拿到的 HTML,快速转成结构化数据。这个工作,selectolax 很擅长。它明确不做的事情包括:抓取页面、渲染 JavaScript、轮换代理、解决 CAPTCHA,或者替你判断“你到底要哪些元素”。这些都还是你的代码来做。selectolax 只是解析层,它也不假装自己能包办更多。
这也正是“托管式提取服务”出现在解析器上层而不是取代解析器的原因。如果你不想自己搭建并维护“抓取-渲染-反爬-提取”整套流程,Thunderbit 可以把这套能力以 API、MCP server 和 CLI 的形式提供出来——POST /distill 可以把页面转换成干净的 Markdown,POST /extract 可以直接返回与 schema 匹配的结构化 JSON,JavaScript 渲染和反爬处理都帮你做好。它处理的是同一条链路里的另一层:当你已经拿到了 HTML、并且希望在自己控制下追求原始解析速度时,用 selectolax;当你希望抓取和提取都交给别人,只拿结构化数据回来时,就可以考虑 Thunderbit 的 API、MCP server 或 CLI。它们不是替代关系,而是同一技术栈不同高度的工具。
优缺点,以及谁真的该用它
selectolax 的优势:
- 在现实的解析 + 提取任务里,比 BeautifulSoup 快大约 12-17 倍,而且在三个数量级的页面大小上都很稳定。
- 内存占用轻(和 lxml 同一档位,约比 BeautifulSoup 轻 1.5-1.8 倍),导入时间约 14 ms。
- 对会让 lxml 出问题的输入更友好,比如空内容、纯空白,以及极深层嵌套。
- 支持现代 CSS,包括
:has()、:is()、:where()、大小写不敏感属性,以及仅 Lexbor 支持的:lexbor-contains()。 - DOM 读写对
None友好:缺失元素返回None或[],不会直接抛错,而且你还可以实际修改并重新序列化树。 - 持续维护中(v0.4.10,2026 年中),安装也非常简单。
selectolax 的短板:
- 并不比 lxml 广泛更快——完整任务上只是打平,在我这里纯解析还输给了 lxml。
- 不支持 XPath,也没有
::text/::attr()——这对依赖 XPath 的爬虫来说是硬迁移门槛。 - 有两个会静默丢数据的坑:Lexbor 的
<template>内容,以及通过.text()处理非 UTF-8 bytes。 - Modest 后端属于旧实现,遇到
:dir()会直接 SIGABRT。 - 这里所有数字都只基于单个平台(macOS arm64,Python 3.14),而且只是趋势性结果。
要不要用 selectolax?如果你希望获得接近 lxml 的解析速度,同时又想要一个更友好、对 None 更安全的 API,而且愿意只在 CSS 领域里工作,那么答案是:要。 如果你的代码库是围绕 XPath 构建的,那重写成本是实打实存在的,你应该认真权衡。至于“谁是单一最快解析器”这个问题,从这次 bench 的结果看,selectolax 和 lxml 已经接近到足以让你把胜负手交给易用性和鲁棒性,而不是纯速度。其实这才是选工具更靠谱的理由。
试试 Thunderbit 进行网页数据提取 Get Started Free
常见问题
selectolax 比 BeautifulSoup 快吗?
是的,非常明显——在真实的“解析 + 提取”任务里,BeautifulSoup(html.parser) 大约慢 12-17 倍,BeautifulSoup(lxml) 也慢 10-14 倍;在 1 KB 到 10 MB 的页面范围内都很稳定(macOS arm64,Python 3.14)。坊间常说的“快 4-5 倍”低估了它相对 html.parser 的优势。
selectolax 比 lxml 快吗? 不能算普遍更快。在完整的解析 + 提取任务里,100 KB 和 1 MB 页面上它们打平,只有 10 MB 页面 selectolax 才赢。在纯解析、没有查询的情况下,我这台机器上反而是 lxml 快了约 33-34%;这个结果和主流认知相反,所以请务必在你自己的硬件上验证。
Lexbor 后端和 Modest 后端该选哪个?
绝大多数情况下都选 Lexbor——它是维护中的、功能更完整的引擎,也是 README 推荐的版本,CSS 覆盖也更好。唯一例外是页面把内容藏在 <template> 元素里时,Lexbor 会按规范把这些内容排除在外,而 Modest 恰好会保留它们。不过 Modest 也有明显问题,包括在遇到 :dir() 时会直接把解释器打崩。
selectolax 支持 XPath 吗?
不支持。两个后端都没有 xpath() 方法——selectolax 只支持 CSS。如果你的爬虫依赖 XPath,那迁移就意味着要重写选择器,这也是从 lxml 或 parsel 迁移到 selectolax 时最大的单项成本。
为什么我的 selectolax 输出乱码或缺少元素?
通常有两个原因。第一,如果文本里出现替换字符或重音符号丢失,说明你可能传入了原始的非 UTF-8 bytes——请先把它们解码成 str(例如 resp.content.decode("latin-1"))再解析。第二,如果现代网站上链接或元素缺失,它们可能被放在 <template> 标签里,而 Lexbor 后端不会进入这些内容;这种页面可以改用 Modest,或者换别的解析器。


