lxml 評測:至今仍能壓制所有 Python 解析器的 XPath 引擎

最後更新於 August 12, 2026
lxml 評測:至今仍能壓制所有 Python 解析器的 XPath 引擎
AI 摘要
這篇 lxml 評測把這個函式庫定位為建立在 libxml2 與 libxslt 上、歷久不衰的 Python 綁定,並點出它至今仍少有新一代解析器能完全取代的核心優勢:真正的 XPath 引擎。文章測試了 XPath 覆蓋率、parser 嚴格模式、串流記憶體行為、CSS 與 XPath 的表達能力差異,以及 libxml2 的深度限制。結果顯示,lxml 在需要軸、條件、函式、串流或穩定回復模式的 XML 與 HTML 工作負載中,依舊兼具速度、低記憶體占用與高能力。評測也說明了出於安全考量的樹深度預設值,以及 huge_tree 何時能改變這個邊界。

每隔幾個月,就會冒出一款更快的 HTML 解析器,基準測試也會被拿出來反覆討論,然後有人就宣告老派工具已經過時。可真到了你要挑出每一個「包含某個字詞」的段落,或是抓出已匹配節點的父節點時,你又會想起為什麼 lxml 仍然開在另一個分頁裡。

lxml 是一個存在了 20 年的 libxml2 綁定。它不新鮮,也不花俏。但在一件特定工作上——任何需要真正 XPath 的情境——主流 Python 生態裡還真沒有別家能正面對打。這篇實測會帶你看它到底能做什麼、在哪些地方默默勝出,以及哪幾個預設值如果你不知道,真的會踩雷。

一句話看懂 lxml:它到底是什麼

lxml 是 Python 對 C 函式庫 libxml2 與 libxslt 的綁定。它是解析器,也是序列化器,但它不是爬蟲,也不是瀏覽器——它把標記內容轉成你可以查詢與編輯的樹狀結構,再把這棵樹轉回 bytes。它提供與 ElementTree 相容的 API、完整的 XPath 1.0 引擎、XSLT 1.0、以及 schema 驗證,並由 Stefan Behnel 維護,官方標語則是 「Python 語言中功能最豐富、也最好用的 XML 與 HTML 處理函式庫」

以下是截至 2026-07-14 這次從 GitHub 與 PyPI 抓取的快照:

欄位數值
Repolxml/lxml
Stars3,043
Forks620
Open issues16
LicenseBSD-3-Clause
Created2011-02-11
Last push2026-07-02
PyPI stable6.1.1 (2026-05-18)
Bundled enginelibxml2 2.14.6 + libxslt 1.1.43

先說清楚一件事,免得有人覺得我在吹捧:這篇評測裡沒有什麼秘密武器。lxml 年紀夠大了,這裡提到的每一個行為,都能在 lxml 文件、libxml2 變更紀錄,或某個討論串裡找到依據。我沒有發現任何獨家、未公開的技巧,也不會憑空編一個出來。這篇文章的價值在於:它把這些能力系統化、量化,並以 lxml 為主體整理,而不是因為內容有多新。

測試環境(以及為什麼時間數據是借來的)

這篇評測使用兩類資料,來源也不同,所以我先講清楚哪一類是哪一類。

