BeautifulSoup là thư viện mà gần như ai mới bắt đầu cào web bằng Python cũng sẽ tìm tới đầu tiên, và đúng là nó chậm nhất trong nhóm các HTML parser nghiêm túc. Cả hai điều đó đều đúng, và không điều nào là một lời chê. Cái thú vị là “chậm nhất” ở đây lại là một con số rất cụ thể, đo đếm được — chứ không chỉ là cảm giác chung chung.
Tôi đã đưa bs4 (tức beautifulsoup4, phiên bản 4.15.0, phát hành tháng 6/2026, giấy phép MIT) qua một loạt bài test năng lực mới và tái sử dụng dữ liệu đo thời gian từ cùng bộ benchmark, và kết quả rất nhất quán: bạn đánh đổi khoảng một bậc độ lớn về tốc độ để lấy API thân thiện nhất và khả năng chịu lỗi mạnh nhất trong lĩnh vực này. Cái đổi đó có đáng hay không thì hoàn toàn còn tùy khối lượng công việc của bạn, nên bài đánh giá này sẽ đặt cả hai mặt lên bàn.
BeautifulSoup thực ra là gì, và không phải là gì
Phần lớn bài hướng dẫn hay bỏ qua chỗ quan trọng nhất: BeautifulSoup không trực tiếp phân tích HTML. Nó là một lớp bọc. Bên dưới, nó chuyển tài liệu của bạn cho một trong ba parser thực sự — html.parser tích hợp sẵn của Python, lxml, hoặc html5lib — rồi bọc cây kết quả trong một API duyệt và tìm kiếm cực kỳ thân thiện. Nhiệm vụ của bs4 không phải là parse. Nhiệm vụ của nó là làm cho việc đi lại trong kết quả trở nên dễ chịu.
Chính tác giả của nó gọi đây là một “screen-scraping library”, và định vị ban đầu của nó vẫn luôn như vậy: đưa nó vào một khối HTML méo mó đến mức trình duyệt cũng phải nhăn mặt, nó vẫn sẽ cố kéo ra dữ liệu bạn cần. Danh tiếng đó là có thật, chỉ có một dấu sao nhỏ mà ta sẽ nói tới sau.
Một vài факт đáng ghi nhận trước khi đi tiếp:
| Hạng mục | Giá trị |
|---|---|
| Gói | beautifulsoup4 (import là bs4) |
| Phiên bản đã kiểm thử | 4.15.0 (upload ngày 2026-06-07) |
| Yêu cầu Python | >=3.7.0 |
| Giấy phép | MIT |
| Trang chính thức | crummy.com/software/BeautifulSoup |
| Mã nguồn + bug tracker | Launchpad — không phải GitHub |
| Tình trạng duy trì | Đang hoạt động (4.15.0 vào tháng 6/2026, sáu bản phát hành trong năm qua) |
Chi tiết “không phải GitHub” quan trọng hơn vẻ ngoài của nó. bs4 là một thư viện đã 20 năm tuổi, sống trên crummy.com và Launchpad, nên kiểu nhìn “bao nhiêu sao GitHub” không áp dụng ở đây. Hãy đánh giá sức sống của nó qua nhịp phát hành; theo thước đo đó thì nó vẫn rất ổn.
Một điểm tinh tế về giấy phép, dành cho ai phải trả lời bộ phận tuân thủ: bản wrapper là MIT, nhưng “dùng bs4” thực ra sẽ kéo gì vào cây phụ thuộc còn tùy backend bạn cài. html.parser là thư viện chuẩn của Python (giấy phép PSF, không thêm phụ thuộc). lxml dùng BSD, nhưng nó dựa trên libxml2/libxslt — một phụ thuộc C bên ngoài mà bạn либо tự biên dịch hoặc dùng wheel có sẵn. html5lib là thuần Python và MIT. Nếu bạn muốn footprint phụ thuộc gọn nhất, parser tích hợp sẵn html.parser sẽ cho bạn điều đó — và trớ trêu thay, đó cũng là backend có cái bẫy lớn nhất. Ta sẽ quay lại ngay sau đây.
Cái giá của tốc độ, được định lượng
Hãy nói con số trước, vì đây là điểm nhấn và che giấu nó là không trung thực. Trên một tác vụ parse rồi trích xuất thực tế — parse chuỗi, lấy mọi <h3 class="title"> và mọi <a href> — BeautifulSoup là parser chậm nhất trong so sánh này, và không hề sít sao.

