PyQuery 在 lxml 上面封装了一层 jQuery 风格的 API。横跨 1 KB 到 10 MB 的五种页面规模,展示出来的五个中位数都低于原生 lxml,其中最大尺寸下差异为 1.5%。这项基准并不能证明这个包装层更快;它只是说明,在这次“选择并读取”的场景里,没有发现足以改变决策的差异。
从 10 KB 往上看,PyQuery 的展示中位数也与 selectolax 相差几个百分点以内。由于没有预先定义等效阈值,这只能算是结果接近,而不是统计意义上的平局。
PyQuery 是什么
PyQuery 是一个 Python 库,它把 jQuery 那套选择器和链式 API 带到了 lxml 文档树上。测试版本:2.1.0,BSD 许可,GitHub stars 2,380,59 个开放 issue,最后一次推送时间为 2026-07-27。
官方文档:PyQuery documentation。

from pyquery import PyQuery as pq
d = pq(html)
titles = [e.text_content() for e in d("h3.title")]
hrefs = [e.get("href") for e in d("a")]
它返回的元素本质上还是 lxml 元素,所以你会用的 lxml 方法,这里基本都还能继续用。PyQuery 的定位就是提升易用性,而不是重新实现解析器。执行 pip install pyquery 会拉取 3 个包——lxml、cssselect 和 PyQuery 本身——总计 20.1 MiB,几乎全部都是 lxml 的编译扩展。
如果你用过 Node 里的 cheerio,会发现它们的 API 思路很接近。这里实际测试的两个选择器在两边都能跑通;但这并不意味着 cssselect 和 cheerio 之间的选择器语言完全一一对应。
测量方法
这套研究基础已经有一份解析器基准测试,而它具备很多 benchmark 没有的特性:内容等价校验门槛——把提取出的内容(排序后的标题和排序后的 href)与参考解析器做哈希比对,这样某个库如果偷工减料,就不可能拿到“快”。测试覆盖五种页面大小,50 次迭代,三轮独立运行。
把 PyQuery 加进来,需要做两件事。
重新运行参考项。 selectolax 在同一进程里重新跑了一遍。它的内容哈希在 5/5 个规模上都复现成功,p50 落在已发布结果的 0.989× 到 1.079× 之间——说明机器和测试环境都是同一套。
也在同一进程里运行 lxml。 已发布基准只记录了机器和 Python 版本,没有记录库版本,所以它里面的 lxml 行,可能来自与这里 PyQuery 所包装的不同 lxml 版本。如果直接跨这个差异比较,就等于把两个 lxml 版本拿来比,然后误以为是包装层带来的损耗。把 lxml 也放到同一进程里跑,就消除了这个疑问——在这个虚拟环境中,两者都使用 lxml 6.1.1。
| 页面大小 | selectolax | PyQuery | lxml | PyQuery vs lxml |
|---|---|---|---|---|
| 1 KB | 0.0286 ms | 0.0456 ms | 0.0508 ms | 0.90× |
| 10 KB | 0.1725 ms | 0.1728 ms | 0.1802 ms | 0.96× |
| 100 KB | 1.4855 ms | 1.4093 ms | 1.4145 ms | 1.00× |
| 1 MB | 14.97 ms | 14.96 ms | 15.03 ms | 1.00× |
| 10 MB | 158.10 ms | 162.86 ms | 165.25 ms | 0.99× |
p50 毫秒,三轮运行的中位数,同一进程内完成。parser-bench.json。所有尺寸下,三组内容哈希都与参考项一致。
没有明显包装层损耗

PyQuery 在每个展示的中位数上都达到了 不高于原生 lxml 的结果。这并不意味着包装层能让解析更快。只有三轮运行,而且没有预先设定等效阈值,因此更稳妥的结论是:在这个样本里,没有出现会影响决策的选择器额外开销。
在 10 MB 时,PyQuery 的三次结果分别是 162.86、163.17 和 161.13 ms;lxml 的三次结果是 169.46、165.25 和 164.18 ms。两者范围接近,但并不重叠。在 1 MB 时,两者中位数差异只有 0.5%。这些小规模运行支持的是实用判断,而不是统计等效性的宣称。

