BeautifulSoup היא הספרייה שכמעט כולם שולפים בפעם הראשונה שהם מגרדים דף אינטרנט ב־Python, ובאופן ממש ברור היא גם האיטית ביותר מבין מפרשי ה־HTML הרציניים. שני הדברים האלה נכונים, ואף אחד מהם אינו ביקורת. החלק המעניין הוא ש"האיטי ביותר" מסתבר כמספר מדויק שאפשר לשלם עליו, ולא רק תחושת בטן.
בדקתי את bs4 (כלומר beautifulsoup4, גרסה 4.15.0, שפורסמה ביוני 2026, ברישיון MIT) באמצעות שילוב של בדיקות יכולת חדשות ונתוני תזמון שנעשה בהם שימוש חוזר מאותו מערך מדידה, והתמונה עקבית: בתמורה לידידותיות API ולסבילות השגיאות הגבוהה ביותר בתחום, משלמים בערך פי עשרה בביצועים. האם זה הוגן? זה תלוי לגמרי בעומס העבודה שלך, ולכן הסקירה הזו שמה את שני הצדדים על השולחן.
מה BeautifulSoup באמת הוא — ומה לא
רוב המדריכים מדלגים על הנקודה הכי חשובה: BeautifulSoup לא מנתח HTML. הוא מעטפת. מאחורי הקלעים הוא מעביר את המסמך לאחד משלושה מנתחים אמיתיים — html.parser המובנה של Python, lxml, או html5lib — ואז עוטף את העץ שהם מייצרים בממשק ניווט וחיפוש אחד, ידידותי מאוד. התפקיד של bs4 הוא לא לנתח, אלא להפוך את התוצאה לנוחה לעבודה.
המחבר עצמו מגדיר את הספרייה כ־"screen-scraping library", וההבטחה תמיד הייתה אותה הבטחה: להפנות אותה ל־HTML כל כך שבור שאפילו דפדפן יירתע, והיא עדיין תחלץ את הנתונים שביקשת. המוניטין הזה אכן מוצדק, עם כוכבית אחת שנגיע אליה.
כמה עובדות שכדאי לקבע לפני שממשיכים:
| שדה | ערך |
|---|---|
| חבילה | beautifulsoup4 (ייבוא כ־bs4) |
| גרסה שנבדקה | 4.15.0 (הועלתה ב־2026-06-07) |
| דרישת Python | >=3.7.0 |
| רישיון | MIT |
| בית רשמי | crummy.com/software/BeautifulSoup |
| מקור + מעקב באגים | Launchpad — לא GitHub |
| תחזוקה | פעילה (4.15.0 ביוני 2026, שש גרסאות בשנה האחרונה) |
ההערה "לא GitHub" חשובה יותר ממה שנדמה. bs4 היא ספרייה בת 20 שנה שחיה על crummy.com ועל Launchpad, כך שמדד הסטארים הרגיל של GitHub פשוט לא רלוונטי כאן. את הבריאות שלה עדיף למדוד לפי קצב השחרור — ולפי המדד הזה היא חיה ובועטת.
עוד נקודה עדינה לגבי רישוי, למי שצריך להסביר את זה לצוות ציות: המעטפת עצמה היא MIT, אבל מה ש"שימוש ב־bs4" באמת מכניס לעץ התלויות שלך תלוי באיזה backend אתה מתקין. html.parser הוא חלק מספריית התקן של Python (רישיון PSF, בלי תלויות נוספות). lxml הוא BSD, אבל יושב על libxml2/libxslt — תלות C חיצונית שמקמפלים או מושכים כ־wheel בנוי מראש. html5lib הוא Python טהור וב־MIT. אם אתה רוצה footprint תלותי מינימלי ככל האפשר, html.parser המובנה נותן זאת — ובאופן לא מפתיע, הוא גם ה־backend עם המחיר הכבד ביותר. עוד מעט נגיע לזה.
מס הביצועים, במספרים
נניח את המספר על השולחן קודם, כי הוא הכותרת, והסתרתו תהיה לא ישרה. במשימת parse-then-extract ריאלית — לנתח את המחרוזת, לשלוף כל <h3 class="title"> וכל <a href> — BeautifulSoup הוא המנתח האיטי ביותר בהשוואה הזו, ובפער לא קטן.