Các số đo này được tái sử dụng từ bộ benchmark của selectolax (cùng máy, cùng phương pháp 3 lần chạy, tính đến 2026-07-13); bài đánh giá này không tự chạy lại bất kỳ benchmark thời gian nào để tránh tranh chấp CPU và trùng lặp công sức. Độ trễ p50 trung vị, tính bằng mili giây:
| Kích thước trang | bs4 (html.parser) | bs4 (lxml) | selectolax-Lexbor | lxml | bs4-hp chậm hơn | bs4-lxml chậm hơn |
|---|---|---|---|---|---|---|
| 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 |
Vậy nên bs4(html.parser) chạy chậm hơn khoảng 12–17 lần so với một parser C như selectolax-Lexbor, và chuyển sang backend lxml cũng chỉ kéo nó về mức 10.5–14 lần — vẫn chậm hơn cả một bậc độ lớn. Lý do nằm ở cấu trúc, không phải lỗi: bất kể backend nào thực hiện parsing, bs4 đều tạo một đối tượng Python hoàn chỉnh (Tag hoặc NavigableString) cho từng node. Lớp “đóng gói thành object” đó là một khoản thu mà parser C không phải trả.
Hãy để ý hệ số nhân tăng theo kích thước trang — 12.0x ở 1 KB, lên 17.4x ở 10 MB. Điều đó cho thấy đây không phải là overhead khởi động cố định có thể “chia loãng” dần. Đây là khoản phí trên từng node, tăng tuyến tính theo số node bạn tạo.
Giờ chuyển sang góc nhìn khác, vì “chậm hơn 10x” nghe đáng sợ hơn thực tế thường thấy. Với một trang 1 MB, bạn đang nói về 232 ms so với 15 ms. Nếu công việc của bạn là “cào vài trăm đến vài nghìn trang, mỗi trang vài trăm KB”, thì khác biệt tuyệt đối đó gần như không cảm nhận được — bạn sẽ không thấy nó, và tối ưu nó cũng chẳng mang lại lợi ích gì. Nhưng nếu công việc của bạn là pipeline một triệu trang, cùng tỷ lệ đó lại quyết định một job có chạy xong hay không. Cùng một con số, kết luận trái ngược. Hãy cân nó theo khối lượng thực tế của bạn, không phải theo benchmark.
Không, đổi backend không làm nó hết chậm
Có một hiểu lầm rất dai dẳng rằng chỉ cần cho bs4 dùng backend lxml là sẽ có tốc độ của lxml. Không phải vậy, và lý do đáng để hiểu. Trên một truy vấn CSS theo batch 100.000 node (chọn mọi <a> và đọc href, cây đã được build sẵn), chênh lệch thông lượng rất rõ:
| Parser | Query p50 | Nodes/giây |
|---|---|---|
| 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) chỉ đạt khoảng 399.000 node/giây — chậm hơn khoảng 6.3–7.5 lần so với ba engine C, dù backend của chính nó đã là lxml. Backend chỉ tăng tốc giai đoạn build cây. Việc truy vấn và duyệt vẫn đi qua soupsieve vào các object Tag của bs4, và mỗi node được khớp vẫn phải bọc trong Python. Vì vậy mô hình tinh thần “cho bs4 lxml thì nó sẽ nhanh như lxml” là sai: backend chỉ làm nhanh một giai đoạn, còn giai đoạn chậm nhất lại không nằm ở đó.
Bộ nhớ và thời gian khởi động lạnh cũng góp phần vào tổng chi phí. Trên tài liệu 10 MB, bs4 dùng khoảng 1.5–1.75x RSS bộ nhớ của selectolax hoặc lxml (218–226 MB so với 129–145 MB) — cùng nguyên nhân gốc: mỗi node là một object Python. Và import bs4 mất khoảng 33.4 ms so với 14.1 ms cho lxml.html, tức là chậm hơn 2.36x khi import. Con số này gần như không đáng kể trong tiến trình chạy lâu, nhưng với CLI tool hoặc hàm serverless thường xuyên cold-start, đó vẫn là một chi phí nhỏ nhưng có thật.
Vì sao thêm nhiều thread cũng không cứu được
Nếu phản xạ của bạn với một tác vụ CPU-bound chậm là “ném thread vào”, bs4 sẽ trừng phạt phản xạ đó. Trên một trang 1 MB được parse 48 lần, so sánh đơn luồng và bốn luồng:
| Parser | 1 thread | 4 thread | Tăng tốc |
|---|---|---|---|
| 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 |
Hãy đọc kỹ dòng cuối hai lần. Bốn thread làm bs4 chậm hơn khoảng 3.9x, chứ không hề nhanh hơn. Tín hiệu thực nghiệm là “nhiều khả năng đang giữ GIL”: việc build cây của bs4 thuần Python nên bị tuần tự hóa dưới Global Interpreter Lock, và việc thêm thread chỉ tạo thêm overhead lập lịch cho một việc vốn không thể chạy song song thật sự. selectolax có thể tăng tốc khoảng 3.5x vì lõi C của nó nhả lock; bs4 thì không có khoảng trống đó.
Trong thời kỳ free-threading, bài học thực tế là: nếu bạn cần song song hóa BeautifulSoup, hãy dùng multiprocessing (ProcessPoolExecutor), không phải thread. selectolax và lxml có thể scale bằng thread; bs4 thì không. Một lưu ý về độ chặt chẽ — đây là một quan sát đơn lẻ ở một mức thread (4) trên một kích thước trang (1 MB), và cơ chế “giữ GIL” là giả thuyết được suy ra từ wall-clock, chứ không phải điều tôi trực tiếp xác nhận bằng cách instrument xem code path nào đang giữ lock. Hướng đi thì rõ; cơ chế chính xác thì vẫn là tạm thời.
Backend mặc định là cái bẫy. Hãy đọc phần này trước tiên.
Nếu bạn chỉ nhớ một điều từ bài đánh giá này, hãy nhớ điều này. BeautifulSoup(html) thuần túy, không truyền đối số thứ hai, sẽ dùng html.parser, và html.parser không triển khai các quy tắc optional-end-tag của HTML5. Nghe có vẻ học thuật cho đến khi nó âm thầm làm hỏng dữ liệu của bạn.

