PyQuery 为 lxml 增加了 jQuery 风格语法,而且在这项基准中没有带来影响决策的选择器开销

最后更新于 August 18, 2026
PyQuery 为 lxml 增加了 jQuery 风格语法,而且在这项基准中没有带来影响决策的选择器开销
AI 摘要
PyQuery 在 lxml 之上封装了 jQuery 风格 API。从 1 KB 到 10 MB 的五种页面大小来看,展示出来的五个中位数都低于原生 lxml,其中最大尺寸下差异为 1.5%。这项基准并不能证明这个包装层更快;它只是说明,在这次“选择并读取”的场景里,没有发现足以改变决策的差异。从 10 KB 往上看,PyQuery 的展示中位数也与 selectolax 相差几个百分点以内。由于没有预先定义等效阈值,这只能算是结果接近,而不是统计意义上的平局。

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

System diagram: jQuery Syntax Over lxml

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。

页面大小selectolaxPyQuerylxmlPyQuery vs lxml
1 KB0.0286 ms0.0456 ms0.0508 ms0.90×
10 KB0.1725 ms0.1728 ms0.1802 ms0.96×
100 KB1.4855 ms1.4093 ms1.4145 ms1.00×
1 MB14.97 ms14.96 ms15.03 ms1.00×
10 MB158.10 ms162.86 ms165.25 ms0.99×

p50 毫秒,三轮运行的中位数,同一进程内完成。parser-bench.json。所有尺寸下,三组内容哈希都与参考项一致。

没有明显包装层损耗

Measured results chart: PyQuery and lxml on the same fixture

PyQuery 在每个展示的中位数上都达到了 不高于原生 lxml 的结果。这并不意味着包装层能让解析更快。只有三轮运行,而且没有预先设定等效阈值,因此更稳妥的结论是:在这个样本里,没有出现会影响决策的选择器额外开销。

在 10 MB 时,PyQuery 的三次结果分别是 162.86、163.17 和 161.13 ms;lxml 的三次结果是 169.46、165.25 和 164.18 ms。两者范围接近,但并不重叠。在 1 MB 时,两者中位数差异只有 0.5%。这些小规模运行支持的是实用判断,而不是统计等效性的宣称。

System diagram: Wrapper and Parser Boundaries

机制其实很简单: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
lxml172.93 ms
parsel231.85 ms
selectolax (modest)247.95 ms
BeautifulSoup + lxml2,261.56 ms
BeautifulSoup + html.parser2,788.75 ms

发布数据来自 bench_parse.json

历史基准中的这些行显示,在这个样本上,BeautifulSoup 的速度比更快的解析器中位数慢了一个数量级以上。这些数据并没有和当前进程中的 PyQuery/lxml 配对重跑,所以更适合作为背景参考,而不是主结论的严格倍数依据。

Cheerio 的历史记录行是 2,927.89 ms(在 parser-bench.json 中为 2927.8857),而且内容哈希与提取结果一致。这个跨运行时的结果同样受 Node、包版本和历史运行控制条件影响,因此不能把它当作单纯的库速度倍数来看。

安装与依赖现实情况

包数量磁盘占用许可证Stars最后推送
PyQuery320.1 MiBBSD2,3802026-07-27
cheerio (Node)22 (npm)9.0 MiBMIT30,4492026-08-11

官方主页:PyQuery on PyPI

metadata-snapshot.json

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 峰值
html2textpython3.1418.719.971.2
pyquerypython3.1430.333.9172.5
resiliparsepython3.1420.525.1225.1
markdownifypython3.1423.928.9278.5
goose3python3.1444.152.4398.5
cheerionode2266.876.5398.5
justextpython3.1430.336.6431.2
newspaper4kpython3.1452.661.8668.5
trafilaturapython3.1452.564.8927.1
turndownnode2247.868.42947.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 才是信息损失最容易发生的地方。

免费试用 Thunderbit 进行网页数据提取

该不该用 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 的计时结果仍不参与排名。

Ke
Ke
Thunderbit 首席技术官 | 高级数据科学家与机器学习专家 Ke Shen 拥有近十年的机器学习和数据科学经验,毕业于哥伦比亚大学,曾任 Walmart Labs 高级数据科学家。他在 Python、R、Java 和统计学方面拥有深厚且备受同行认可的专业能力,并分享如何将复杂的 AI 算法从理论落地到生产级架构的实战经验。
目录
Thunderbit · AI 网页数据助手

1 次点击 内提取任意页面的数据

25 万+ 用户信赖
提供免费方案
从网页到表格
描述你需要的内容——Thunderbit 的 AI Agent 会帮你抓取并导出到 Excel、Google Sheets、Airtable 或 Notion。免费即可开始。
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week