המדידות האלה נלקחו מחדש ממערך הבדיקה של selectolax (אותו מחשב, אותה מתודולוגיית 3 ריצות, נכון ל־2026-07-13); הסקירה הזו לא מריצה מחדש שום benchmark תזמוני משלה, כדי להימנע מתחרות על ה־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) רץ בערך פי 12–17 לאט יותר ממנתח C כמו selectolax-Lexbor, והמעבר ל־lxml רק מקצר את הפער ל־פי 10.5–14 — עדיין פער של סדר גודל. הסיבה מבנית, לא תקלה: לא משנה איזה backend מבצע את הניתוח, bs4 בונה אובייקט Python מלא (Tag או NavigableString) עבור כל צומת. שכבת חומרת־האובייקטים הזו היא מס שאותו מנתחי C פשוט לא משלמים.
שימו לב שהמכפיל גדל ככל שהדפים גדלים — 12.0x ב־1 KB, מול 17.4x ב־10 MB. זה אומר שלא מדובר בתקורה קבועה של אתחול שאפשר לדלל. זהו מס פר־צומת, שגדל ליניארית עם מספר הצמתים שנבנים.
ועכשיו למסגור מחדש, כי "פי 10 איטי יותר" נשמע מפחיד יותר ממה שזה בדרך כלל. בדף של 1 MB, מדובר ב־232 ms מול 15 ms. אם העבודה שלך היא "לגרד כמה מאות עד כמה אלפי דפים, כל אחד בגודל כמה מאות KB", ההבדל האבסולוטי הזה כמעט לא מורגש — לא תרגיש אותו, ואופטימיזציה שלו לא תקנה לך כלום. אם העבודה שלך היא צינור של מיליון דפים, אותו יחס בדיוק הוא ההבדל בין עבודה שמסתיימת לכזו שלא. אותו מספר, פסק דין הפוך. תשפוט לפי היקף העבודה האמיתי, לא לפי הבנצ'מרק.
לא, החלפת backend לא פותרת את זה
יש מיתוס עקשן שאם נותנים ל־bs4 את backend של lxml, מקבלים את המהירות של lxml. זה לא עובד כך, וחשוב להבין למה. בשאילתת CSS על אצווה של 100,000 צמתים (בחירת כל <a> וקריאת ה־href שלו, כשהעץ כבר נבנה), חלוקת התפוקה ברורה מאוד:
| מנתח | Query p50 | Nodes/sec |
|---|---|---|
| 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) מגיע לכ־399,000 צמתים לשנייה — בערך פי 6.3–7.5 איטי יותר משלושת מנועי ה־C, למרות שה־backend שלו עצמו הוא lxml. ה־backend מאיץ את שלב בניית העץ. אבל השאילתה והמעבר בעץ עדיין עוברים דרך soupsieve אל אובייקטי bs4 מסוג Tag, וכל צומת שנמצא עדיין נארז ב־Python. לכן המודל המנטלי של "תן ל־bs4 lxml ותקבל lxml-speed" פשוט שגוי: ה־backend מאיץ שלב אחד, והשלב האיטי ביותר הוא לא השלב הזה.
זיכרון ואתחול קר מוסיפים לעלות הכוללת. על מסמך של 10 MB, bs4 משתמש בערך ב־פי 1.5–1.75 יותר זיכרון תושבות מ־selectolax או lxml (218–226 MB מול 129–145 MB) — אותה סיבה בסיסית, אובייקט Python אחד לכל צומת. בנוסף, ייבוא bs4 לוקח בערך 33.4 ms לעומת 14.1 ms עבור lxml.html, כלומר הוא פי 2.36 איטי יותר בייבוא. זה כמעט זניח בתהליך ארוך־חיים, אבל עבור כלי CLI או פונקציית serverless שנפתחת קר הרבה, זו עלות קטנה ואמיתית שכדאי להכיר.
למה עוד Threads לא יצילו אותך
אם האינסטינקט שלך במשימה כבדה של CPU הוא "נזרוק עליה threads", bs4 יעניש את האינסטינקט הזה. על דף של 1 MB שנפרס 48 פעמים, ריצה עם thread אחד מול ארבעה threads:
| מנתח | thread אחד | 4 threads | האצה |
|---|---|---|---|
| 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 |
קראו את השורה התחתונה פעמיים. ארבעה threads הפכו את bs4 לאיטי פי 3.9 בערך, לא למהיר יותר. האות האמפירית היא "כנראה מחזיק את ה־GIL": בניית העץ של bs4 היא Python טהור, ולכן היא מתבצעת בסידור תחת ה־Global Interpreter Lock, והוספת threads רק מוסיפה overhead של תזמון למשימה שאי אפשר באמת להריץ במקביל. ל־selectolax יש את ההאצה של פי 3.5 בערך כי הליבה שלו ב־C משחררת את הנעילה; ל־bs4 אין מרווח כזה.
בעידן ה־free-threading, המסקנה המעשית היא: אם אתה צריך להקביל את BeautifulSoup, פנה ל־multiprocessing (ProcessPoolExecutor), לא ל־threads. selectolax ו־lxml יכולים להתרחב היטב על threads; bs4 לא. הסתייגות אחת של הקפדה מדעית — זו תצפית אחת בלבד על נקודת thread אחת (4) ועל גודל דף אחד (1 MB), ומנגנון "מחזיק את ה־GIL" הוא השערה שמוסקת מהתנהגות זמן־קיר, לא משהו שאימתתי על ידי מכשור שמראה איזה נתיב קוד מחזיק את הנעילה. הכיוון ברור; המנגנון המדויק עדיין משוער.
ה־backend ברירת המחדל הוא המלכודת. קראו את זה קודם
אם תיקחו דבר אחד מהסקירה הזו, קחו את זה. BeautifulSoup(html) בלי ארגומנט שני משתמש ב־html.parser, ו־html.parser לא מיישם את חוקי optional-end-tag של HTML5. זה נשמע אקדמי עד שזה הורס לכם נתונים בשקט.