Tôi đã chạy 15 mẫu HTML cố tình bị lỗi qua cả ba backend, với một phép kiểm tra cấu trúc độc lập với backend được định nghĩa trước cho từng mẫu trước khi chạy (để không ai được phép chọn người thắng sau khi nhìn kết quả). Điểm số:
| Backend | Đáp ứng kỳ vọng / 15 |
|---|---|
| lxml | 15 |
| html5lib | 15 |
| html.parser | 12 |
Ba lỗi thất bại đều cùng một gốc. Lấy ví dụ một bảng chưa đóng thẻ: <table><tr><td>a<td>b<tr><td>c<td>d</table>. Với html.parser, text ô trích xuất ra là ['abcd','bcd','cd','d'] — mỗi <td> nuốt mọi thứ phía sau nó, vì parser lồng ô vào nhau thay vì đóng ô lại. lxml và html5lib đều trả về đúng ['a','b','c','d']. Các mục danh sách trần trụi cũng tương tự: <li>a<li>b<li>c cho ra ['abc','bc','c'] theo kiểu lồng nhau dưới html.parser, còn hai backend kia trả về sạch ['a','b','c']. Thuộc tính trùng lặp cũng đảo kết quả — <div id="first" id="second"> giữ "second" trong html.parser nhưng giữ "first" trong lxml/html5lib, và đặc tả HTML5 nói rằng phải giữ giá trị đầu tiên.
Điều nguy hiểm ở đây không chỉ là khó chịu: nó xảy ra mà không báo lỗi. Một scraper vô tư dùng BeautifulSoup(html) rồi gặp bảng hoặc danh sách chưa đóng thẻ — điều này đáng buồn thay lại rất phổ biến ở site cũ, HTML viết tay, hoặc template quên mất thẻ đóng — sẽ làm text ở ô liền kề trộn vào cùng một field, trả cho bạn dữ liệu bẩn, mà không hề kêu ca. Cách sửa chỉ cần một đối số: BeautifulSoup(html, "lxml") hoặc BeautifulSoup(html, "html5lib").
Công bằng mà nói với html.parser, 12 trong 15 mẫu lỗi còn lại đều cho kết quả giống nhau trên cả ba backend — các tag lồng sai như <b><i></b></i>, thiếu khung html/body, thuộc tính không có dấu ngoặc kép, thẻ đóng lạc chỗ, comment chưa đóng, form lồng nhau, chữ hoa/chữ thường lẫn lộn, v.v. Khả năng chịu lỗi của bs4 thực sự rất mạnh trên diện rộng; sự khác biệt gần như tập trung hoàn toàn vào nhóm optional-end-tag. Và không có gì ở đây là phát hiện mới — tài liệu “Differences between parsers” của bs4 vốn đã nói html.parser là “less lenient” bằng ngôn ngữ rất thẳng. Điều bảng lỗi mang lại là những trường hợp cụ thể, lặp lại được, nơi “kém linh hoạt hơn” biến thành đầu ra sai.
Bạn không đánh đổi những gì: API và CSS mới là điểm mạnh nhất
Vậy bs4 chậm, đơn luồng, và có cái bẫy backend mặc định. Thế mà người ta vẫn dùng nó, vì nửa “thân thiện” của sự đánh đổi này là hoàn toàn có thật — và nó vẫn trụ vững khi kiểm thử.

