每一篇寫著「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 選擇器查詢內容。它不是爬蟲,不是瀏覽器,也不是那種按一下按鈕就能「抓資料」的工具。它是你在已經拿到 HTML 之後,把原始內容交給它處理的解析層。維護者自己的簡短介紹是:「一個使用 Cython 撰寫、採用 Modest 與 Lexbor 引擎的快速 HTML5 解析器,支援 CSS 選擇器。」
它有兩個後端,而且這個差異比文件寫得更重要:
LexborHTMLParser(Lexbor 引擎)——README 在 2024 年之後推薦使用的版本。HTMLParser(Modest 引擎)——原始版本;同一份 README 也明說它底下的 C 函式庫「已不再維護」。
在談速度之前,還有幾個值得先知道的事。根據 repo 截圖(截於 2026-07-10),selectolax 目前有 1,653 顆星,最新版本是 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 自己的基準測試對得上,README 暗示它比 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 在解析步驟上的優勢最後被稀釋掉,總時間就拉平了。
這是整篇評測中最容易被挑戰的結論,所以我想先把原因講清楚。它和常見認知相反,而我找到唯一一篇專門把純解析拆出來比較的公開基準 aows.jpt.sh 則得到相反結果:selectolax 大約快 4 倍。因此我做了隔離:這個結果只來自單一平台(macOS arm64、Python 3.14、預建 cp314 wheels——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 都屬於較省記憶體的那一級;若只看 RSS,lxml 是最省的。

我前一次量出來曾寫成「約 3 倍」,那個數字之所以錯,反而很有教育意義:我當時開著 tracemalloc,tracemalloc 針對每次配置的記錄開銷,會把高配置量解析器的表面 RSS 大致拉高一倍。所以給任何想測解析器記憶體的人一個重點:**要用關掉 profiler 的 RSS 來排名。**如果用 tracemalloc 的 peak 來排,會讓 C 後端解析器的排序失真——我當時甚至因此讓 selectolax-Lexbor 看起來比 Modest 還重,但實際 RSS 其實差不多。BeautifulSoup 的確是這裡最吃記憶體的,只是沒有到那個被污染儀器誤導出的 3 倍差距。
冷啟動也不大,但確實存在:selectolax 的 import 大約 14 ms,和 lxml 差不多,比 bs4 或 parsel 快約 2.3 倍。如果你在做 CLI 工具或 serverless 函式,而 import 時間會算進每次執行,這個差距還是值得注意。
CSS 選擇器支援:很強,但還是有幾個真洞
CSS 支援我做了 41 個案例的矩陣測試,每個選擇器都對照一組已知正解,另外還加了一組專門用來破壞 Lexbor 引擎的故障測試。每個案例都在獨立子程序中執行,這很必要——因為其中有一個會直接讓整個 interpreter 當掉。結果如下:

| 引擎 | 通過 | 錯誤 | 不支援 | PROCESS_ABORT |
|---|---|---|---|---|
| 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),它們會直接被當作 parse error 拒絕。除此之外,它幾乎全都能處理,包括 :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 tracker 中;那裡記錄的是 :has() 的限制會直接報錯。Lexbor 也能正確處理大小寫不敏感屬性旗標 [data-role="LEAD" i],這是 cssselect 直接拒絕的。
不過,真正會影響遷移決策的還有兩個缺口。selectolax 完全不支援 XPath——兩個後端都沒有 xpath()——而且也沒有 ::text / ::attr() 這些 pseudo-elements,因為那是 parsel/Scrapy 的擴充,不是真正的 CSS。如果你現有的爬蟲大量依賴 XPath,那會是你碰到的最大牆:你不是在換函式庫,而是在重寫選擇器。反過來說,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 個元素。
最明顯的差異出現在深層巢狀結構。當我把 <div> 巢到 1,000 層和 5,000 層時,**lxml 會悄悄捨棄最深處的內容,而 selectolax 會保留。**libxml2 的解析深度上限大約是 256 層,超過後會把樹截斷,而且不會報錯,所以最深的文字根本碰不到。兩個 selectolax 後端都會保留完整樹。這和下一節要談的 <template> 問題正好相反:那裡是 Lexbor 丟掉其他人保留的內容;這裡則是 lxml 丟掉 selectolax 保留的內容。
不過不是每一格都贏。Modest 後端在遇到 :dir() 時,會直接讓整個 Python interpreter 因 SIGABRT 而終止——不是你能捕捉的例外,而是硬性把程序殺掉。對還在用舊後端的人來說,這是很實際的穩定性風險,而這種問題往往要等到凌晨三點把正式環境任務打掛時才會浮現。
上線前一定要知道的兩個靜默資料遺失陷阱
這兩個都不是新發現——上游文件其實都有提——但它們都會安靜地讓你少資料,而且 README 裡講得不夠醒目。
Lexbor 會漏掉 <template> 裡的 <a>
我在實際的 MDN 頁面測試時,**selectolax-Lexbor 只找到 497 個連結,而 lxml、兩種 BeautifulSoup 後端,甚至 selectolax 自己的 Modest 後端都找到 508 個。**少掉的那 11 個,是放在 <template> 裡的語言切換器和討論連結(該頁面使用 Lit Web Components)。