הרצתי 15 דוגמאות HTML מכוונות־שבירה דרך כל שלושת ה־backends, עם טענת מבנה אגנוסטית ל־backend שנרשמה מראש לכל אחת לפני ההרצה (כדי שאף אחד לא יוכל לבחור את המנצח בדיעבד). הציונים:
| Backend | עומד בציפייה / 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>a<li>b<li>c נותן ['abc','bc','c'] מקונן תחת html.parser, ו־['a','b','c'] נקי תחת שני האחרים. גם תכונות כפולות מתהפכות — <div id="first" id="second"> שומר את "second" תחת html.parser אבל "first" תחת lxml/html5lib, וה־HTML5 spec אומר לשמור את הראשונה.
למה זה מסוכן ולא רק מעצבן: כי זה קורה בלי שגיאה. scraper שעושה פשוט BeautifulSoup(html) ונופל על טבלת או רשימת <li> שלא נסגרו — דבר שמאוד נפוץ באתרי ישנים, ב־HTML שנכתב ידנית, ובתבניות ששכחו בהן תג סגירה — ידביק טקסט של תאים סמוכים יחד לאותו שדה, יחזיר לכם נתונים מלוכלכים, ולא יתלונן אפילו פעם אחת. הפתרון הוא ארגומנט אחד: BeautifulSoup(html, "lxml") או BeautifulSoup(html, "html5lib").
למען ההגינות כלפי html.parser, ב־12 מתוך 15 הדוגמאות השבורות האחרות התוצאה הייתה זהה בין כל שלושת ה־backends — תגים מקוננים לא נכון כמו <b><i></b></i>, חסרון של שלד html/body, תכונות בלי מירכאות, תגי סגירה יתומים, הערות שלא נסגרו, טפסים מקוננים, אותיות גדולות/קטנות מעורבות, ועוד. הסבילות של bs4 באמת חזקה מאוד ברוב המקרים; הפער מרוכז כמעט כולו במשפחת ה־optional-end-tag. ושום דבר מזה אינו גילוי חדש — התיעוד של bs4 עצמו על "Differences between parsers" כבר אומר ש־html.parser הוא "less lenient" במפורש. מה שמטריצת ה־malformed מוסיפה הוא המקרים הספציפיים, שאפשר לשחזר, שבהם "פחות סלחני" הופך לפלט שגוי.
מה לא מוותרים עליו: ה־API וה־CSS הם החלק הטוב ביותר
אז bs4 איטי, חד־תהליכי, ויש לו מלכודת ב־backend ברירת המחדל. אנשים עדיין בוחרים בו, כי החלק ה"ידידותי" של הטרייד־אוף הוא לגמרי אמיתי — והוא מחזיק יפה בבדיקות.