Tôi đã chạy 29 phép kiểm API bao phủ search, CSS, duyệt cây, trích text và chỉnh sửa DOM. Cả 29 đều qua, và kết quả từng phép kiểm được tính bằng cách so sánh đầu ra thực tế với giá trị kỳ vọng chứ không phải nhìn bằng mắt. Hai khả năng trong số đó là những tiện ích mà các parser C đơn giản là không có:
- Predicate bằng hàm trong
find/find_all. Bạn có thể viếtsoup.find(lambda t: t.name == "a" and "btn" in t.get("class", []))để diễn đạt một điều kiện phức tạp chỉ trong một dòng Python — không cần kiểu “chọn tất cả rồi mới lọc” hai bước. - Duyệt cây có tên, hai chiều.
.parent,.next_sibling,.find_parent,.stripped_strings,.descendants— các bước duyệt đọc như tiếng Anh và đi được cả hai hướng. selectolax với vài trường hợp sẽ cần nhiều bước hơn, hoặc không cung cấp hẳn.
Đó là phần “mua thời gian cho nhà phát triển” một cách rất cụ thể. Đây không phải lời quảng cáo; đây là 29 dấu kiểm màu xanh.
Có hai cái bẫy cần lưu ý, vì một bài đánh giá công bằng phải nói cả hai mặt. Thứ nhất là boolean attribute: <input disabled> trả về chuỗi rỗng "" cho disabled trong bs4 (selectolax trả về None). Cả hai đều là giá trị falsy, nên if node.get("disabled") sẽ âm thầm bỏ sót một boolean attribute thực sự đang tồn tại trong cả hai thư viện — cách kiểm tra an toàn là "disabled" in tag.attrs. Thứ hai, get_text(strip=True) ghép text các node lại mà không có dấu ngăn cách sau khi strip, nên "...with " + "link1" sẽ thành "withlink1". Hãy truyền separator=" " khi bạn cần giữ ranh giới từ. Cả hai bẫy này không riêng gì bs4; chúng là lỗi dễ gặp ở nhiều thư viện.
Và đây là phần khiến nhiều người bất ngờ: chọn bs4 không hề làm bạn mất khả năng CSS. Bộ máy CSS của nó, soupsieve, là implementation đầy đủ nhất trong toàn bộ so sánh này. Trên ma trận cơ sở 41 trường hợp (tái sử dụng từ rig của selectolax), soupsieve đạt 41/41 — điểm tuyệt đối duy nhất trong nhóm, vượt qua selectolax-Lexbor với 39/41 và cssselect (lxml/parsel) với 37/41. Sau đó tôi chạy thêm 20 trường hợp mở rộng mà tài liệu của soupsieve quảng bá, và nó đạt 20/20, bao gồm các selector mà Lexbor từ chối thẳng: :lang(en), selector riêng của soupsieve :-soup-contains('featured'), :is(), :where(), và :has(> a). Những khoảng trống thực sự chỉ là XPath (soupsieve chỉ có CSS) và các pseudo-element ::text / ::attr() của parsel, vốn là phần mở rộng của Scrapy. Nếu bạn sống trong XPath, việc chuyển sang đây sẽ đau.
Kết luận cho phần này rất rõ: thứ bạn hy sinh khi chọn BeautifulSoup là tốc độ. Không phải tiện dụng API, và chắc chắn không phải độ phủ CSS.
Hai lỗi production đáng để dự trù
Ngoài backend mặc định, có hai hành vi sẽ cắn bạn trong các workload chạy lâu hoặc không phải UTF-8.
Vòng tham chiếu: hãy gọi decompose() trong vòng lặp dài
Mỗi Tag của bs4 giữ tham chiếu tới parent và tới các child, tạo thành một vòng tham chiếu. Cơ chế đếm tham chiếu của CPython không thể tự dọn một vòng như vậy — đó là việc của garbage collector thế hệ. Để xem điều đó quan trọng đến mức nào, tôi đã build và xóa một cây 300 lần khi tắt GC, rồi đếm số object Tag còn sống trong bộ nhớ:

| Kịch bản | Tags còn giữ lại sau del |
|---|---|
| Tắt GC | 120,900 (300 vòng, không được thu hồi gì) |
| Bật GC | 26,598 (GC thế hệ đã chạy giữa vòng lặp) |
Sau khi ép gc.collect() | 0 (thu hồi hết) |
| Nhóm đối chứng không có vòng tham chiếu (list chuỗi, tắt GC) | delta 0 |
Khi GC tắt, del soup không thu hồi gì cả — toàn bộ 120,900 object vẫn ở lại bộ nhớ, vì vòng tham chiếu phá vỡ cơ chế đếm tham chiếu. Chỉ một lần gc.collect() là dọn sạch tất cả. Nhóm đối chứng không có vòng tham chiếu (một list chuỗi bình thường, vốn chắc chắn không có cycle) giữ delta bằng 0, chứng minh việc tích tụ đến từ vòng tham chiếu của bs4 chứ không phải nhiễu đo lường. Tài liệu của bs4 cũng nói rõ các object này “densely interconnected ... exactly the sort a garbage collector would have trouble with”, nên đây là hành vi đã được ghi nhận; bài test chỉ bổ sung số object còn giữ lại và bằng chứng rằng collect() đưa nó về 0.
Quy tắc thực tế: trong pipeline parse nhiều trang lớn trong một vòng lặp chặt, nếu code của bạn (hoặc một cấu hình chạy rất cao) tắt GC hoặc không kích hoạt GC đủ thường xuyên, cây bs4 sẽ còn lưu luyến và bộ nhớ sẽ tăng. Hãy gọi soup.decompose() sau mỗi trang — bs4 cung cấp nó chính xác để phá vòng và giải phóng sớm. Cây C từ selectolax và lxml thì không gặp vấn đề này.
Encoding: UnicodeDammit là lợi thế thầm lặng của bs4
bs4 đi kèm một thành phần mà các parser nhanh không có: UnicodeDammit, nó dò encoding của tài liệu và tự động chuyển sang Unicode. Tôi đã đưa nó vào một ma trận 8 trường hợp “khai báo charset so với charset thực”:

| Trường hợp | Encoding thực | UnicodeDammit đoán | Khôi phục được? |
|---|---|---|---|
| utf8_no_decl | utf-8 | utf-8 | Có |
| utf16_bom | utf-16 | utf-16le | Có |
| gbk_chinese | gbk | gb18030 | Có (superset) |
| shiftjis | shift_jis | cp932 | Có (superset) |
| latin1_declared_utf8 | latin-1 (khai báo utf-8) | iso-8859-1 | Có (bỏ qua lời khai báo sai) |
| latin1_no_decl | latin-1 | cp720 | Không |
| cp1252_no_decl | cp1252 | cp862 | Không |
| utf8_declared_latin1 | utf-8 (khai báo latin-1) | iso-8859-1 | Không (theo lời khai báo sai) |
Khôi phục được 5 trên 8. UTF-8, UTF-16 có BOM, GBK, Shift-JIS, và cả latin-1 bị gắn nhãn sai đều trở về đúng, còn các phỏng đoán superset (GBK→gb18030, Shift-JIS→cp932) vẫn giải mã ổn. Hai kiểu thất bại đáng nhớ: các mẫu byte latin-1/cp1252 quá ngắn bị nhận nhầm thành code page DOS, vì bộ dò thống kê không đáng tin trên input ngắn và ký tự vẽ khung của DOS chồng lấn với mã Latin-1; và khi một khai báo <meta charset> đơn giản là sai, UnicodeDammit tin vào khai báo đó. Tài liệu bs4 đã nhắc cả hai — mẫu có thể “so short that Unicode, Dammit can't get a lock on it”, và càng nhiều dữ liệu thì đoán càng tốt hơn.
So với selectolax, vốn sẽ làm sai ngầm các byte không phải UTF-8 và bắt bạn tự decode, đây là một ưu thế thật sự: bs4 ít nhất cố dò và thường làm đúng. Nhưng đó không phải là bảo đảm tuyệt đối. Với encoding đã biết, đừng đoán — hãy khai báo rõ: BeautifulSoup(bytes, from_encoding="...").
Backend có thật sự khác nhau trên trang thật không?
Ma trận HTML lỗi cho thấy các backend lệch nhau trên input cố tình phá. Câu hỏi tiếp theo là: chuyện đó có quan trọng ngoài đời không? Vì vậy tôi đã chạy cả ba backend trên 11 trang thật đã fetch — BBC, Wikipedia, Craigslist, MDN, old.reddit, tài liệu Python, Hacker News, Books to Scrape, webscraper.io, whitehouse.gov, và một trang quotes render bằng JS — rồi so sánh số link, heading và ảnh.
Cả ba đều đồng ý trên cả 11 trang. Không có độ lệch nào. Điều đó có nghĩa là sự bất đồng backend ở phần “bẫy” chỉ xuất hiện trên HTML cố tình lỗi; khi một site production hiện đại được cấu trúc đủ tốt — dù có hơi “bừa” — lựa chọn backend không làm đổi dữ liệu bạn trích ra. Diễn giải thực tế: với các site phổ biến, cấu trúc ổn, html.parser hoàn toàn ổn và giúp bạn khỏi thêm phụ thuộc. Chỉ khi bạn cào HTML cổ, viết tay, hoặc rõ ràng không theo chuẩn thì backend mới bắt đầu làm thay đổi kết quả, và đó là lúc bạn chuyển sang lxml hoặc html5lib.
Một điểm ngoài lề từ lần chạy đó, vì đây là một biên rất thực. Trang MDN có một phần tử <template>, và tất cả backend của bs4 đều trả về 508 link — nghĩa là bs4 làm phẳng nội dung <template> vào cây chính. Điều đó đặt bs4 cùng phía với lxml, và ngược với selectolax-Lexbor, vốn tuân thủ chặt đặc tả HTML5 (một <template> là DocumentFragment vô hiệu) và chỉ trả về 497, âm thầm bỏ 11 link nằm trong template. Vì vậy bs4 sẽ bắt được dữ liệu bên trong <template> — hữu ích, nhưng cũng có thể kéo vào những nội dung “ảo” mà trình duyệt sẽ không bao giờ render. Không bên nào sai; chúng chỉ khác cách diễn giải đặc tả, và bạn nên biết mình đang nhận cái nào.
BeautifulSoup phù hợp ở đâu — và không phù hợp ở đâu
Thay vì nén tất cả thành một điểm số 0–100 (cách đó sẽ che mất đúng những đánh đổi quan trọng), đây là bảng chấm theo từng chiều, kèm một lưu ý cho mỗi dòng:
| Chiều đánh giá | Kết quả kiểm thử | Lưu ý cho người đọc |
|---|---|---|
| Cài đặt / chạy lần đầu | Chỉ là wrapper, không cần browser/setup; html.parser không có phụ thuộc; tất cả đều có wheel sẵn | Backend lxml cần phụ thuộc C |
| Tốc độ so với parser C | chậm hơn 12–17x (html.parser) / 10.5–14x (backend lxml), mọi kích thước | Một rig duy nhất; tái sử dụng dữ liệu selectolax |
| Thông lượng truy vấn CSS | chậm hơn ~6–7.5x trên 100k node; backend lxml không cứu được | Dữ liệu tái sử dụng; chịu thuế Python Tag |
| Bộ nhớ | 1.5–1.75x selectolax/lxml; nặng nhất | Dữ liệu tái sử dụng; đo bằng RSS |
| Import cold start | chậm hơn 2.36x (33.4 so với 14.1 ms) | Dữ liệu tái sử dụng; mục nhỏ |
| Scale bằng thread | bs4-lxml chậm hơn ~3.9x ở 4 thread (giữ GIL) | Một quan sát; nên dùng multiprocessing |
| Độ tiện của API | 29/29 kiểm đạt; find với predicate hàm + duyệt hai chiều | Bẫy boolean-attr chuỗi rỗng và strip dính từ |
| Độ phủ CSS | soupsieve mạnh nhất: 41/41 cơ bản + 20/20 mở rộng; hỗ trợ :lang | Không có XPath, không có ::text |
| Khả năng chịu lỗi 3 backend | lxml/html5lib 15/15; html.parser 12/15 | Chỉ khác nhau trên HTML lỗi |
| Tính nhất quán trên trang thật | 3 backend đồng ý 11/11; tất cả đều làm phẳng <template> (508) | Site chuẩn: backend không quan trọng |
| GC vòng tham chiếu | Cây là cycle; 300 vòng giữ 120,900 object, collect() dọn sạch | Vòng lặp dài cần decompose() |
| Encoding | UnicodeDammit khôi phục 5/8; đoán sai mẫu ngắn, tin vào khai báo sai | Một quan sát đơn lẻ |
| Bảo trì | Đang hoạt động (4.15.0, tháng 6/2026); MIT | Trang chủ trên crummy/Launchpad, không phải GitHub |
Vậy BeautifulSoup dành cho ai? Cho những ai coi API dễ đọc và parsing linh hoạt quan trọng hơn throughput thô, làm việc ở quy mô vừa — prototype, scraper một lần, công cụ nội bộ, nhóm mà thời gian dev đắt hơn thời gian chạy. Ai nên tìm lựa chọn khác? Pipeline một triệu trang, nơi thuế tốc độ cộng dồn thành tiền thật; workload cần song song cấp thread; và bất kỳ ai đã gắn chặt với XPath.
Một lưu ý về vị trí của nó trong một stack scraping thực tế, và vai trò của công cụ của chúng tôi. BeautifulSoup giả định bạn đã có sẵn HTML. Nó không fetch trang, không render JavaScript, và cũng không xử lý chống bot hay CAPTCHA — đó là một công việc khác hẳn, và thực sự khó trong web hiện đại. Đây là nơi một AI scraping API nằm ở lớp khác: stack dành cho developer của Thunderbit — gồm REST API, MCP server và CLI — sẽ lo phần fetch, render JS và bài toán anti-bot, rồi trả về hoặc Markdown sạch (POST /distill) hoặc JSON có cấu trúc khớp schema (POST /extract) mà bạn không cần viết selector. Hai bên không cạnh tranh; chúng bổ trợ cho nhau. bs4 parse HTML bạn đã cầm trong tay; API, MCP và CLI của Thunderbit giúp bạn lấy được HTML mà bình thường khó chạm tới ngay từ đầu. Nếu nút thắt của bạn là parsing, bs4 là câu trả lời ổn. Nếu nút thắt của bạn là thu thập, đó là một lớp khác.
Dùng thử Thunderbit để trích xuất dữ liệu web
Kết luận cuối cùng
BeautifulSoup cho bạn API thân thiện nhất, khả năng chịu HTML lỗi mạnh nhất, và bộ máy CSS đầy đủ nhất trong so sánh này — đổi lại là cái giá tốc độ khoảng một bậc độ lớn và footprint bộ nhớ nặng nhất. Đó là toàn bộ sự đánh đổi, nói thẳng ra như vậy. Backend mặc định html.parser là cái bẫy thật sự: nó âm thầm làm méo các bảng và danh sách chưa đóng thẻ, nên hãy truyền "lxml" hoặc "html5lib" bất cứ khi nào input của bạn có thể xấu. Thread sẽ không làm nó nhanh hơn — multiprocessing mới làm được. Và trong các vòng lặp dài, hãy gọi decompose() cho mỗi trang để các vòng tham chiếu không tích tụ.
Hai giới hạn để kết lại. Tất cả dữ liệu ở đây được đo trên một nền tảng duy nhất (macOS arm64, Python 3.14, wheel dựng sẵn), và các hệ số thời gian được tái sử dụng từ rig của selectolax (cùng bộ test, tính đến 2026-07-13) thay vì chạy lại — nên chúng thừa hưởng giới hạn nền tảng đơn đó; một setup Linux x86_64 hoặc tự biên dịch từ source có thể làm thay đổi các con số chính xác. Và không điều gì trong các kết quả này là phát hiện mới: bs4 là một thư viện 20 năm tuổi, nên mọi hành vi được kiểm thử đều hoặc đã được tài liệu hóa hoặc đã được ghi nhận công khai. Giá trị của bài này không nằm ở chuyện “phát hiện bí mật”. Nó là việc gán con số thật cho những đánh đổi mà tài liệu chỉ mô tả định tính.
Câu hỏi thường gặp
BeautifulSoup có chậm không?
Có, và là chậm có đo được. Trên một tác vụ parse cộng trích xuất, nó chạy chậm hơn khoảng 12–17 lần so với một parser C như selectolax-Lexbor khi dùng backend html.parser mặc định, và chậm hơn 10.5–14 lần với backend lxml, vì nó tạo object Python cho từng node. Điều đó quan trọng đến đâu tùy quy mô: với một trang 1 MB, đó là 232 ms so với 15 ms — gần như không đáng chú ý cho vài nghìn trang, nhưng là yếu tố quyết định với pipeline một triệu trang.
Nên dùng parser nào của BeautifulSoup — html.parser, lxml hay html5lib?
Với site phổ biến, cấu trúc tử tế, html.parser mặc định là đủ và không thêm phụ thuộc. Nhưng nó không triển khai optional-end-tags của HTML5, nên với bảng hoặc danh sách chưa đóng thẻ, nó sẽ trộn text liền kề lại với nhau mà không báo lỗi. Khi input có thể bị lỗi, viết tay, hoặc quá cũ, hãy truyền rõ "lxml" hoặc "html5lib" — cả hai đều đạt 15/15 trên ma trận HTML lỗi, trong khi html.parser chỉ đạt 12/15.
BeautifulSoup có chạy song song bằng thread được không?
Không. Việc build cây của bs4 là thuần Python và giữ GIL, nên thêm thread chỉ làm chậm hơn chứ không nhanh hơn — trong thử nghiệm, bốn thread chạy một lần parse 1 MB chậm hơn khoảng 3.9x so với một thread. Nếu muốn song song hóa bs4, hãy dùng multiprocessing (ProcessPoolExecutor). Các thư viện có lõi C như selectolax và lxml mới là bên hưởng lợi từ song song cấp thread.
BeautifulSoup xử lý HTML hỏng có tốt không?
Nhìn chung là có — trên nhiều mẫu lỗi (tag lồng sai, thiếu khung skeleton, thuộc tính không có dấu ngoặc kép, v.v.), cả ba backend đều khôi phục ổn. Điểm yếu duy nhất là html.parser mặc định với optional-end-tags: <td>/<li> chưa đóng sẽ bị lồng vào nhau thay vì đóng lại, làm hỏng text trích xuất. Chuyển sang backend lxml hoặc html5lib là hết loại lỗi đó.
BeautifulSoup so với lxml — cái nào tốt hơn?
Chúng là hai công cụ khác nhau. lxml nhanh hơn rất nhiều ở cả build cây lẫn truy vấn, và hỗ trợ XPath. BeautifulSoup bọc lxml (và các backend khác) trong một API thân thiện hơn nhiều, đồng thời có độ phủ CSS rộng hơn thông qua soupsieve. Chỉ đừng kỳ vọng backend lxml sẽ làm bs4 nhanh như lxml thật — backend chỉ tăng tốc parsing, còn truy vấn và duyệt vẫn phải trả chi phí object Python theo từng node của bs4, nên trên các batch chọn lớn nó vẫn chậm hơn khoảng 6–7.5x.
Dùng thử Thunderbit để trích xuất dữ liệu web Get Started Free