能力測試——像是 XPath 行為、兩種 parser API、命名空間、編碼、節點生命週期——我是在一台機器上重新跑的:macOS arm64、Python 3.14.2、lxml 6.1.1、libxml2 2.14.6。那些 artifacts/raw/*.json 檔案裡的每個數字,都是腳本執行後算出來的,不是手寫上去的。能力測試是可重現的布林值與列舉值,所以單次執行就足夠穩定——機器負載不會改變 //a/@href 會不會回傳屬性字串。

時間與記憶體占用數據不是這次重新量的。 這些數字直接沿用前一份 selectolax 基準包的結果——同一台機器、同一個虛擬環境、同一版 lxml 與 libxml2、基準時間截至 2026-07-13——這次我沒有重跑。這是刻意的。若在跑一批能力腳本的同時重測時間基準,CPU 競爭會污染沿用的數值,而且也是重複勞動:在那份基準包裡,lxml 已經被當成完整量測過的對照組了。沿用同一組 benchmark,才能保持前後一致,不會引入第二種細微不同的量測方式。所以當你看到下面的毫秒數,請把它理解成「同一套測試平台、截至 2026-07-13 的數據」,而不是「我今天又重新測了一次」。

每個結論都會附上信心標記:single-observation 表示可重現的能力測試,triple-run 表示沿用的時間分佈,hypothesis 則是我提出但沒有獨立拆解驗證的機制。

XPath:selectolax 和 BeautifulSoup 做不到的那件事

這是全文重點,所以我先講這個。

lxml XPath coverage moat with axes predicates and functions

我把 lxml 的 xpath() 放進一個事先註冊的 37 項矩陣裡測試——每個案例的預期結果都在測試前先寫進原始碼,因此我不可能不小心放水。內容包括十種 axes、九種 predicate 寫法、十種內建函式、三種純量回傳型別,以及五個刻意使用 XPath 2.0 專屬語法的陷阱案例,lxml 的 1.0 引擎理應拒絕它們。

類別覆蓋範圍結果
Axeschild / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding10/10 pass
Predicates[1] / last() / position()<n / 屬性相等 / 屬性存在 / and / or / 巢狀 [.//a] / not()9/9 pass
Functionstext() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string()10/10 pass
Return typesboolean / number scalars3/3 pass
Trap casesmatches() / sequences / if-then-else / except / syntax error5/5 correctly rejected

結果是 37/37,而真正重要的是陷阱欄。matches()、序列表達式、if/then/elseexcept 都是 XPath 2.0 語法,libxml2 的 1.0 引擎不會半套支援,而是直接丟出 XPathEvalError 並拒絕執行,不會悄悄回傳錯誤的節點集合。換句話說,這是「真的去刁難過之後」的滿分,不是靠簡單題灌出來的滿分。這裡的每一個行為,都與 lxml XPath 文件的描述完全一致,這正是重點。

我也承認一件事:測試框架有一開始判錯的地方,但這反而是你可以信任的那種 37/37。我對 //div[.//a[@href]] 的第一版預期以為會命中兩個,實際跑出來只有一個。我當下還以為是 lxml 出錯了大概三十秒,後來回頭檢查 fixture,才發現第二個元素其實是 <footer>,不是 <div>——錯的是我的預期,不是引擎。我把預期修正了,也在原始碼註解裡保留了這個失誤。這才是正確的甩鍋順序:先懷疑自己的測試,再懷疑那個用了 20 年的 C 函式庫。

XPath 對 CSS:有些事你就是不能用 CSS 表達

「XPath 比 CSS 強」這句話太抽象,所以我把差距量化了。lxml 同時提供 .xpath().cssselect()(後者底層會把 CSS 轉成 XPath)。我挑了十個選取目標,檢查 CSS 到底能不能表達。

XPath expresses seven of ten tasks CSS cannot express

目標XPathCSS(cssselect)
依文字內容過濾(contains(text(),"bargain")可以沒有文字判斷式
從子節點選父節點(//b/parent::p可以沒有 parent selector
回傳屬性值(//a/@href可以只能回傳元素
回傳文字節點(//p/text()可以沒有 text nodes
Ancestor axis(//td/ancestor::div可以沒有向上走訪
依子元素數量過濾父節點(//ul[count(li)=4]可以沒有 count 判斷式
依文字長度過濾(string-length(text())>5可以沒有長度判斷式
nth-child / last-child / adjacent sibling可以可以(3 個基本能力)

十個目標裡,有七個 CSS 完全沒有對應寫法。依文字內容過濾、往上抓父節點或祖先節點、把屬性值或純文字節點直接當結果抓出來、依數量做判斷——CSS 通通說不出來。只有三個(nth-childlast-child、adjacent sibling)兩邊都能做。這就是「我到底用了 lxml 會得到什麼」的量化答案。selectolax 是純 CSS,根本沒有 xpath() 方法,所以那七種查詢在那裡不是得改寫成多段 Python 迴圈,就是根本做不到。如果你的抓取邏輯會用到這些,那答案其實已經很明確了。

(是的,這個地方測試框架又抓到我一次:我本來以為 string-length(text())>5 會是空集合,但實際上有兩個六個字元的字串符合。修正的是預期,不是工具。)

三段式嚴格度:etree、recover、lxml.html

XPath 是你選 lxml 的理由,而三段式的嚴格度控制,則是你會想繼續留著它的原因。

lxml strictness gears: etree, recover, and lxml.html

大多數 parser 對壞掉的輸入只有一種反應。lxml 給你三種,而且它們都夠可預測,所以我把六類不完整或錯誤的標記各自丟進去,並在測試前先註冊每一種情境應該怎麼表現。

錯誤輸入lxml.etree(嚴格)etree + recover=Truelxml.html(寬鬆)
未關閉標籤 <root><a>x</root>raisesrecoversaccepts
標籤錯位 <b><i></b></i>raisesrecoversaccepts
未定義實體 &nbsp;raisesrecoversaccepts
單獨的 &Tom & Jerryraisesrecoversaccepts
多個根節點 <a>1</a><b>2</b>raisesrecoversaccepts
格式正確的 XMLacceptsaccepts (0 errors)accepts
布林屬性 <input disabled>raisesrecoversaccepts

七種情況全部與預先註冊的預期一致。lxml.etree 會對這六類錯誤輸入全部丟出 XMLSyntaxError。在同一個 parser 上加上 recover=True,它就會把錯誤吞下去並重建出可用的樹——而且這裡最值得注意的是——parser.error_log 會把所有被吞掉的錯誤逐條列出。lxml.html 則是全部照單全收,完全不抱怨。

決定「raises / recovers / accepts」的分類器,本身是根據執行時 error_log 的長度,而不是寫死的,所以即使是 recover=True 但遇到的是乾淨文件,也會被正確標成「accepts」(空 log),而不是「recovers」。我第一版的分類器只要看到 recover=True 就一律標成 recovers,因此把乾淨輸入標錯了;改成讀實際的 error_log 之後就正常了。

這在實務上代表什麼:如果來源必須嚴格驗證、壞資料要明顯失敗,就用 lxml.etree。如果是髒到不行、但你只想把真實世界的 HTML 盡量讀進來,就用 lxml.html。而大多數工具做不到的中間狀態——「可以寬鬆一點,但我要知道到底壞在哪,方便記錄」——就用 recover=True 再讀 error log。selectolax 只有寬鬆模式,沒有嚴格模式,也沒有 error log。

iterparse:selectolax 完全沒有的串流模式

這裡講的是一種能力,不是速度旋鈕。selectolax 只能一次吃進完整字串——沒有增量式介面。lxml 的 iterparse 會在元素關閉時逐個吐出,而配上經典的 fast_iter 模式(邊走邊 elem.clear(),再刪掉前面的兄弟節點),不管文件多大,記憶體都能維持平坦。

lxml iterparse streams 300K records with about 1-2 MB RSS

我直接量了記憶體特性——以 ru_maxrss 看峰值 RSS、每個 subject 都放在獨立新進程裡跑,資料是 300,000 個 <record> 元素,總大小約 26.7 MB(26,744,801 bytes)。

模式峰值 RSS 增量備註
iterparse + clear(fast_iter)約 1-2 MB邊讀邊釋放;不隨數量增加而上升
iterparse 不清除約 386 MB保留參照;跟整份載入一樣重
etree.parse(完整載入,基準)約 386 MB已知會很重;證明量測器真的有抓到差距

在這個受控模式下,峰值 RSS 增量只落在約 1-2 MB,相對於完整載入的約 386 MB,差距大概是 0.3-0.4%;而且第一個 record 事件甚至在檔案還沒完全讀完前就發生了,所以它真的是增量式,而不是假串流。最有啟發性的,是中間那一列。把同一個 iterparse 迴圈跑一遍,但拿掉 clear(),記憶體又會回到約 386 MB,因為你把所有東西都引用住了。真正的勝利點在 clear(),不是 iterparse 本身。完整載入那列明顯高於受控模式,也證明 RSS 計量真的看得到量級差,而不是瞎量。(這次的記憶體測試是我在本包裡實際跑的——它是占用量量測,和前面借來的時間數據不同。)

放到真實世界來說:如果你有一份好幾 GB、根本塞不進 RAM 的 XML 匯出檔,selectolax 完全沒有路可走。你只能用 lxml 的串流 parser,或者換語言。

命名空間:RSS、SVG,以及預設命名空間的陷阱

我測了 12 組命名空間情境,包含跨三個命名空間的 RSS、帶預設命名空間與 xlink 的 SVG,以及預設命名空間 XML。12 組全部通過。

lxml 可以從 RSS feed 中精準抓出 //dc:creator/text(),結果就是 ["Alice", "Bob"];也能在同一份文件裡跨三個不同命名空間解析 //atom:link/@href//content:encoded;在 SVG 的第二個命名空間中處理 //s:rect//s:use/@xlink:href;還能用 QName 拆解 Clark notation 的 {uri}local 名稱,並透過 nsmap 做 introspection。這些都是有維護、也有文件記載的行為,而且這整個維度是 selectolax 完全碰不到的,因為 selectolax 只支援 HTML5,並不處理任意 XML 命名空間。

不過有一個有文件寫明、但很值得牢記的陷阱。XPath 根本沒有「預設命名空間」這個概念。你把 //book 丟去查一份宣告了 xmlns="urn:..." 的文件,結果會是零筆命中——在 XPath 裡,空前綴是未定義的,這點 lxml 文件講得很清楚。你必須替它綁一個人工前綴(例如 //c:book,再搭配 namespaces={"c": "urn:..."},這樣就會找到全部三筆),或者退回 //*[local-name()='book'](也會是三筆)。這不是 bug,而是 XPath 規格被忠實實作。只是每個人都會被它嚇到一次而已。

真實髒頁面:11 份實際抓取內容的還原度

合成測試很乾淨,真實網頁不是。這次我重用 selectolax 基準包裡的 11 份真實頁面截圖 fixture(截至 2026-07-10,唯讀),把 lxml 當作主體丟進去測。

Fixture大小Linkslibxml2 recovered errorsStrict XML
news_bbc.html398 KB2554ok
docs_mdn_array.html243 KB5080raised
wiki_scraping.html227 KB4600raised
gov_whitehouse.html289 KB1540raised
oldstyle_craigslist.html561 KB3510raised
forum_reddit.html129 KB3180raised
docs_python.html80 KB3412raised
ecommerce_books.html51 KB940raised
news_hackernews.html35 KB2290raised
ecommerce_webscraper_allinone.html16 KB350raised
spa_quotes_js.html6 KB50raised

這 11 份全部都能用 lxml.html 解析,而且 link、heading、image 的數量,和 selectolax 基準包裡沿用的 lxml 計數全部一致——交叉比對為 true。這樣的相符結果,才讓我相信這次沿用是確實 apples-to-apples,而不是兩組不同的量測硬貼同一個標籤。

副結論也很明顯:嚴格 XML parser 在 11 份裡有 10 份都丟錯。真實世界的網頁幾乎都不是 well-formed XML,這也正是 libxml2 的 HTML recovery 模式存在的原因:就是要吃掉這些亂七八糟的頁面。唯一的例外是 BBC News,它由 Next.js 渲染,而且格式乾淨到甚至可以通過嚴格 XML 解析。不是每個被叫做「HTML」的頁面,都一定需要 recovery 路線。

這裡還有一個很容易搞混的計數細節。對 docs_python.html 來說,//a[@href](屬性存在)計到 343,而 selectolax 基準包裡的 if n.get("href")(truthy 值)只計到 341。多出來的兩筆是空的 href="" 連結。這是計數規則不同——「屬性存在」對上「屬性非空」——不是 lxml 行為有差異,只要 predicate 對齊,數字就會一致。抓資料時這件事很重要:空 href 要不要算,取決於你的過濾條件,不是 parser 幫你決定。

那個看起來像 bug、其實不是的深度限制

selectolax 基準包曾記錄到 lxml 在 1,000 與 5,000 層巢狀 <div> 標記時,會漏掉最深層內容,並把它描述成「lxml 靜默遺失最深層內容」。我想知道機制,所以把預設 parser 與 huge_tree=True 版本都跑了一遍。

lxml default depth guard around 253 levels and huge_tree to 2045

要求深度預設 parser 可到達深度huge_tree=True 可到達深度
300253(後面會丟失)299(已恢復)
1000253(後面會丟失)999(已恢復)
5000253(後面會丟失)2045(仍然丟失)

預設 parser 大約會在 253 層處截斷,並靜默丟掉更深的內容。這不是 bug,而是 libxml2 的 DoS 防護:它大約在 256 層巢狀深度就會設下上限,避免惡意文件把 stack 撐爆,這點在 lxml 的 launchpad 討論串 也有提到 XML_PARSE_HUGE。把 huge_tree=True 打開後,300 層和 1,000 層都能完整回來。不過到 5,000 層時,即使開了 huge_tree,也只到 2,045 層就停了——上面還有第二道更硬的 libxml2 recursion 上限,而 huge_tree 不會突破那條線。

所以實際可做的事很明確:當你要解析來自可信來源、而且可能很深的標記時,請用 lxml.html.HTMLParser(huge_tree=True)。這份測試在沿用先前觀察的基礎上,補上的則是機制(安全上限,不是資料損毀)、修正方法(huge_tree),以及一條修正不了的第二道上限。

可讀可寫 DOM、序列化、編碼

lxml 是完整的讀寫樹,不是只能讀的抽取器;我也逐項驗證了它的編輯能力。八項 DOM 操作全部通過:SubElementinsertremovereplacestrip_tags(移除標籤但保留文字)、strip_elements(標籤與其文字都刪掉)、drop_treelxml.html 的獨有功能),以及容易讓新手卡住的 text / tail 雙槽模型——在 <p>head<b>bold</b>tail</p> 裡,p.text"head"b.text"bold",而 b.tail"tail"

序列化則是五項全通:XML 與 HTML 模式下的 tostring(HTML 會正確保留 void elements 不自我關閉)、pretty_print、C14N canonicalization(method="c14n",也是 lxml 的獨有功能),以及乾淨的 round-trip。

編碼處理則是 lxml 默默拉開差距的地方。丟進去非 UTF-8 bytes——像 "<p>café éè</p>".encode("latin-1") 經由 lxml.html.fromstring——它可以完整還原出 café éè,沒有 U+FFFD 替代字元,也沒有遺失 bytes。這直接重現了它在 selectolax 基準包裡作為「乾淨參考值」的角色:同樣的輸入在另外兩個引擎上都出現了破壞(Lexbor 產生替代字元,Modest 直接丟字節),只有 lxml 表現穩定。libxml2 背後的字元集偵測在這方面就是比較可靠。

反過來說,它對你怎麼宣告編碼也很嚴格。XML 宣告裡如果寫 encoding="latin-1",會直接丟 XMLSyntaxError: Unsupported encoding: latin-1;但如果寫 IANA 標準名稱 encoding="ISO-8859-1",就能正常解析並回傳 café。libxml2 只接受標準化的編碼名稱,不接受別名——這個細節在很早以前的 launchpad #613302 就有文件紀錄。你不曉得時會很煩,知道之後其實很簡單。

最後是節點生命週期。我在獨立子進程裡跑了三種 stale-handle 情境(如果真的 hard crash,exit code 會是非零):在 tree 被 garbage collection 後還持有節點、在 drop_tree() 後讀取 handle、以及在 remove() 後繼續使用節點。三種都沒有 segfault——lxml 會保留節點對其 tree 的引用,避免 use-after-free。這項測試上,lxml 的表現也一樣乾淨,和 selectolax 相同。

速度與記憶體(借來的,而且坦白承認)

這一節的所有內容,都沿用自 selectolax 基準包,截至 2026-07-13。這份包裡沒有產生任何自己的時間數據,我寧可再說一次,也不要讓你以為我有重測過什麼。

維度lxml 數值解讀
純解析 p50(10 MB)77.9 ms比 selectolax-Lexbor 約快 33-34%
完整解析 + 抽取 p50(1 MB / 10 MB)14.18 ms / 172.9 ms小檔時與 Lexbor 大致打平
100k 節點 CSS throughput3,002,646 nodes/s三個 C 引擎中的最快級距
10 MB RSS delta128.9 MB六款 parser 裡最省記憶體,比 BeautifulSoup 約省 1.7 倍
Import cold start14.1 ms比 parsel 類型的 imports 約快 2.3 倍

純解析與吞吐量的數據很強,而在這六款 parser 裡,lxml 也是記憶體最節省的一個。不過執行緒面向要加註解。沿用的資料顯示,4 執行緒的 wall-clock 加速只有 1.21x,而且被標記為 inconclusive——但那是在共用預設 parser 的情況下。 lxml FAQ 明確指出:只有每個執行緒各自使用自己的 parser(或是複製過的 default parser)時,解析過程中 GIL 才會被釋放;共用同一個 parser 反而會序列化存取。我已經在 API 層面確認了正確做法的存在(XMLParser.copy()get/set_default_parser 都有,XPathEvaluator 也帶內部鎖),但我沒有測每個執行緒各自 parser 的加速效果——那會是新的時間測量,而這份包不產生那種數據。所以請把 1.21x 理解成「在天真的共用路徑下的結果」,不要把它當成 lxml 的執行緒上限。

另外也要提醒一個共同前提:這些都是單一平台的數字,macOS arm64。至於「lxml 純解析比 Lexbor 還快」這個結果,和一般認知裡 Lexbor 背後的 parser 速度最快的印象其實有衝突,所以它確實需要在 Linux x86_64 上再確認一次,才適合被當成定論。

授權:那種無聊但很重要的勝利

lxml 採用 BSD-3-Clause 授權,而它所捆綁的 C 函式庫——libxml2 與 libxslt——也都是 MIT。這是一條完全寬鬆、沒有任何 copyleft 的授權鏈,這點在你要重新分發時非常重要。相較之下,selectolax 的 wheel 捆綁的是 LGPL-2.1 的 Modest 和 Apache-2.0 的 Lexbor,所以在封閉式產品裡部署時,lxml 的法律故事更單純。

實務安裝上也有優勢:lxml 會提供預建 wheels,裡面已經靜態連結 libxml2 與 libxslt,所以一般情況下你 pip install lxml 不需要系統內另外裝 libxml2,也不需要本機有編譯器——這和從原始碼自己編譯是完全不同的體驗。

lxml 適合放在哪一層——以及 AI 抽取層何時接手

這裡要把界線講清楚,因為很容易混淆分類。lxml 是解析函式庫。它把樹狀結構交給你,也給你非常強的查詢引擎;但圍繞那棵樹的事情還是你的責任:抓網頁、渲染 JavaScript、處理反爬機制、撰寫與維護 XPath,還有把結果整理好。這一層跟託管式抽取服務不是同一層,兩者與其說是競爭者,不如說是鄰居。

如果你是開發者,而且不想自己維護「抓取—渲染—選取—維護」整套流程,那上面那層就是像 Thunderbit 這種工具發揮的地方——而對這個讀者群來說,真正重要的是它的 API、MCP server 和 CLI,不是瀏覽器擴充功能。 Thunderbit Open API 提供 POST /distill,可把頁面轉成乾淨 Markdown;也提供 POST /extract,可依 JSON Schema 擷取結構化資料,並支援 renderMode 切換與批次任務。相同引擎也可作為 MCP server 提供給 agents 和 coding assistants 使用(thunderbit_suggest_fieldsthunderbit_distillthunderbit_extract),或者直接用 CLI,透過 npx @thunderbit/thunderbit-cli 在終端機跑。它預設就處理 JS 渲染、反爬與 CAPTCHA,並回傳與 schema 匹配的 JSON——這是解析之上的層級,而不是取代解析。

試用 Thunderbit 進行網頁資料擷取

理解方式很簡單:如果你擁有整條 pipeline,而且想對一棵你看得懂的樹做精準 XPath 控制,就用 lxml。如果你不想維護 selector 和渲染流程,就用 AI 抽取 API。很多真實系統其實兩者都會用——lxml 處理你可控的結構化 feed,抽取服務處理你不想碰的髒亂長尾頁面。

這份評測沒測到的地方

這是一篇暫定版評測,不是最終成績單,所以也要交代它沒涵蓋什麼。

所有時間與記憶體數據都沿用自單一平台(macOS arm64、Python 3.14)的結果,也承襲了那份基準包的限制——「lxml 純解析比較快」這個結果與常見共識相反,需要在 Linux x86_64 上再驗證。每個執行緒各自 parser 的 threading 加速沒測到(那需要新的時間測試)。我量了 30 萬筆 record 的 iterparse 記憶體,但沒有量 GB 級真實 XML,也沒有測 HTML 與 XML 的 iterparse 對照,更沒有跑長時間 soak 測試。lxml 的 XSLT 1.0、RelaxNG / XMLSchema / DTD 驗證、以及 EXSLT extensions 這次完全沒碰——那是一大片能力範圍,但超出了這次 parsing 與 selection 的核心。雖然我觀察到了 2,045 層的第二道深度上限,但沒有精準釘出 libxml2 的遞迴常數。這次只測了穩定版 6.1.1,沒有測 7.0.0 alpha。Windows、source build,以及 free-threaded 3.14t build 也都沒測。至於 XPath 本身,我有涵蓋內建函式,但沒有測 XPath variables、自訂 Python extension functions,或是預編譯 etree.XPath 物件的重用。

結論

lxml 不是最新、最快的那一個,而這正是它值得推薦的地方。它是個存在超過二十年的 libxml2 綁定,擁有主流 Python 裡沒人能完全匹敵的 XPath 1.0 引擎、三段式且可預測的解析嚴格度、中間還帶 error log、可處理超大文件的真正串流 parser、正確的多命名空間與編碼處理,以及完全寬鬆的授權。那幾個尖銳邊角——約 253 層的深度上限,以及共用 parser 的執行緒數據——都是有文件、可配置,而且現在也被說明清楚了。

如果你掌控自己的爬取 pipeline,而且很依賴 XPath,lxml 到現在仍然是你應該先拿起來的 parser。如果你不想自己維護 selector 和渲染,像 Thunderbit API、MCP 與 CLI 這樣的 AI 抽取層就是用來做這件事的——兩者是分工,不是競賽。無論選哪一邊,都請把這些數字視為暫定版,並在你自己的平台上重新確認時間數據後,再寫進設計文件。

試用 Thunderbit 進行網頁資料擷取 Get Started Free

常見問題

lxml 是網頁爬蟲嗎? 不是。lxml 是解析與序列化函式庫——也就是 Python 對 libxml2/libxslt 的綁定,負責把標記內容轉成可編輯、可查詢的樹狀結構。它不會抓頁面、不會渲染 JavaScript,也不會處理反爬防護;這些要由你自己提供 request 層(例如 requestshttpx、headless browser,或爬取服務),再把 bytes 丟給 lxml。

我什麼時候該用 lxml,而不是 BeautifulSoup 或 selectolax? 當你需要 XPath 時,就用 lxml。BeautifulSoup 雖然可以把 lxml 當後端 parser,但它本身沒有原生 XPath;selectolax 則是純 CSS,而且在它擅長的小範圍裡速度很快。如果你的選取邏輯需要依文字內容過濾、向上找父節點或祖先節點、抽取屬性或文字節點,或是用數量做判斷,那只有 lxml 的 XPath 引擎能直接表達這些需求。

為什麼 lxml 會靜默丟掉很深層的內容? 它的預設 parser 會把巢狀深度限制在約 253 層——這是 libxml2 為了防止惡意文件造成 DoS 的防護,不是 bug。把 huge_tree=True 打開(例如 lxml.html.HTMLParser(huge_tree=True))後,300 層與 1,000 層都能完整恢復。只是要注意,還有一條大約 2,045 層的第二道硬性遞迴上限,huge_tree 不會把它關掉。

lxml 會在多執行緒解析時釋放 GIL 嗎? 只有在條件正確時才會。lxml FAQ 說明:當每個執行緒都使用自己的 parser,或是使用複製過的預設 parser 時,解析過程中才會釋放 GIL;如果多個執行緒共用同一個 parser,則會序列化存取。這次沿用的 4 執行緒 1.21x 加速結果,反映的是天真的共用 parser 路徑,不是每執行緒 parser 的上限;後者這次沒有量。

lxml 到 2026 年還有人維護嗎? 有。穩定版 6.1.1 已在 2026-05-18 釋出,repo 最後一次 push 是 2026-07-02,而且 7.0.0 alpha 也正在進行中。GitHub 大約 3,000 顆 stars,加上底層的 libxml2 仍在持續維護,這代表它依然是現役、受支援良好的函式庫,而不是遺留專案。

Ke
Ke
Thunderbit 技術長|資深資料科學家與機器學習專家 Ke Shen 在機器學習與資料科學領域擁有近十年經驗,畢業於哥倫比亞大學,曾任 Walmart Labs 資深資料科學家。他精通 Python、R、Java 與統計學,且具備深受同儕認可的深厚專業,分享如何將複雜的 AI 演算法從理論落實到可投入生產的架構的實戰見解。
Topics
網頁爬蟲工具AI 網頁爬蟲
目錄
Thunderbit · AI 網頁資料代理

1 次點擊 內擷取任何頁面的資料

深受 250,000+ 用戶信賴
提供免費方案
從網頁到試算表
描述你需要的內容——Thunderbit 的 AI 代理會幫你爬取,並匯出到 Excel、Google Sheets、Airtable 或 Notion。免費即可開始。
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week