PyQuery 在 lxml 之上包了一層 jQuery 風格的 API。從 1 KB 到 10 MB 這五種頁面大小來看,五組顯示出的中位數都比原生 lxml 更低;就算是在最大尺寸時,也只差 1.5%。不過,這份基準測試不能證明這個包裝層比較快;它只說明在這種選擇器加讀取的情境下,差異小到不太會影響選擇。
從 10 KB 起,在顯示的中位數裡,它也都和 selectolax 相差幾個百分點以內。若沒有先定義等效範圍,這最多只能算結果很接近,不能當成統計上的平手。
什麼是 PyQuery
PyQuery 是一個 Python 函式庫,讓你可以用 jQuery 式的選擇器和鏈式 API 來操作 lxml 文件樹。測試版本:2.1.0,BSD 授權,2,380 個 GitHub stars,59 個開放議題,最後一次推送時間為 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")]
PyQuery 回傳的元素本質上就是 lxml 元素,所以你如果會 lxml 的用法,在這裡也一樣能用。這正是它的設計概念:PyQuery 是用來提升操作手感的外層,不是解析器本身。pip install pyquery 會裝入 3 個套件——lxml、cssselect 與 PyQuery 本身——總計 20.1 MiB,幾乎全部都是 lxml 的編譯擴充模組。
如果你用過 Node 裡的 cheerio,這在 Python 裡基本上就是同一種 API 思路。這裡測的兩個選擇器在兩邊都能正常運作;但這份測試不能證明 cssselect 和 cheerio 在選擇器語言上完全對等。
測量方式
這個研究基礎本來就有一套解析器基準測試,而它比多數 benchmark 多了一個特點:內容一致性門檻,會把擷取出的內容——排序後的標題與排序後的 href——和參考解析器做雜湊比對,所以不可能讓「少做事」的函式庫直接拿到好看的數字。測試規模為五種頁面大小、50 次迭代、3 組獨立執行。
把 PyQuery 加進來需要兩件事。
重新執行參考項。 selectolax 在同一個行程中再次執行。它的內容雜湊在 5/5 種尺寸都能重現,而且 p50 落在已發表數值的 0.989× 到 1.079× 之間——這代表是同一台機器、同一個測試環境。
也在同一個行程中執行 lxml。 已發表的 benchmark 記錄了機器和 Python 版本,但沒有記錄函式庫版本,因此其中的 lxml 列,可能來自和這裡 PyQuery 所包裝的不同版本。如果直接跨過這個差異比較,就會把兩個不同的 lxml 版本混在一起,然後誤以為是包裝層的成本。把 lxml 一起跑,這個問題就消失了——這個 venv 裡兩者都使用 lxml 6.1.1。
| 頁面大小 | selectolax | PyQuery | lxml | PyQuery 相對 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") 會透過 cssselect 把 CSS 選擇器編譯起來,就像 tree.cssselect() 一樣,而回傳的元素也是 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 到 0.0404 ms 不等。以 28 微秒來看,計時器和排程器的影響已經蓋過解析器本身。我不會在那裡做排名。
對這種選擇器加讀取的工作負載來說,這三者應該優先根據 API 和實際依賴條件來選,而不是預設哪個一定比較快。PyQuery 對 lxml 沒有顯示出足以影響決策的成本。selectolax 用的是不同的解析器堆疊,但這篇文章沒有在同一個基準下測它的安裝體積、wheel 覆蓋範圍或建置需求。
如果要做對照,已發表的 benchmark 在同一個 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。
歷史 benchmark 的這些列顯示,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 個套件的依賴足跡算是相當乾淨,而且其中 2 個——lxml 與 cssselect——對很多 Python 網頁爬取專案來說本來就已經存在。若是這樣,PyQuery 的額外成本就只剩幾十 KB。
20.1 MiB 指的是 lxml 的編譯擴充模組,不是 PyQuery 本身。這和你直接使用 lxml 時要付出的 20 MiB 是同一筆成本。
這個套件採 BSD 授權。以當時的快照來看,它有 59 個開放議題,而測試前大約三週有一次推送;單看這些資訊,並不能證明維護品質或未來相容性。
記憶體,以及損壞 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 份各自只壞一個地方的文件——未關閉標籤、巢狀錯誤的 inline 元素、帶空白卻沒加引號的屬性、亂入的結尾標籤、完全沒有 <html>、重複屬性、在標籤中途截斷的文件、錯誤 entity、未關閉的 <script>、說謊的 charset 宣告、包含標記的註解,以及 600 層巢狀結構——再加上 兩份同尺寸的正常對照組,因為「它什麼都沒回傳」只有在同尺寸乾淨文件上也同樣沉默時,才真的能說明它對壞格式有反應。
pyquery 在 14 份測試中有 0 份報錯,1 份回傳空結果,並在這些破損測例中恢復了 10/22 個 sentinel(malformed-results.json)。其中有一份測例不計入這個統計:依 HTML5 規範,未關閉 <script> 後面的內容本來就都算 script 內容,所以在那裡少掉是正確行為,若把內容找回來反而才是偏差。若沒有把直接替代方案的 sentinel 結果放在一起看,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 則整理了自架工具。如果解析後的輸出要交給模型,請看 convertir HTML a Markdown in Python,那裡最容易丟失的是忠實度。
那麼,你該用 PyQuery 嗎?
如果你想在 Python 裡用 jQuery 風格語法,而且這次測到的選擇和讀取路徑剛好符合你的工作負載,那答案是肯定的。
這份 benchmark 在保留內容雜湊一致性的前提下,沒有發現相對 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,而不是直接引用已發表的數字? 因為已發表的 benchmark 記錄了機器和 Python 版本,卻沒有記錄函式庫版本。它的 lxml 結果可能來自和今天 PyQuery 所包裝的不同 lxml;如果版本有差,就會被誤看成包裝層成本,而事實上不是。把兩者放在同一個行程裡、都使用 lxml 6.1.1,就能消除這種歧義。
它和 cheerio 相比如何? API 思路相同,但生態不同。這裡測到的兩個選擇器在兩邊都可運作,而且五個尺寸的內容雜湊都一致;但這不代表所有選擇器都完全相容。cheerio 的存檔時間較慢,不過跨執行環境和歷史跑法的差異,讓我們不能把它解讀成純函式庫倍率。
這裡沒有測什麼? 記憶體測了匯入-only、226 KB 文件和 10 MB 文件時的峰值 RSS。錯誤 HTML 則用 12 份破損文件加 2 份對照組測過了。仍未測的包括 PyQuery 的修改和遍歷效能、重複查詢快取、URL 抓取、並行處理,以及具代表性的真實網站工作負載。1 KB 的計時結果仍不列名次。