根本原因是合理的:依 HTML5 規範,<template> 內容會被解析到一個獨立、非活動的 fragment,而不是正常 DOM;Lexbor 也確實嚴格遵守這點——tree.css("a") 不會往 template 裡找。lxml、兩種 BeautifulSoup 後端和 Modest 則會把 template 內容攤平到主樹裡,所以能找到那些連結。這是已知且有記錄的問題(selectolax#146,對應引擎根因在 lexbor#170),兩種解讀都站得住腳——Lexbor 可以說更符合規範。不過,開發者如果選的是 官方推薦 的後端,卻會在沒有任何錯誤提示的情況下默默漏資料。反過來也要說:其他解析器會把瀏覽器實際不渲染的 inert template 內容抓出來,反而可能給你使用者看不到的「幻影資料」。如果某個頁面真的卡在這裡,可靠的退路是改用 Modest 後端,或換別的函式庫。
非 UTF-8 位元組會讓 .text() 靜默損壞
如果你把不是有效 UTF-8 的 bytes 丟給 selectolax,解析本身會成功,但損壞會延後浮現,而且比直接當掉更糟。以 "<p>café éè</p>".encode("latin-1") 為例,Lexbor 的 .text() 會回傳替代字元,Modest 的 .text() 則會悄悄丟掉出問題的位元組;兩個引擎都只會在你碰 .html 時才丟出 UnicodeDecodeError。這個 binding 是在讀回時才用嚴格 UTF-8 解碼,不是在 parse 當下。這一點和 selectolax 已知 issue 提到的 encode/decode 嚴格性有關。
修正方式只有一句,而且應該變成肌肉記憶:在送進去之前先自己解碼——LexborHTMLParser(resp.content.decode("latin-1"))——這樣兩個引擎都會正確回傳 'café éè'。實務上,請永遠把 str 丟給 selectolax,不要直接丟非 UTF-8 的 raw bytes。README 並沒有把這件事講清楚。
生產環境維度(只有單次觀測,請把它們當方向參考)
下面這幾項我只量了一次,沒有重複三次,所以我把它們視為訊號,不當成定論。
最有意思的是執行緒擴展性。把 1 MB 頁面跑 48 次、分散到四個執行緒後,selectolax 的實際牆鐘時間加速約 3.5-3.9 倍——這很像是 C 解析時有釋放 GIL 的函式庫;相對地,BeautifulSoup(lxml) 在多執行緒下反而慢了好幾倍,這代表工作在 GIL 上被序列化了。lxml 則落在中間,結果不夠明確。對 Python 正在走向的 free-threading 時代來說,能在多執行緒中平行解析、而 BeautifulSoup 做不到,這確實是一個有價值但還只是暫時的優勢。這只是一個執行緒數、一個頁面大小,而且機制也只是推測,並不是我透過 instrument C 程式碼所證實的。
再看記憶體洩漏:在 1 MB、2,000 次 parse-extract-drop 循環中,這三個解析器都沒有出現線性上升的 RSS 曲線——都停在有限的工作集範圍內。我之所以信這個結果,是因為我先拿一個已知會漏記憶體的測試對象跑同樣的儀器,它如預期一路爬到 +198 MB,證明儀器看得到洩漏,只是在這些解析器上沒找到。即使把一個 node handle 在其所屬樹離開作用域後繼續保留,它也依然可用,沒有 segfault。這些都只是單次觀測,不是長時間浸泡測試。
selectolax 適合放在哪裡,又該在哪裡交棒
上面談的全部,其實都圍繞一件事:把你已經拿到的 HTML,快速轉成結構化資料。這正是 selectolax 很擅長的事。它刻意不做的,是幫你抓頁面、執行 JavaScript、輪換代理、解 CAPTCHA,或替你判斷 到底該抓哪些元素。那些都還是要你自己寫。selectolax 只負責解析層,而且它也不假裝自己比這更多。
這也正是「代管式抽取服務」會存在於解析器上層,而不是取代解析器的原因。如果你不想自己搭建並維護抓取、渲染、反爬和抽取這整條鏈,Thunderbit 提供了 API、MCP server 與 CLI——POST /distill 會把頁面轉成乾淨的 Markdown,而 POST /extract 會回傳符合 schema 的結構化 JSON,並且把 JS 渲染和反爬處理都包好。這是同一個問題的不同層次:當你已經有 HTML、而且想在自己的掌控下追求原始解析速度時,就會選 selectolax;如果你希望抓取與抽取都有人處理,只想拿回結構化資料,那就像 Thunderbit 的 API、MCP server 或 CLI 這樣的方案更適合。它不是替代關係,而是同一棵技術堆疊中的不同高度。
優缺點與適合誰用
selectolax 的優勢:
- 在真實世界的「解析 + 擷取」工作上,比 BeautifulSoup 快約 12-17 倍,而且在三個數量級的頁面大小下都穩定。
- 記憶體精簡(屬於 lxml 那一級,約比 BeautifulSoup 省 1.5-1.8 倍),import 約 14 ms。
- 對會讓 lxml 出問題的輸入更有韌性:空內容、空白內容、以及病態的深層巢狀。
- 支援現代 CSS,包括
:has()、:is()、:where()、大小寫不敏感屬性,以及 Lexbor 專屬的:lexbor-contains()。 - DOM 讀寫行為對
None友善:缺少元素時回傳None或[],不會直接丟錯,而且你真的可以修改並重新序列化樹。 - 持續維護中(v0.4.10,2026 年中),安裝也很簡單。
selectolax 的限制:
- 並沒有比 lxml 全面更快——完整任務上是平手,而在我的測試裡純解析還輸掉了。
- 沒有 XPath,也沒有
::text/::attr()——對依賴 XPath 的爬蟲來說,這是很大的遷移門檻。 - 兩個靜默資料遺失陷阱:Lexbor 的
<template>內容,以及透過.text()處理非 UTF-8 bytes。 - Modest 後端已屬舊版,而且遇到
:dir()會直接 SIGABRT。 - 這裡的所有數字都只來自單一平台(macOS arm64、Python 3.14),而且都是初步結果。
那你該不該用 selectolax?答案是:如果你想要接近 lxml 等級的解析速度,同時又希望 API 更順手、對 None 更友善,並且在空內容與格式錯誤輸入下有更好的表現——而且你願意待在只用 CSS 的世界裡——那就值得用。若你的程式碼庫是建立在 XPath 上,重寫成本是真實存在的,應該誠實估算。如果你追求的是「單一最快解析器」,這次測試的正確答案其實是: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 的 raw bytes——請先解碼成 str 再解析(例如 resp.content.decode("latin-1"))。如果在現代網站上少了連結或元素,它們可能藏在 <template> 標籤裡,而 Lexbor 後端不會往裡面深入;這種頁面請改用 Modest 或其他解析器。


