BeautifulSoup 幾乎是每個人在 Python 裡第一次抓網頁時都會先想到的函式庫,而它也確實是嚴肅 HTML 解析器中最慢的一個。這兩件事都是真的,而且都不是缺點。真正有意思的是,這個「慢」其實不是一種感覺,而是一個可以精準量化、也可以被拿來權衡成本的數字。
我把 bs4(也就是 beautifulsoup4,版本 4.15.0,於 2026 年 6 月釋出,採 MIT 授權)放進一套新的能力測試,加上同一組基準環境裡既有的計時資料,結果非常一致:你用大約一個數量級的速度代價,換來的是業界最友善的 API,以及最強的容錯能力。這筆交易值不值得,完全取決於你的工作負載,所以這篇評測會把兩邊都攤開來看。
BeautifulSoup 到底是什麼,又不是什麼
大多數教學都會跳過最重要的一點:BeautifulSoup 並不負責解析 HTML。它其實是一層包裝器。底層它會把文件交給三個真正的解析器之一——Python 內建的 html.parser、lxml,或 html5lib`——然後把它們產生的樹結構包成一套單一、非常好用的導覽與搜尋 API。bs4 的工作不是解析,而是讓你更舒服地在結果裡走動、查找、抽取。
它的作者自己稱它為「螢幕抓取函式庫(screen-scraping library)」,訴求一直都很一致:你只要把它丟到那些連瀏覽器都會皺眉的破 HTML 上,它還是能把你要的資料抓出來。這個名聲是名副其實的,只是有一個小但重要的例外,等等會講。
先把幾個基本事實釘牢:
| 欄位 | 值 |
|---|---|
| 套件 | beautifulsoup4(匯入為 bs4) |
| 測試版本 | 4.15.0(2026-06-07 上傳) |
| Python 要求 | >=3.7.0 |
| 授權 | MIT |
| 官方首頁 | crummy.com/software/BeautifulSoup |
| 原始碼 + Bug 追蹤 | Launchpad — 不是 GitHub |
| 維護狀態 | 持續活躍(2026 年 6 月的 4.15.0,過去一年有六次釋出) |
「不是 GitHub」這一點比表面看起來更重要。bs4 是一個有 20 年歷史、運作在 crummy.com 和 Launchpad 上的函式庫,所以你不能用一般 GitHub 星數來判斷它的健康程度。比較合理的做法是看釋出頻率;照這個標準,它運作得很好。
再補一個授權上的細節,方便需要向法務或合規團隊交代的人:包裝器本身是 MIT,但你實際安裝 bs4 後,依賴樹裡會多出什麼,取決於你選哪個後端。html.parser 是 Python 標準庫的一部分(PSF 授權,沒有額外依賴)。lxml 是 BSD 授權,但它底下還有 libxml2/libxslt——這是外部 C 依賴,你要嘛自己編譯,要嘛裝預編譯 wheel。html5lib 是純 Python、MIT 授權。如果你想要最乾淨的依賴組合,內建的 html.parser 最省事——而巧的是,它也是最有問題的那個後端。後面就會看到。
速度稅到底有多重
先把數字放前面,因為這就是標題,而且刻意不講清楚是不誠實的。在一個接近真實情境的 parse 再 extract 任務裡——先解析字串,再抓出所有 <h3 class="title"> 和所有 <a href>——BeautifulSoup 在這組比較裡是最慢的,而且差距不小。

這些計時數據沿用自 selectolax 的基準環境(同一台機器、同樣 3 次測試方法,截至 2026-07-13);這篇評測沒有重新跑任何自己的時間基準,以避免 CPU 互相干擾,也避免重複計算。以下是 p50 中位延遲,單位毫秒:
| 頁面大小 | bs4 (html.parser) | bs4 (lxml) | selectolax-Lexbor | lxml | bs4-hp 慢多少 | bs4-lxml 慢多少 |
|---|---|---|---|---|---|---|
| 1 KB | 0.323 | 0.281 | 0.027 | 0.036 | 12.0x | 10.5x |
| 10 KB | 2.049 | 1.705 | 0.160 | 0.166 | 12.8x | 10.7x |
| 100 KB | 20.599 | 16.540 | 1.464 | 1.423 | 14.1x | 11.3x |
| 1 MB | 232.558 | 181.855 | 14.901 | 14.177 | 15.6x | 12.2x |
| 10 MB | 2788.746 | 2261.557 | 159.935 | 172.933 | 17.4x | 14.1x |
所以 bs4(html.parser) 大約比 selectolax-Lexbor 這類 C 解析器慢 12–17 倍,而換成 lxml 後端後也只把差距拉回到 10.5–14 倍,仍然落後整整一個數量級。原因不是 bug,而是架構設計:不管底層解析器是誰,bs4 都會為每個節點建立完整的 Python 物件(Tag 或 NavigableString)。這一層物件化的成本,是 C 解析器根本不用付的稅。
你會發現倍數會隨頁面變大而上升——1 KB 是 12.0x,10 MB 則是 17.4x。這代表這不是可以被攤平的固定啟動成本,而是每個節點都要付一次的成本,而且節點越多,成本就越高。
再換個角度看,因為「慢 10 倍」聽起來通常比實際情況更可怕。1 MB 的頁面上,差距是 232 ms 對 15 ms。若你的工作是「抓幾百到幾千個、每個幾百 KB 的頁面」,這個絕對差異幾乎感覺不到——你不會有體感,也不會因為優化它而得到什麼。若你的工作是一個百萬頁級的流程,同樣的比例就會變成能不能跑完的關鍵差別。數字一樣,結論完全不同。判斷時要看你的實際量,而不是只看 benchmark。
不,換後端也救不了它
有個很常見的迷思是:只要把 bs4 指到 lxml 後端,就能得到 lxml 的速度。其實不行,而且這點值得講清楚。以一批 10 萬節點的 CSS 查詢為例(選出所有 <a> 並讀取 href,且樹已經先建好),吞吐量差距非常明顯:
| 解析器 | 查詢 p50 | 節點/秒 |
|---|---|---|
| lxml | 33.30 ms | 3,002,646 |
| selectolax-Modest | 34.19 ms | 2,924,550 |
| selectolax-Lexbor | 39.46 ms | 2,534,027 |
| bs4 (lxml) | 250.56 ms | 399,111 |
bs4(lxml) 每秒只能處理大約 39.9 萬個節點,仍比三個 C 引擎慢 6.3–7.5 倍,即使它的底層後端本來就是 lxml。原因在於:後端只加速了樹的「建立」,但查詢與遍歷仍然會經過 soupsieve 與 bs4 的 Tag 物件,而且每個匹配到的節點都還要再被 Python 包裝一次。所以「給 bs4 lxml,它就會跟 lxml 一樣快」這種直覺是錯的:後端只加速了一個階段,而最慢的那個階段根本不是那一段。
記憶體與冷啟動也補上了成本。對一份 10 MB 文件來說,bs4 的常駐記憶體大約是 selectolax 或 lxml 的 1.5–1.75 倍(218–226 MB 對 129–145 MB)——根本原因還是一樣:每個節點都得有一個 Python 物件。另外,匯入 bs4 大約要 33.4 ms,而 lxml.html 只要 14.1 ms,所以前者的匯入速度慢了 2.36 倍。對長時間運行的程式來說,這是小數點後的差異;但如果你做的是 CLI 工具,或是經常冷啟動的 serverless function,這就是一個值得知道的真實成本。
為什麼多執行緒救不了它
如果你對 CPU 密集型慢工作負責人的第一反應是「開執行緒硬上」,bs4 會讓你失望。以一個 1 MB 頁面重複解析 48 次為例,單執行緒與四執行緒的結果如下:
| 解析器 | 1 執行緒 | 4 執行緒 | 加速比 |
|---|---|---|---|
| selectolax-Lexbor | 0.563 s | 0.159 s | 3.54x |
| lxml | 0.459 s | 0.378 s | 1.21x |
| bs4 (lxml) | 6.945 s | 26.842 s | 0.26x |
請把最下面那一列再看一次。四個執行緒讓 bs4 慢了大約 3.9 倍,不是更快。實驗訊號很明確:它大概率持有 GIL。bs4 的樹建立完全是 Python 寫的,所以會被 Global Interpreter Lock 序列化;把執行緒堆上去,只會在一個根本無法真正平行執行的工作上增加排程開銷。selectolax 能拿到約 3.5 倍的加速,是因為它的 C 核心會釋放鎖;bs4 沒這個空間。
面對未來的 free-threading 版本,實務上的結論是:如果你真的需要平行化 BeautifulSoup,請用多行程(ProcessPoolExecutor),不要用執行緒。selectolax 和 lxml 可以吃到執行緒層級的並行,bs4 不行。這裡也補一個嚴謹性上的說明——這只是 4 個執行緒、1 MB 頁面的一次觀察,而「持有 GIL」這個機制,是根據 wall-clock 行為推斷出來的假設,不是我逐行 instrument 後確認的結論。方向很明確,但精確機制仍是推論。
預設後端是陷阱。先看這段
如果你只從這篇評測記住一件事,那就記這個。單純寫 BeautifulSoup(html)、不傳第二個參數時,預設會用 html.parser,而 html.parser 不會處理 HTML5 的可選結尾標籤規則。這句話聽起來很學術,直到它悄悄把你的資料弄壞。

我把 15 個刻意做壞的 HTML 範例丟給三個後端測試,而且每個案例在跑之前都先預先定義了與後端無關的結構性斷言(也就是說,不存在事後挑贏家)。結果如下:
| 後端 | 符合預期 / 15 |
|---|---|
| lxml | 15 |
| html5lib | 15 |
| html.parser | 12 |
這三個失敗案例都有同一個根源。拿一個沒關閉的表格來說:<table><tr><td>a<td>b<tr><td>c<td>d</table>。在 html.parser 下,抽出的儲存格文字會變成 ['abcd','bcd','cd','d']——每個 <td> 都把後面的內容一起吞掉,因為解析器把儲存格巢狀包起來了,而不是把它關閉。lxml 和 html5lib 則正確回傳 ['a','b','c','d']。純 <li> 也一樣:<li>a<li>b<li>c 在 html.parser 下會變成巢狀的 ['abc','bc','c'],另外兩個後端則是乾淨的 ['a','b','c']。重複屬性也會翻轉——<div id="first" id="second"> 在 html.parser 下保留的是 "second",而 lxml/html5lib 保留 "first";HTML5 規範要求保留第一個。
這危險的地方不只是煩人,而是它不會丟錯誤。一個隨手寫 BeautifulSoup(html) 的爬蟲,只要碰到未關閉的表格或清單——這在老網站、手寫 HTML、以及漏了關閉標籤的模板裡其實很常見——就可能把相鄰欄位文字黏在一起,給你髒資料,整個過程卻完全不吭聲。修正方式只要多傳一個參數:BeautifulSoup(html, "lxml") 或 BeautifulSoup(html, "html5lib")。
公平地說,html.parser 也不是一無是處;另外 15 個裡有 12 個做壞的樣本,在三個後端上結果都一樣。像 <b><i></b></i> 這種錯誤巢狀、缺少 html/body 骨架、未加引號的屬性、孤立的結尾標籤、未關閉的註解、巢狀表單、大小寫混用等等,bs4 的容錯其實都很強。真正的分歧幾乎都集中在可選結尾標籤這一類。這也不是什麼新發現——bs4 自己的「不同解析器差異」文件早就用白話寫明了 html.parser 比較「沒那麼寬容」。而這次的壞 HTML 矩陣,多補的是那些具體、可重現、會讓「沒那麼寬容」變成錯誤輸出的案例。
你不會失去什麼:API 與 CSS 才是它最強的地方
所以 bs4 慢、單執行緒,而且預設後端有坑。那為什麼大家還是照樣用?因為它「親切」的那一半是真的,而且實測下來確實成立。

我做了 29 個 API 探針,涵蓋搜尋、CSS、樹狀導覽、文字抽取與 DOM 修改。29 個全數通過,而且每個結果都是拿實際回傳值跟預期值做比對,而不是憑肉眼判定。這些能力裡,有兩個是 C 解析器通常不提供的易用性優勢:
find/find_all支援函式條件。 你可以寫soup.find(lambda t: t.name == "a" and "btn" in t.get("class", [])),用一行 Python 就表達很複雜的條件,不需要先抓全部再自己過濾。- 有名稱、雙向的樹狀導覽。
.parent、.next_sibling、.find_parent、.stripped_strings、.descendants——這些走訪方式看起來就像人話,而且前後都能走。有些在 selectolax 要多做幾步,甚至根本沒有。
這就是它「幫你省開發時間」的那一半,而且是具體可驗證的,不是行銷話術:29 個綠燈。
也要順手提醒兩個容易踩的坑,因為公平的評測要把兩面都講清楚。第一個是布林屬性:<input disabled> 在 bs4 裡會把 disabled 回傳成空字串 ""(selectolax 則回 None)。兩者都算 falsy,所以如果你寫 if node.get("disabled"),其實會默默漏掉這個真的存在的布林屬性。安全的判斷是 "disabled" in tag.attrs。第二個是 get_text(strip=True):它在去除空白後會直接把節點文字黏起來,不會自動加分隔符,所以 "...with " 加上 "link1" 會變成 "withlink1"。當你需要詞與詞之間的邊界時,請傳 separator=" "。這兩個坑都不是 bs4 專屬,但都是真實存在的跨庫陷阱。
再來是很多人會意外的一點:選 bs4 並不代表你得放棄 CSS 覆蓋率。它的 CSS 引擎 soupsieve 是這整組比較裡最完整的實作。在 41 個基礎案例的矩陣上(沿用自 selectolax 的基準環境),soupsieve 拿到 41/41,是全場唯一滿分,領先 selectolax-Lexbor 的 39/41,以及 cssselect(lxml/parsel)的 37/41。之後我又跑了 20 個 soupsieve 文件中宣稱支援的延伸案例,它也拿到 20/20,包括 Lexbor 直接拒絕的選擇器::lang(en)、soupsieve 專有的 :-soup-contains('featured')、:is()、:where()、還有 :has(> a)。真正缺的只有 XPath(soupsieve 只支援 CSS),以及 parsel 的 ::text / ::attr() 偽元素,那是 Scrapy 的擴充。如果你平常都靠 XPath,這種遷移會很痛。
這一節的結論很乾淨:選 BeautifulSoup 你犧牲的是速度,不是 API 易用性,也不是 CSS 覆蓋率。
兩個上線後值得預留預算的坑
除了預設後端之外,還有兩個行為會在長時間運行或非 UTF-8 工作負載裡咬你一口。
參考循環:長迴圈裡要呼叫 decompose()
每個 bs4 的 Tag 物件都同時持有對父節點的參考,以及對子節點的參考,這就形成了參考循環。CPython 的 reference counting 沒辦法自己回收循環物件;那是世代式垃圾回收器的工作。為了看這件事到底多重要,我在關閉 GC 的情況下建立並刪除一棵樹 300 次,然後數記憶體中還活著的 Tag 物件:

| 情境 | del 後仍保留的 Tag 數量 |
|---|---|
| 關閉 GC | 120,900(300 次循環,完全沒回收) |
| 開啟 GC | 26,598(世代式 GC 在迴圈中途觸發) |
強制執行 gc.collect() 後 | 0(全部回收) |
| 無循環對照組(字串列表,GC 關閉) | delta 0 |
在關閉 GC 的情況下,del soup 什麼也沒回收——全部 120,900 個物件都還留在記憶體裡,因為參考循環讓 reference counting 失效了。只要手動跑一次 gc.collect(),就會全部清掉。無循環的對照組(單純字串列表,已知不會形成循環)則是零差異,這證明堆積真的來自 bs4 的循環,而不是測量雜訊。bs4 自己的文件也說這些物件「彼此高度交織……正是垃圾回收器最頭痛的那種」,所以這本來就是文件中寫得明白的行為;而這個測試補上的,是殘留物件數,以及 collect() 可以把它清成 0 的證明。
實務規則很簡單:如果你的流程會在緊密迴圈裡解析很多大頁面,而且你的程式(或某些高吞吐設定)把 GC 關掉了,或者 GC 觸發不夠頻繁,bs4 樹就會滯留,記憶體也會一路往上漲。每抓完一頁就呼叫 soup.decompose()——bs4 之所以提供這個方法,就是為了提早打斷循環並回收資源。selectolax 和 lxml 的 C 樹沒有這個問題。
編碼:UnicodeDammit 是 bs4 悄悄的優勢
bs4 有一個快速解析器沒有的元件:UnicodeDammit。它會偵測文件編碼並自動轉成 Unicode。我給它一組 8 種「宣告編碼 vs 實際編碼」的案例:

| 案例 | 真實編碼 | UnicodeDammit 猜測 | 是否還原成功? |
|---|---|---|---|
| utf8_no_decl | utf-8 | utf-8 | 是 |
| utf16_bom | utf-16 | utf-16le | 是 |
| gbk_chinese | gbk | gb18030 | 是(超集) |
| shiftjis | shift_jis | cp932 | 是(超集) |
| latin1_declared_utf8 | latin-1(宣告為 utf-8) | iso-8859-1 | 是(忽略了謊言) |
| latin1_no_decl | latin-1 | cp720 | 否 |
| cp1252_no_decl | cp1252 | cp862 | 否 |
| utf8_declared_latin1 | utf-8(宣告為 latin-1) | iso-8859-1 | 否(照著謊言走) |
8 個案例裡有 5 個成功還原。UTF-8、帶 BOM 的 UTF-16、GBK、Shift-JIS,甚至被錯標的 latin-1 都抓對了,而且那些「超集」猜測(GBK→gb18030、Shift-JIS→cp932)仍然能正常解碼。失敗的兩種情況也值得知道:很短的 latin-1/cp1252 位元組樣本,會被誤判成 DOS code page,因為統計式偵測器對短輸入不夠可靠,而且 DOS 方框字元和 Latin-1 的碼點又會重疊;另外,當 <meta charset> 的宣告本身就是錯的時候,UnicodeDammit 會相信那個宣告。bs4 的文件也有提醒:樣本可能「短到 Unicode Dammit 也抓不準」,而資料越多,判斷通常越好。
跟 selectolax 相比,後者對非 UTF-8 位元組不會幫你修正,而是默默弄壞,要求你自己先解碼;因此 bs4 至少會嘗試偵測,而且常常成功。只是這不是保證。對已知編碼的資料,別猜,直接明講:BeautifulSoup(bytes, from_encoding="...")。
在真實頁面上,這些後端真的會不同嗎?
壞 HTML 矩陣已經證明,刻意做壞的輸入會讓後端分歧。下一個很自然的問題是:這在真實世界有沒有影響?所以我拿 11 個實際抓下來的頁面——BBC、Wikipedia、Craigslist、MDN、old.reddit、Python docs、Hacker News、Books to Scrape、webscraper.io、whitehouse.gov,以及一個 JS 渲染的 quotes 頁面——分別用三個後端去比較連結、標題與圖片數量。
結果是:11 個頁面全部一致,完全沒有分歧。這代表前面那個陷阱區看到的後端差異,只會出現在刻意做壞的 HTML 上;當現代生產網站的結構夠好——即使有點亂——後端選擇並不會改變你抽到的內容。實務解讀就是:對主流、結構正常的網站來說,html.parser 完全夠用,也能省掉依賴。只有在你抓的是明顯不標準、手寫、或年代久遠的 HTML 時,後端選擇才會開始影響結果,這時才該切到 lxml 或 html5lib。
那次測試還有一個有意思的邊界案例。MDN 頁面裡有一個 <template> 元素,而所有 bs4 後端都回傳了 508 個連結——也就是說,bs4 會把 <template> 裡的內容扁平化到主樹中。這讓 bs4 跟 lxml 站在同一邊,跟 selectolax-Lexbor 則相反;後者嚴格遵守 HTML5 規範(<template> 是 inert 的 DocumentFragment),所以只回傳 497 個連結,把 template 裡的 11 個連結靜默忽略了。因此 bs4 會抓到 <template> 裡的資料——這很實用,但也可能讓你撈到瀏覽器根本不會渲染的「幽靈內容」。兩種做法都不能算錯,只是對規範的解讀不同,你要知道自己拿到的是哪一種。
BeautifulSoup 適合放在哪裡,又不適合放在哪裡
與其把這一切硬壓成一個 0–100 分,不如直接看各維度的成績單,並在每一列附上一個提醒:
| 維度 | 測試結果 | 讀者提醒 |
|---|---|---|
| 安裝 / 首次執行 | 純包裝器,不需要瀏覽器或額外設定;html.parser 零依賴;都有預編譯 wheel | lxml 後端需要 C 依賴 |
| 速度 vs C 解析器 | 慢 12–17 倍(html.parser)/ 10.5–14 倍(lxml 後端),所有大小都一樣 | 單一基準環境;沿用 selectolax 資料 |
| CSS 查詢吞吐量 | 在 10 萬節點上約慢 6–7.5 倍;lxml 後端也救不了 | 沿用資料;要付 Python Tag 稅 |
| 記憶體 | 比 selectolax/lxml 高 1.5–1.75 倍;最重 | 沿用資料;以 RSS 測量 |
| 匯入冷啟動 | 慢 2.36 倍(33.4 vs 14.1 ms) | 沿用資料;屬小項目 |
| 執行緒擴展性 | bs4-lxml 在 4 執行緒下反而慢約 3.9 倍(持有 GIL) | 單次觀察;請用多行程 |
| API 易用性 | 29/29 探針通過;find 可用函式條件 + 雙向導覽 | 布林屬性空字串與 strip 的詞邊界陷阱要注意 |
| CSS 覆蓋率 | soupsieve 最強:41/41 基礎 + 20/20 延伸;支援 :lang | 沒有 XPath,也沒有 ::text |
| 三後端容錯 | lxml/html5lib 15/15;html.parser 12/15 | 只在壞 HTML 上分歧 |
| 真實頁面一致性 | 三後端在 11/11 頁面上都一致;都會扁平化 <template>(508) | 對結構正常的網站來說,後端影響不大 |
| 參考循環 GC | 樹是循環結構;300 次迴圈留住 120,900 個物件,collect() 後歸零 | 長迴圈需要 decompose() |
| 編碼 | UnicodeDammit 可還原 8 個案例中的 5 個;短樣本容易誤判,也會照著錯誤宣告走 | 單次觀察 |
| 維護狀態 | 活躍(2026 年 6 月的 4.15.0);MIT | 主站在 crummy/Launchpad,不在 GitHub |
那麼 BeautifulSoup 適合誰?適合那些重視可讀 API、以及願意用容錯解析換取開發效率的人,尤其是中等規模的工作:原型、一次性抓取、內部工具、或是開發時間比執行時間更昂貴的團隊。誰該找別的工具?百萬頁級流程,因為速度稅會累積成真金白銀;需要執行緒層級平行化的工作;以及任何深度依賴 XPath 的人。
最後補一點它在真實爬蟲堆疊中的位置,以及我們自己的工具在其中扮演什麼角色。BeautifulSoup 的前提是:你已經有 HTML 了。它不負責抓頁、不負責渲染 JavaScript,也不處理反爬或 CAPTCHA——那是另一個層級的工作,而在現代網路上,這其實才是最難的一段。這也是 AI 抓取 API 會出現在另一層的原因:Thunderbit 的開發者堆疊——REST API、MCP server 與 CLI——負責抓取、JavaScript 渲染,以及反爬問題,然後直接回傳乾淨的 Markdown(POST /distill)或符合 schema 的結構化 JSON(POST /extract),你甚至不用自己寫 selector。兩者不是競爭關係,而是互補。bs4 負責解析你已經拿到的 HTML;Thunderbit 的 API、MCP 與 CLI 則幫你拿到一開始不容易碰到的 HTML。如果你的瓶頸在解析,bs4 是很好的答案;如果瓶頸在取得資料,那就是另一個層次的問題了。
最後結論
BeautifulSoup 提供了這組比較裡最親切的 API、對壞 HTML 最強的容錯能力,以及最完整的 CSS 引擎——代價則是大約一個數量級的速度稅,外加最重的記憶體占用。這就是整筆交易,講白了就是這樣。真正的陷阱是預設的 html.parser:它會悄悄把未關閉的表格和清單弄亂,所以只要輸入可能很髒,就請明確傳入 "lxml" 或 "html5lib"。執行緒不會讓它更快——多行程才會。長時間迴圈裡,請對每一頁呼叫 decompose(),避免參考循環一路累積。
最後補兩個限制。這裡的所有測試都只在單一平台上完成(macOS arm64、Python 3.14、預編譯 wheel),而且時間倍數是沿用 selectolax 基準環境的資料(同一組 benchmark,截至 2026-07-13),不是重新跑的,所以也繼承了單平台限制;若換成 Linux x86_64 或自行編譯原始碼的環境,數字可能會有些變動。再來,這些結果裡沒有任何「新發現」:bs4 本來就是一個 20 年歷史的函式庫,因此每個被測試的行為不是文件寫過,就是公開紀錄過。這篇文章的價值不是爆料,而是把文件只會定性描述的取捨,變成可量化的實際數字。
常見問題
BeautifulSoup 很慢嗎?
是,而且可以量化。在 parse 加 extract 的任務上,它用預設的 html.parser 後端,速度大約比 selectolax-Lexbor 這種 C 解析器慢 12–17 倍;換成 lxml 後端則慢 10.5–14 倍,因為它會為每個節點建立 Python 物件。這有沒有影響,要看規模:1 MB 的頁面是 232 ms 對 15 ms,處理幾千頁幾乎沒差,但百萬頁流程就很關鍵。
BeautifulSoup 應該用哪個解析器:html.parser、lxml 還是 html5lib?
如果是結構正常、主流的網站,預設的 html.parser 就夠了,而且沒有額外依賴。但它不支援 HTML5 的可選結尾標籤,所以遇到未關閉的表格或清單時,會悄悄把相鄰文字黏在一起,而且不會報錯。當你的輸入可能是壞掉的、手寫的、或很老的 HTML,請明確傳入 "lxml" 或 "html5lib"——在壞 HTML 測試矩陣上,這兩者都拿到 15/15,而 html.parser 只有 12/15。
BeautifulSoup 能用執行緒平行解析嗎?
不能。bs4 的樹建立是純 Python,而且會持有 GIL,所以加執行緒不會更快,只會更慢——測試中,四個執行緒解析 1 MB 頁面,比單執行緒慢了約 3.9 倍。若要平行化 bs4,請用多行程(ProcessPoolExecutor)。像 selectolax 和 lxml 這種 C 核心的庫,才有執行緒層級的平行收益。
BeautifulSoup 對壞 HTML 的處理好嗎?
整體來說是好的——在一系列刻意做壞的樣本(錯誤巢狀、缺少骨架、未加引號的屬性等等)上,三個後端大多都能正確恢復。唯一明顯的弱點是預設的 html.parser 與可選結尾標籤:未關閉的 <td> / <li> 會被巢狀包起來而不是關閉,導致抽出的文字被污染。切到 lxml 或 html5lib 後,這類問題就會消失。
BeautifulSoup 跟 lxml 比,哪個比較好?
它們是不同工具。lxml 在樹建立與查詢上都快得多,也支援 XPath。BeautifulSoup 則把 lxml(以及其他後端)包成更友善的 API,而且透過 soupsieve,CSS 覆蓋率其實更完整。只是不要期待把後端設成 lxml 就能讓 bs4 變成 lxml 的速度——後端只加速解析,查詢與遍歷仍然要付 bs4 每節點 Python 物件的成本,所以在大型批次選取上還是會慢大約 6–7.5 倍。
試用 Thunderbit 進行網頁資料擷取 Get Started Free