הרצתי 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> מחזיר מחרוזת ריקה "" עבור disabled ב־bs4 (ב־selectolax מתקבל None). בשני המקרים הערך שקרי, ולכן if node.get("disabled") מפספס בשקט תכונה בוליאנית שבאמת קיימת — בכל אחת מהספריות. הבדיקה הבטוחה היא "disabled" in tag.attrs. שנית, get_text(strip=True) מחבר טקסט צמתים בלי מפריד אחרי ה־strip, כך ש־"...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, כולל selectors ש־Lexbor דוחה לגמרי: :lang(en), ה־soupsieve-only :-soup-contains('featured'), :is(), :where(), ו־:has(> a). הפערים האמיתיים היחידים הם XPath (כי soupsieve תומך רק ב־CSS) וה־pseudo-elements של parsel כמו ::text / ::attr(), שהם הרחבות של Scrapy. אם אתם חיים ב־XPath, המעבר הזה יכאב.
המסקנה של הסעיף פשוטה: מה שמקריבים כשבוחרים ב־BeautifulSoup הוא מהירות. לא ארגונומיית API, ובוודאי לא כיסוי CSS.
שתי תקלות ייצור שכדאי לתקצב מראש
מעבר ל־backend ברירת המחדל, יש עוד שתי התנהגויות שיפגעו בכם במיוחד בעומסים ארוכי־טווח או בעבודות שאינן UTF-8.
לולאות הפניה: קראו ל־decompose() בלולאות ארוכות
כל Tag ב־bs4 מחזיק הפניה להורה שלו וגם לילדיו, וכך נוצר מעגל הפניות. מנגנון ה־reference counting של CPython לא יכול לשחרר מעגל כזה לבדו — זה תפקידו של ה־generational garbage collector. כדי לראות כמה זה משפיע, בניתי ומחקתי עץ 300 פעמים כשה־GC כבוי, ואז ספרתי כמה אובייקטי Tag עדיין נשארו בזיכרון:

| תרחיש | Tags שנשארו אחרי del |
|---|---|
| GC כבוי | 120,900 (300 מחזורים, שום דבר לא שוחרר) |
| GC פעיל | 26,598 (ה־GC הגנרטיבי הופעל באמצע הלולאה) |
אחרי gc.collect() מאולץ | 0 (הכול שוחרר) |
| קבוצת בקרה בלי מחזור (רשימת מחרוזות, GC כבוי) | הפרש 0 |
כשה־GC כבוי, del soup לא שחרר כלום — כל 120,900 האובייקטים נשארו בזיכרון, משום שמעגל ההפניות שובר את ה־reference counting. קריאה אחת ל־gc.collect() ניקתה את כולם. קבוצת הבקרה ללא מחזור (רשימת מחרוזות פשוטה, הידועה ככזו שאין בה מחזור) הראתה delta של אפס, מה שמוכיח שההצטברות נבעה מהמעגל של bs4 ולא מרעש מדידה. התיעוד של bs4 עצמו אומר שהאובייקטים הם "densely interconnected ... exactly the sort a garbage collector would have trouble with", כך שזהו התנהגות מתועדת; מה שהבדיקה מוסיפה הוא ספירת האובייקטים שנותרו וההוכחה ש־collect() מאפס את הכול.
הכלל המעשי: בצינור שמעבד הרבה דפים גדולים בלולאה הדוקה, אם הקוד שלך (או איזו הגדרת throughput גבוהה) מכבה GC או לא מפעיל אותו מספיק, עצי bs4 יישארו, והזיכרון יעלה. קראו ל־soup.decompose() אחרי כל דף — bs4 מספקת את זה בדיוק כדי לשבור את המעגל ולשחרר מוקדם. לעצי C של selectolax ו־lxml אין את הבעיה הזו בכלל.
קידוד: UnicodeDammit הוא היתרון השקט של bs4
bs4 מגיעה עם רכיב שמנתחים מהירים לא כוללים: UnicodeDammit, שמנסה לזהות את קידוד המסמך וממיר אותו אוטומטית ל־Unicode. נתתי לו מטריצה בת 8 מקרים של "הקידוד המוצהר מול הקידוד האמיתי":