机制其实很简单:pq(html) 先构建一次 lxml 树,d("h3.title") 像 tree.cssselect() 那样通过 cssselect 编译 CSS 选择器,返回的元素仍然是 lxml 元素。也就是说,这个测到的热点路径里,PyQuery 本身做的工作其实很少。遍历、修改、重复查询、导入和内存占用,都不在这次“选择器时间”结论的范围内。
从 10 KB 起,结果就已经很接近
更有价值的发现,其实是第一列。
从 10 KB 往上看,selectolax、lxml 和 PyQuery 这三者中,最快和最慢的中位数差距在 10 KB 时为 4.5%,100 KB 时为 5.4%,1 MB 时为 0.5%,10 MB 时为 4.5%。这次运行并不是等效性检验;更实际的判断是,对于这个工作负载,这些差距不足以改变大多数解析器选择。
selectolax 在 1 KB 时确实更快——0.0286 ms 对 0.0456 和 0.0508——但这一行其实不适合用来下结论。三种解析器在这个规模下的差距达到 77.6%,而 selectolax 自己三次运行的范围也从 0.0267 ms 到 0.0404 ms。28 微秒这个量级上,计时器和调度器本身的影响都已经很大了。我不会在这里对任何方案做排名。
对于这种“选择 + 读取”的工作流,真正该依据的是 API 习惯和测得的依赖事实,而不是预设的速度等级。PyQuery 对 lxml 没有表现出会影响决策的性能损失。selectolax 使用的是不同的解析器栈,但本文并没有在同等条件下测量它的安装体积、wheel 覆盖范围或构建要求。
作为对照,已发布基准还在同一个 10 MB 样本上给出了另外两个 Python 方案,而它们的差异就明显多了:
| 解析器(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 |
发布数据来自 bench_parse.json。
历史基准中的这些行显示,在这个样本上,BeautifulSoup 的速度比更快的解析器中位数慢了一个数量级以上。这些数据并没有和当前进程中的 PyQuery/lxml 配对重跑,所以更适合作为背景参考,而不是主结论的严格倍数依据。
Cheerio 的历史记录行是 2,927.89 ms(在 parser-bench.json 中为 2927.8857),而且内容哈希与提取结果一致。这个跨运行时的结果同样受 Node、包版本和历史运行控制条件影响,因此不能把它当作单纯的库速度倍数来看。
安装与依赖现实情况
| 库 | 包数量 | 磁盘占用 | 许可证 | Stars | 最后推送 |
|---|---|---|---|---|---|
| PyQuery | 3 | 20.1 MiB | BSD | 2,380 | 2026-07-27 |
| cheerio (Node) | 22 (npm) | 9.0 MiB | MIT | 30,449 | 2026-08-11 |
官方主页:PyQuery on PyPI。
3 个包的依赖 footprint 算是相当干净,而且其中 2 个——lxml 和 cssselect——很多 Python 抓取项目本来就已经在用了。这样一来,PyQuery 的额外成本其实只有几十 KB。
20.1 MiB 说的是 lxml 的编译扩展,不是 PyQuery 本身。换句话说,这就是你直接使用 lxml 也要付出的那 20 MiB 左右。
这个包采用 BSD 许可证。在这个带时间戳的快照里,它有 59 个未关闭 issue,且在测试前三周有一次推送;但仅凭这些信息,不能直接推断维护质量或未来兼容性。
内存,以及损坏 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 的基线彼此不能直接比较;解释器本身就包含在这两个数字里。
PyQuery 在大文档上的内存占用低于 resiliparse——172.5 MiB 对 225.1 MiB——尽管它的导入基线更高。lxml 的树结构很紧凑,PyQuery 30.3 MiB 的导入基线大部分其实来自 lxml 的加载,而不是 PyQuery 自己做了什么。
错误 HTML。 共 12 份文档,每份都只“坏”了一处——未闭合标签、嵌套错误的行内元素、带空格却未加引号的属性、多余的闭合标签、根本没有 <html>、重复属性、在标签中间被截断的文档、错误实体、未闭合的 <script>、伪造的字符集声明、包含标记的注释,以及 600 层嵌套——另外再加上 两份同尺寸的正常文档对照组,因为“它什么都没返回”只有在同尺寸的干净文档上它也同样沉默时,才能说明和“格式损坏”有关。
pyquery 在 14 个测试中没有抛异常,在 1 个测试中什么都没返回,并且在这些损坏样本里恢复了 10/22 个哨兵值(malformed-results.json)。其中有一个样本不计入这个统计:根据 HTML5 规范,未闭合 <script> 之后的所有内容本来就属于脚本内容,所以在那里丢掉这些内容才是正确行为,恢复出来反而算偏差。由于这里没有把其他解析器的哨兵结果并排放出来,10/22 只能算鲁棒性观察,不能据此排出解析器优劣。
优缺点
优点。 jQuery 语法,对写过前端 JavaScript 或用过 cheerio 的人来说很熟悉。在这个样本中,相比原生 lxml 没有出现会影响决策的选择器开销。只有 3 个包,而且其中 2 个很可能你本来就已经有了。它返回的是 lxml 元素,所以 lxml 的方法依然可用。BSD 许可。五种页面大小下,内容哈希都与参考项一致。
缺点。 20.1 MiB,原因是 lxml。2,380 个 stars 意味着社区规模比 cheerio 的 30,449 小得多——遇到怪问题时,可参考的实战例子也会更少。它本质上是一个便捷层,所以 lxml 做不到的事情,它也做不到。要是你希望 jQuery API 顺带带来性能提升,那就别指望了:它带来的是易用性,真正干活的还是底层解析器。
谁适合用,谁不适合
适合用 PyQuery:如果你或你的团队更喜欢 Python 里的 jQuery 风格选择器。测试中的构建、两次选择和读取过程,没有显示出相对 lxml 的明显性能损失;但 PyQuery 的其他操作并没有计时。
适合直接用 lxml:如果你偏好 XPath,或者想少装一个包。这次运行没有给出足以在两者之间做性能取舍的理由。
可以评估 selectolax:如果它的解析器 API 和依赖栈更适合你的项目。1 KB 那一行明确不参与排名,而本文也不足以支持“依赖最少”的说法。
在 Node 里,cheerio 是对应的 API 形态。这里存档的跨运行时结果确实更慢,但由于运行时和历史测试条件不同,不能据此得出纯粹的库级结论。
托管式 API 适合什么场景
PyQuery 处理的是你已经拿到手的 HTML。它不会抓取页面、不会渲染 JavaScript,也不会处理反爬层——这里对比的任何解析器都做不到,而在真实网站上,这往往才是更难的那一半。
作者说明:Thunderbit 是我们提供的托管方案,负责从 URL 抓取、渲染和提取。这次并没有拿它和 PyQuery 做基准对比。真正该分辨的是:你是已经拿到了 HTML,只想本地做选择器操作;还是希望把页面获取和提取当成一项服务交给平台。
更坦白地说:如果 HTML 已经在手,而且你也清楚自己的选择器,PyQuery 就是省事又顺手的选择。可如果选择器总是失效,或者你要大规模抓取,那就是另一类需求了。
如果你想看更大的全局对比,可以参考我们的 web scraping API roundup;如果你关注自托管方案,则可以看 open-source scraper pillar。如果解析结果还要喂给模型,那么 用 Python 将 HTML 转成 Markdown 才是信息损失最容易发生的地方。
该不该用 PyQuery?
如果你想在 Python 里使用 jQuery 风格语法,而且这次测到的“选择 + 读取”路径确实代表你的工作负载,那答案是:可以。
基准测试没有发现相对 lxml 会影响决策的选择器开销,同时还保持了内容哈希一致。它并没有证明这个库整体没有成本。
在五种页面大小下,这三种 Python 解析器的中位数都足够接近,API 适配度很可能比性能排名更重要。若要把这个判断扩展成更宽泛的解析器排序,最好先定义等效阈值,再把这些替代方案按完全相同的条件重跑一次。
免费试用 Thunderbit 进行网页数据提取 Get Started Free
常见问题
PyQuery 会拖慢 lxml 吗?
这次运行里没有出现会影响决策的选择器开销。五种页面规模下,它的中位数都达到了不高于原生 lxml 的表现,而且两者都在同一进程中使用 lxml 6.1.1。10 MB 时,两者的范围不重叠:PyQuery 是 161.13–163.17 ms,lxml 是 164.18–169.46 ms。pq(html) 会先构建 lxml 树,测试中的选择器则通过 cssselect 编译。
selectolax 比 PyQuery 快吗?
在 1 KB 时它的中位数更低,但这行不参与排名,因为在微秒级别上波动已经占主导。从 10 KB 往上看,中位数差距在 0.5% 到 5.4% 之间。对这个工作负载来说,这叫接近,不足以证明等效,也不代表每种情况下区间都重叠。
为什么要重新跑 lxml,而不是直接引用已发布数值?
因为已发布基准只记录了机器和 Python 版本,没有记录库版本。它里面的 lxml 结果,可能来自与今天 PyQuery 包装的不同 lxml 版本;如果版本不同,差异就会被误认为是包装层损耗,而实际上并不是。把两者放在同一进程里、统一到 lxml 6.1.1,就把这个歧义消掉了。
它和 cheerio 怎么比?
API 思路相似,但生态不同。这里测试的两个选择器在两边都能正常工作,而且五种页面大小下内容哈希都一致;但这并不能证明所有选择器都完全兼容。cheerio 的存档时间结果更慢,但由于跨运行时和历史测试条件存在差异,不能把它当作纯库速度倍数。
这里没测什么?
内存测的是仅导入、226 KB 文档和 10 MB 文档时的峰值 RSS。错误 HTML 用了 12 份损坏文档加 2 份同尺寸对照。仍未测试的包括:PyQuery 的修改与遍历性能、重复查询缓存、URL 抓取、并发能力,以及真实网站上的代表性工作负载。1 KB 的计时结果仍不参与排名。