| מקרה | קידוד אמיתי | מה 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 | לא (הלך לפי השקר) |
חמישה מתוך שמונה שוחזרו. UTF-8, UTF-16 עם BOM, GBK, Shift-JIS, ואפילו latin-1 שסומן לא נכון — כולם חזרו נכון, והניחושים כתת־קבוצה (GBK→gb18030, Shift-JIS→cp932) עדיין מפוענחים היטב. שתי שגיאות שחשוב להכיר: דגימות קצרות של latin-1/cp1252 מזוהות בטעות כקודי DOS, כי הזיהוי הסטטיסטי אינו אמין על קלט קצר וסימני box-drawing של DOS חופפים לנקודות הקוד של Latin-1; וכאשר הצהרת <meta charset> פשוט שגויה, UnicodeDammit מאמין לה. התיעוד של bs4 מדגיש את שני הדברים — דגימה יכולה להיות "so short that Unicode, Dammit can't get a lock on it", וככל שיש יותר נתונים כך הניחוש טוב יותר.
לעומת selectolax, שפשוט עלול להשחית בשקט בתים שאינם UTF-8 ומצפה שתפענחו אותם בעצמכם, זהו יתרון אמיתי: bs4 לפחות מנסה לזהות קידוד ולעיתים קרובות מצליחה. אבל זו לא הבטחה. עבור קידוד ידוע, אל תנחשו — היו מפורשים: BeautifulSoup(bytes, from_encoding="...").
האם ה־backends באמת חולקים דעה על דפים אמיתיים?
מטריצת ה־malformed מראה שה־backends נבדלים על קלט שבור במכוון. השאלה הבאה היא האם זה בכלל משנה בעולם האמיתי, אז הרצתי את כל שלושת ה־backends על 11 דפים אמיתיים שנשלפו — BBC, Wikipedia, Craigslist, MDN, old.reddit, תיעוד Python, Hacker News, Books to Scrape, webscraper.io, whitehouse.gov, ודף quotes שמרונדר ב־JS — והשוויתי ספירות של קישורים, כותרות ותמונות.
כל השלושה הסכימו בכל 11 הדפים. אפס סטייה. כלומר, חוסר ההסכמה מהסעיף על המלכודת מופיע רק על HTML שבור במכוון; כשאתר ייצור מודרני בנוי מספיק טוב — אפילו אם הוא "מבולגן" — בחירת ה־backend לא משנה מה תחלצו ממנו. הקריאה המעשית: באתרים מיינסטרימיים ובעלי מבנה תקין, html.parser מצוין וחוסך לכם תלות. רק כשמגרדים HTML לא סטנדרטי, ידני, או עתיק בצורה נראית לעין, בחירת ה־backend מתחילה להזיז את התוצאות — ואז עוברים ל־lxml או html5lib.
עוד נקודת צד מההרצה ההיא, כי זה מקרה קצה אמיתי. דף MDN כולל אלמנט <template>, וכל ה־backends של bs4 החזירו 508 קישורים — כלומר bs4 משטח את תוכן <template> לתוך העץ הראשי. זה שם את bs4 באותו צד כמו lxml, ובצד ההפוך ל־selectolax-Lexbor, שמיישם בקפדנות את מפרט HTML5 (<template> הוא DocumentFragment אינרטי) ומחזיר 497, תוך שהוא מפיל בשקט את 11 הקישורים שבתוך ה־template. לכן bs4 תתפוס נתונים בתוך <template> — שימושי, אבל גם דרך לקלוט "תוכן פנטום" שדפדפן לעולם לא ירנדר. אף אחת מההתנהגויות אינה שגויה; אלו פרשנויות שונות למפרט, וחשוב לדעת מה אתם מקבלים.
איפה BeautifulSoup מתאים — ואיפה לא
במקום למחוץ את כל זה לציון אחד של 0–100, שיחביא בדיוק את הטרייד־אופים החשובים, הנה כרטיס ניקוד לפי ממדים, עם הסתייגות לכל שורה:
| ממד | מה שהבדיקות גילו | הסתייגות לקורא |
|---|---|---|
| התקנה / ריצה ראשונה | מעטפת טהורה, בלי browser/setup; ל־html.parser אין תלות נוספת; כל ה־wheels בנויים מראש | ל־lxml backend נדרשת תלות C |
| מהירות מול מנתחי C | איטי פי 12–17 (html.parser) / פי 10.5–14 (lxml backend), בכל הגדלים | מערך בדיקה יחיד; נתוני selectolax נעשה בהם שימוש חוזר |
| תפוקת שאילתות CSS | איטי בערך פי 6–7.5 על 100k צמתים; ה־lxml backend לא מציל | נעשה שימוש חוזר; משלם את מס ה־Python Tag |
| זיכרון | פי 1.5–1.75 מ־selectolax/lxml; הכבד ביותר | נעשה שימוש חוזר; נמדד לפי RSS |
| אתחול קר | פי 2.36 איטי יותר (33.4 מול 14.1 ms) | נעשה שימוש חוזר; פריט קטן |
| סקיילינג עם threads | bs4-lxml איטי בכ־פי 3.9 ב־4 threads (מחזיק GIL) | תצפית יחידה; השתמשו ב־multiprocessing |
| ארגונומיית API | 29/29 בדיקות; find עם פרדיקט פונקציונלי + ניווט דו־כיווני | מלכודות של תכונת־בוליאנית כמחרוזת ריקה וגבולות מילים ב־strip |
| כיסוי CSS | soupsieve הוא החזק ביותר: 41/41 בסיסי + 20/20 מורחב; תומך ב־:lang | בלי XPath, בלי ::text |
| סבילות בשלושה backends | lxml/html5lib 15/15; html.parser 12/15 | הפער רק ב־HTML שבור |
| עקביות בדפים אמיתיים | שלושת ה־backends הסכימו 11/11; כולם משטחים <template> (508) | באתרים תקינים: ה־backend לא משנה |
| GC של לולאות הפניה | העץ הוא מחזור; 300 לולאות השאירו 120,900 אובייקטים, ו־collect איפס זאת | בלולאות ארוכות צריך decompose() |
| קידוד | UnicodeDammit משחזר 5/8; שוגה בדגימות קצרות, מציית להצהרות שגויות | תצפית יחידה |
| תחזוקה | פעילה (4.15.0, יוני 2026); MIT | הבית על crummy/Launchpad, לא GitHub |
אז למי BeautifulSoup מיועד? לכל מי שמעריך API קריא וניתוח סלחני על פני תפוקה גולמית, בעבודה בהיקף בינוני — פרוטוטייפים, גרידות חד־פעמיות, כלים פנימיים, וצוותים שבהם זמן מפתח שווה יותר מזמן ריצה. מי צריך לחפש משהו אחר? צינורות של מיליון דפים שבהם מס הביצועים מצטבר לכסף אמיתי, עומסים שצריכים parallelism ברמת thread, וכל מי שחי ב־XPath.
הערה חשובה על המקום של זה בתוך סטאק scraping אמיתי, ואיפה הכלי שלנו נכנס. BeautifulSoup מניח שכבר יש לכם את ה־HTML. הוא לא מביא דפים, הוא לא מרנדר JavaScript, והוא לא מטפל בהגנות נגד בוטים או ב־CAPTCHA — זו משימה נפרדת לגמרי, וקשה באמת ברשת המודרנית. כאן נמצא API של scraping בשכבה אחרת: סטאק המפתחים של Thunderbit — REST API, שרת MCP ו־CLI — מטפל בשליפה, ברינדור JS ובבעיית anti-bot, ואז מחזיר Markdown נקי (POST /distill) או JSON מובנה שמתאים לסכימה (POST /extract) בלי שתכתבו selectors בכלל. השניים אינם מתחרים; הם משלימים. bs4 מנתח HTML שכבר יש לכם; ה־API, MCP ו־CLI של Thunderbit מביאים לכם את ה־HTML שלא קל להגיע אליו מלכתחילה. אם צוואר הבקבוק שלכם הוא parsing, bs4 היא תשובה טובה. אם צוואר הבקבוק הוא השגה, זו שכבה אחרת.
נסו את Thunderbit לחילוץ נתוני רשת
השורה התחתונה
BeautifulSoup נותנת לכם את ה־API הידידותי ביותר, את הסבילות הגבוהה ביותר ל־HTML שבור, ואת מנוע ה־CSS השלם ביותר בהשוואה הזו — במחיר של בערך פי עשרה בביצועים ובטביעת זיכרון כבדה יותר מכל השאר. זה כל הטרייד, מנוסח בפשטות. backend ברירת המחדל html.parser הוא המלכודת האמיתית היחידה: הוא מעוות בשקט טבלאות ורשימות שלא נסגרו, לכן העבירו "lxml" או "html5lib" בכל פעם שהקלט עלול להיות מכוער. threads לא יאיצו אותו — multiprocessing כן. ובלולאות ארוכות, קראו ל־decompose() לכל דף כדי למנוע מהמחזוריות להיערם.
שתי מגבלות לסיום. כל מה שכאן נמדד על פלטפורמה אחת בלבד (macOS arm64, Python 3.14, wheels בנויים מראש), ומכפילי התזמון נעשו בהם שימוש חוזר ממערך הבדיקה של selectolax (אותו bench, נכון ל־2026-07-13) ולא הורצו מחדש — כך שהם יורשים את מגבלת הפלטפורמה הזו, והגדרת Linux x86_64 או קימפול מקורית עשויים להזיז את המספרים המדויקים. ושום דבר מהתוצאות האלה אינו גילוי חדש: bs4 היא ספרייה בת 20 שנה, כך שכל התנהגות שנבדקה כאן מתועדת או קיימת ברשומות ציבוריות. הערך אינו סקופ. הוא לשים מספר אמיתי על טרייד־אופים שהתיעוד רק מתאר באופן איכותי.
שאלות נפוצות
האם BeautifulSoup איטי?
כן, ובאופן מדיד. במשימת parse-plus-extract הוא רץ בערך פי 12–17 לאט יותר ממנתח C כמו selectolax-Lexbor עם backend ברירת המחדל html.parser, ופי 10.5–14 לאט יותר עם backend של lxml, כי הוא בונה אובייקט Python לכל צומת. האם זה משנה? זה תלוי בהיקף: בדף של 1 MB מדובר ב־232 ms מול 15 ms — זניח בכמה אלפי דפים, אבל מכריע בצינור של מיליון דפים.
באיזה מנתח של BeautifulSoup כדאי להשתמש — html.parser, lxml, או html5lib?
באתרים מיינסטרימיים ובעלי מבנה תקין, html.parser המובנה בסדר גמור ואינו מוסיף תלות. אבל הוא לא מיישם את optional-end-tags של HTML5, כך שעל טבלאות או רשימות שלא נסגרו הוא ידביק טקסט סמוך יחד בלי להציג שגיאה. כשקלט יכול להיות שבור, ידני או ישן, העבירו במפורש "lxml" או "html5lib" — שניהם קיבלו 15/15 נקי במטריצת HTML שבור, מול 12/15 של html.parser.
האם BeautifulSoup יכול לעבד במקביל עם threads?
לא. בניית העץ של bs4 היא Python טהור ומחזיקה את ה־GIL, ולכן הוספת threads הופכת אותו לאיטי יותר, לא למהיר יותר — בבדיקה, ארבעה threads הריצו parse של 1 MB בערך פי 3.9 לאט יותר מאשר thread אחד. כדי להקביל bs4, השתמשו ב־multiprocessing (ProcessPoolExecutor). ספריות עם ליבות C, כמו selectolax ו־lxml, הן אלה שמרוויחות מקביליות ברמת threads.
האם BeautifulSoup מטפל היטב ב־HTML שבור?
בגדול כן — על פני מגוון דוגמאות שבורות (תגים מקוננים לא נכון, שלדים חסרים, תכונות בלי מרכאות ועוד), כל שלושת ה־backends התאוששו יפה. נקודת החולשה היחידה היא html.parser כברירת מחדל ו־optional-end-tags: <td>/<li> שלא נסגרו מקוננים במקום להיסגר, והטקסט שנשלף נפגם. עברו ל־backend של lxml או html5lib וסוג הבעיה הזה נעלם.
BeautifulSoup מול lxml — מה עדיף?
אלה כלים שונים. lxml מהיר בהרבה הן בבניית העץ והן בשאילתות, ותומך ב־XPath. BeautifulSoup עוטף את lxml (ועוד) ב־API הרבה יותר ידידותי, ויש לו בפועל כיסוי CSS רחב יותר דרך soupsieve. רק אל תצפו שה־backend של lxml יהפוך את bs4 למהיר כמו lxml — ה־backend מאיץ רק את ה־parsing, בעוד ששאילתות והליכה בעץ עדיין משלמות את מס האובייקטים של bs4 לכל צומת, כך שהן נשארות בערך פי 6–7.5 איטיות יותר בבחירות אצווה גדולות.
נסו את Thunderbit לחילוץ נתוני רשת Get Started Free


