lxml, בבדיקה: מנוע ה-XPath שעדיין גובר על כל מנתח Python אחר

עודכן לאחרונה ב- July 17, 2026
lxml, בבדיקה: מנוע ה-XPath שעדיין גובר על כל מנתח Python אחר
סיכום AI
סקירה זו של lxml מציגה את הספרייה כ-binding ותיק ל-libxml2 ול-libxslt, עם יתרון מרכזי שעדיין קשה למצוא במנתחים חדשים יותר: מנוע XPath אמיתי. המאמר בודק כיסוי XPath, מצבי קשיחות של המנתח, התנהגות זיכרון בסטרימינג, ההבדל בין CSS ל-XPath, ומגבלות העומק של libxml2. הוא מראה ש-lxml מהיר, חסכוני בזיכרון, ויוצא דופן ביכולותיו עבור עומסי XML ו-HTML שדורשים axes, predicates, פונקציות, סטרימינג או מצבי התאוששות חזקים. הסקירה גם מסבירה את ברירות המחדל הבטוחות סביב עומק העץ ומתי huge_tree משנה את הגבול הזה.

כל כמה חודשים צץ מנתח HTML מהיר יותר, הבנצ'מרקים יוצאים, ומישהו מכריז שהדור הישן כבר לא רלוונטי. ואז צריך לבחור כל פסקה שמכילה מילה מסוימת, או לשלוף את ההורה של צומת תואם, ונזכרים למה lxml עדיין פתוח בטאב השני.

lxml הוא binding ותיק בן 20 שנה ל-libxml2. הוא לא נוצץ. הוא לא חדש. ולמשימה אחת מאוד מסוימת — כל דבר שדורש XPath אמיתי — כמעט שום דבר אחר ב-Python המיינסטרימי לא באמת מתחרה בו. זו סקירה מעשית על מה הוא עושה, איפה הוא מנצח בשקט, ואילו שני מקומות בברירות המחדל שלו ינשכו אותך אם לא תדע שהם שם.

lxml בפסקה אחת: מה הוא בעצם

lxml הוא binding של Python לספריות C בשם libxml2 ו-libxslt. הוא מנוע ניתוח וסריאליזציה, לא סקרייפר ולא דפדפן — הוא הופך markup לעץ שאפשר לשאול ולערוך, ואז מחזיר את העץ לבייטים. הוא מספק API תואם ElementTree, מנוע מלא של XPath 1.0, ‏XSLT 1.0, ואימות סכמות, ומתוחזק על ידי Stefan Behnel תחת הסיסמה "הספרייה העשירה ביותר בתכונות והקלה ביותר לשימוש לעיבוד XML ו-HTML בשפת Python".

כך הוא נראה, לפי snapshot מ-GitHub ומ-PyPI שנמשכו ב-2026-07-14:

שדהערך
מאגרlxml/lxml
כוכבים3,043
Forks620
בעיות פתוחות16
רישיוןBSD-3-Clause
נוצר2011-02-11
דחיפה אחרונה2026-07-02
גרסה יציבה ב-PyPI6.1.1 (2026-05-18)
מנוע מובנהlibxml2 2.14.6 + libxslt 1.1.43

דבר אחד שכדאי לסגור כבר עכשיו לפני שמישהו יחשוד בי בהייפ: אין כאן סודות. lxml ותיק מספיק כדי שכל התנהגות שמוזכרת כאן תופיע איפשהו ב-תיעוד של lxml, ביומן השינויים של libxml2, או בשרשור launchpad. לא מצאתי טריק בלעדי ולא מתועד, וגם לא אמציא אחד. הערך של מה שמגיע כאן הוא בכך שהוא ממוסד, כמותי ומאורגן סביב lxml עצמו — לא בכך שהוא חדשותי.

סביבת הבדיקה (ולמה מספרי הזמנים מושאלים)

לסקירה הזו נכנסות שתי קטגוריות של נתונים, והן מגיעות משני מקומות שונים — אז חשוב להיות שקוף לגביהן.

בדיקות היכולות — התנהגות XPath, שני ממשקי ה-parser, namespaces, קידוד, מחזור חיים של צמתים — רצתי מחדש על מכונה אחת: macOS arm64, Python 3.14.2, lxml 6.1.1, ‏libxml2 2.14.6. כל מספר בקבצי artifacts/raw/*.json מחושב מהרצה של סקריפט, לא הוקלד ידנית. בדיקות יכולת הן בוליאנים ו-enums דטרמיניסטיים, ולכן הרצה אחת יציבה — עומס המכונה לא משנה אם //a/@href מחזיר מחרוזת של תכונה.

מספרי הזמן וטביעת הרגל בזיכרון אינם מהאוסף הזה. הם ממוחזרים מילה במילה מחבילת הבנצ'מרקים הקודמת של selectolax — אותה מכונה, אותה סביבה וירטואלית, אותו build של lxml ו-libxml2, בנצ'מרקים נכון ל-2026-07-13 — ולא הרצתי אותם מחדש כאן. זה מכוון. הרצת בנצ'מרקי זמן במקביל לאוסף סקריפטים של יכולת מזמינה תחרות על CPU, שעלולה לזהם את המספרים הממוחזרים, וזה גם היה כפילות עבודה: lxml כבר שימש שם כספריית בקרה שנמדדה במלואה. שימוש חוזר באותם בנצ'ים שומר על apples-to-apples במקום להכניס סט מדידה שני, מעט שונה. לכן כשאתם רואים למטה מספר במילישניות, קראו אותו כ-"אותה סביבת בדיקה, נכון ל-2026-07-13", לא כ-"מדדתי את זה מחדש היום".

לכל ממצא יש תג ביטחון: single-observation לבדיקות היכולת הדטרמיניסטיות, triple-run להתפלגויות הזמן הממוחזרות, hypothesis כשאני מציע מנגנון שלא בודדתי.

XPath: הדבר האחד שאין ל-selectolax ול-BeautifulSoup

זה הכותרת הראשית, אז נתחיל כאן.

lxml XPath coverage moat with axes predicates and functions

הרצתי את xpath() של lxml דרך מטריצה של 37 פריטים שהוגדרה מראש — התוצאה הצפויה לכל מקרה נכתבה לקוד לפני שהבדיקה רצה, כך שלא יכולתי בטעות לצפות בציון מרוכך. עשרה axes, תשעה סגנונות predicate, עשר פונקציות מובנות, שלושה סוגי החזר סקלריים, וחמישה מקרי מלכודת מכוונים שהשתמשו בתחביר XPath 2.0 בלבד, שאמור להידחות על ידי מנוע 1.0 של lxml.

קטגוריהכיסויתוצאה
Axeschild / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding10/10 עבר
Predicates[1] / last() / position()<n / שוויון תכונה / קיום תכונה / and / or / [.//a] מקונן / not()9/9 עבר
פונקציותtext() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string()10/10 עבר
סוגי החזרהבוליאני / סקלרים מספריים3/3 עבר
מקרי מלכודתmatches() / רצפים / if-then-else / except / שגיאת תחביר5/5 נדחו כראוי

הציון הוא 37/37, וחלק ה-trap הוא החשוב. matches(), ביטויי רצף, if/then/else ו-except הם כולם תחביר XPath 2.0, ומנוע 1.0 של libxml2 לא תומך בהם רק חצי-חצי — הוא מרים XPathEvalError ומסרב, במקום להחזיר בשקט קבוצת צמתים שגויה. אז זה ציון מושלם אחרי שניסיתי לשבור אותו, לא ציון מושלם שהורכב משאלות רכות. כל ההתנהגות כאן תואמת בדיוק למה שמתאר התיעוד של XPath ב-lxml, וזו בדיוק הנקודה.

אודה על דבר אחד שה-harness טעה בו, כי זו הגרסה של "37/37" שאפשר באמת לסמוך עליה. הסט הצפוי הראשון שלי עבור //div[.//a[@href]] חזה שני התאמות; הריצה החזירה אחת. הנחתי ש-lxml טועה בערך שלושים שניות, ואז בדקתי את ה-fixture וגיליתי שהאלמנט השני היה <footer>, לא <div> — הציפייה שלי הייתה שגויה, לא המנוע. תיקנתי את הסט הצפוי והשארתי את הטעות כהערת מקור. זה סדר האשמה הנכון: קודם לחשוד בבדיקה שלך, ורק אחר כך בספריית C בת 20 שנה.

XPath מול CSS: מה אי אפשר לנסח בכלל ב-CSS

הטענה המופשטת ש-XPath חזק יותר ראויה למספר קונקרטי, אז כימותי את הפער. ל-lxml יש גם .xpath() וגם .cssselect() (האחרון מתרגם CSS ל-XPath מאחורי הקלעים). לקחתי עשר מטרות בחירה ובדקתי אילו מהן CSS באמת יודע לבטא.

XPath expresses seven of ten tasks CSS cannot express

מטרהXPathCSS (cssselect)
סינון לפי תוכן טקסט (contains(text(),"bargain"))כןאין predicate לטקסט
בחירת הורה מתוך ילד (//b/parent::p)כןאין parent selector
החזרת ערך של תכונה (//a/@href)כןרק elements
החזרת צומת טקסט (//p/text())כןאין text nodes
ancestor axis (//td/ancestor::div)כןאין ניווט כלפי מעלה
סינון הורה לפי מספר ילדים (//ul[count(li)=4])כןאין predicate של count
סינון לפי אורך טקסט (string-length(text())>5)כןאין predicate של length
nth-child / last-child / sibling סמוךכןכן (3 בסיסיים)

שבע מתוך עשר המטרות פשוט אין להן מקבילה ב-CSS. סינון לפי תוכן טקסט, ניווט כלפי מעלה להורים ולאבות, שליפת ערך תכונה או צומת טקסט גולמי כתוצאה, predicates שמבוססים על ספירה — CSS לא יודע לבטא אף אחד מהם. רק שלושה (nth-child, ‏last-child, וסמוך-אח) עובדים בשניהם. זו התשובה הכמותית לשאלה "מה אני באמת מרוויח כשאני ניגש ל-lxml". ל-selectolax יש CSS בלבד ואין בכלל xpath(), אז שבעת סוגי השאילתות האלה הופכים שם או ללולאות Python בכמה שלבים, או פשוט לא קורים. אם הלוגיקה של הסקרייפינג שלכם נשענת על אחד מהם, זו כבר ההחלטה שלכם.

(וכן, ה-harness תפס אותי שוב כאן: חזיתי סט ריק עבור string-length(text())>5, אבל שתי מחרוזות באורך שש תווים התאימו. תיקנתי את הציפייה, לא את הכלי.)

שלושה הילוכים של קשיחות: etree מול recover מול lxml.html

XPath הוא הסיבה לבחור ב-lxml. בקרת הקשיחות בשלושה מצבים היא הסיבה להשאיר אותו.

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

רוב המנתחים נותנים התנהגות אחת לקלט שבור. lxml נותן שלוש, והן מספיק צפויות כדי שרצתי שישה סוגי markup שבור מול כל אחת מהן והגדרתי מראש איך כל נתיב אמור להתנהג.

קלט שבורlxml.etree (מחמיר)etree + recover=Truelxml.html (סלחני)
תג סגור-למחצה <root><a>x</root>זורקמתקןמקבל
קינון שגוי <b><i></b></i>זורקמתקןמקבל
ישות לא מוגדרת &nbsp;זורקמתקןמקבל
אמפרסנד בודד & (Tom & Jerry)זורקמתקןמקבל
כמה שורשים <a>1</a><b>2</b>זורקמתקןמקבל
XML תקיןמקבלמקבל (0 שגיאות)מקבל
תכונת בוליאנית <input disabled>זורקמתקןמקבל

שבעה מתוך שבעה תואמים לציפייה שהוגדרה מראש. lxml.etree מרים XMLSyntaxError בכל שש הקטגוריות השבורות. הוסיפו recover=True לאותו parser והוא בולע את השגיאות ובונה מחדש עץ שמיש — והחלק המוערך כאן הוא ש-parser.error_log אז מפרט כל שגיאה שנבלעה. lxml.html מקבל הכול בלי להתלונן.

המסווג שמחליט "זרק מול תוקן מול התקבל" מונע בעצמו על ידי אורך ה-error_log בזמן ריצה, לא על ידי ערך קשיח, ולכן מסמך תקין שרץ תחת recover=True מסומן נכון כ-"מקבל" (יומן ריק) ולא כ-"מתקן." הגרסה הראשונה שלי של המסווג סימנה כל תוצאה עם recover=True כ-"מתקן" וסימנה לא נכון את הקלט התקין; קריאת ה-error_log האמיתי תיקן את זה.

מה זה נותן בפועל: כשאתם חייבים ולידציה מחמירה על פיד שבור — השתמשו ב-lxml.etree. כשיש HTML מלוכלך מהעולם האמיתי ורק צריך לעבור דרכו — השתמשו ב-lxml.html. ובמקרה האמצעי שרוב הכלים לא יודעים לעשות — "להיות סלחני, אבל להגיד לי בדיוק מה נשבר כדי שאוכל לרשום לוג" — השתמשו ב-recover=True וקראו את ה-error log. ל-selectolax יש את ההילוך הסלחני, וזהו, בלי מצב מחמיר ובלי error log.

iterparse: הילוך הסטרימינג שאין בכלל ל-selectolax

זו יכולת, לא כפתור מהירות. selectolax בולע רק מחרוזת שלמה — אין לו ממשק הדרגתי. ‏iterparse של lxml פולט אלמנטים כשהם נסגרים, ובשילוב עם דפוס fast_iter הקלאסי (קריאה ל-elem.clear() ומחיקת אחים קודמים תוך כדי) הוא שומר על זיכרון שטוח לא משנה כמה גדול המסמך.

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

מדדתי את מאפיין הזיכרון ישירות — RSS שיא דרך ru_maxrss, כל נושא בתהליך חדש ונקי משלו, על 300,000 אלמנטים מסוג <record> במשקל כולל של כ-15 MB.

מצבתוספת RSS בשיאהערות
iterparse + clear (fast_iter)~1-2 MBמשתחרר תוך כדי; שטוח בלי קשר לכמות
iterparse בלי clear~386 MBשומר references; כבד כמו טעינה מלאה
etree.parse (טעינה מלאה, עוגן)~386 MBכבד ידוע; מוכיח שהמדד קורא סדר גודל

המצב המוגבל שומר על תוספת RSS בשיא סביב 1-2 MB מול 386 MB בטעינה מלאה — פער סדר גודל של 0.3-0.4% — והאירוע של הרשומה הראשונה מגיע לפני שהקובץ אפילו סיים להיטען, כך שזה סטרימינג אמיתי, לא סטרימינג מזויף. הקו הממחיש הוא האמצעי. הריצו את אותו לולאת iterparse אבל דלגו על clear(), והזיכרון שוב מטפס לכ386 MB, כי אתם מחזיקים references לכל דבר. הניצחון יושב ב-clear(), לא ב-iterparse לבדו. נקודת העוגן של טעינה מלאה, שקוראת הרבה יותר גבוה מהמצב המוגבל, גם מאשרת שמד ה-RSS באמת רואה את הפער בגודל ולא קורא בחושך. (זו בדיקת זיכרון שרצתי בחבילה הזו — זו מדידת footprint, נפרדת ממספרי הזמן הממוחזרים.)

הגרסה מהעולם האמיתי: לייצוא XML רב-ג׳יגבייט שלא נכנס ל-RAM אין בכלל נתיב ב-selectolax. זה parser הסטרימינג של lxml או שפה אחרת.

Namespaces: RSS, SVG, ומלכודת ה-Default Namespace

שתים-עשרה תצורות namespace, כולל RSS על פני שלושה namespaces, ‏SVG עם namespace ברירת מחדל בתוספת xlink, ו-XML עם default namespace. כל השתים-עשרה עברו.

lxml שולף מ-//dc:creator/text() בפיד RSS בדיוק את ["Alice", "Bob"], פותר //atom:link/@href ו-//content:encoded על פני שלושה namespaces נפרדים באותו מסמך, מטפל ב-//s:rect וב-//s:use/@xlink:href ב-namespace השני של SVG, מפצל שמות בסגנון Clark notation ‏{uri}local עם QName, ומבצע introspection דרך nsmap. זו התנהגות מתוחזקת ומתועדת, וזו ממש שכבה שלמה ש-selectolax לא נוגע בה, כי selectolax הוא HTML5 בלבד ולא מעבד namespaces שרירותיים של XML.

יש מלכודת אחת מתועדת שכדאי לזכור. ל-XPath אין מושג של default namespace. אם תצביעו //book על מסמך שמכריז xmlns="urn:...", תקבלו אפס התאמות — הקידומת הריקה אינה מוגדרת ב-XPath, כפי ש-תיעוד lxml מסביר. צריך לקשור קידומת מלאכותית (//c:book עם namespaces={"c": "urn:..."}, שמצאה את כל השלוש) או לעבור ל-//*[local-name()='book'] (גם שלוש). לא באג — זו спецификация של XPath, מיושמת נאמנה. פשוט דבר שמפתיע את כולם בדיוק פעם אחת.

דפים מלוכלכים באמת: נאמנות על 11 סריקות אמיתיות

בדיקות סינתטיות הן נקיות; הרשת לא. השתמשתי מחדש ב-11 דפים אמיתיים שנתפסו מחבילת ה-fixtures של selectolax (נכון ל-2026-07-10, לקריאה בלבד) והרצתי עליהם את lxml.html כש-lxml הוא נושא הבדיקה.

Fixtureגודלקישוריםשגיאות libxml2 שתוקנוStrict 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, וספירות הקישורים, הכותרות והתמונות תואמות לספירות lxml הממוחזרות מחבילת selectolax בכל אחד מה-11 — בדיקת צולבת true. ההסכמה הזו היא מה שאומר לי שהשימוש החוזר באמת apples-to-apples ולא שני מדדים שונים עם אותה תווית.

הממצא המשני: המנתח המחמיר של XML זרק על עשרה מתוך אחד-עשר הדפים. דפי רשת אמיתיים הם ברובם המכריע לא XML תקין, וזו בדיוק הסיבה שמצב ה-recovery של libxml2 ל-HTML קיים כדי לבלוע אותם. החריג היחיד היה BBC News, שנרנדר ב-Next.js והיה תקין מספיק כדי לעבור ניתוח XML מחמיר. לא כל מה שמסומן "HTML" חייב את מסלול ההתאוששות.

עוד הערת ספירה שקל להיתקל בה. ב-docs_python.html, ‏//a[@href] (קיום תכונה) ספר 343, בעוד שחבילת selectolax עם if n.get("href") (ערך truthy) ספרה 341. שתי התוספות הן קישורי href="" ריקים. זה הבדל ב-convention של ספירה — תכונה קיימת מול תכונה שאינה ריקה — לא הבדל בהתנהגות של lxml, והספירות מסתדרות כשמיישרים את ה-predicate. חשוב לדעת כשמסקרייפים: האם href ריק נחשב או לא — זו בחירה של הפילטר שלכם, לא של המנתח.

מגבלת העומק שנראית כמו באג (אבל לא)

חבילת selectolax תיעדה ש-lxml מפיל את התוכן העמוק ביותר ב-markup מקונן בעומק 1,000 ו-5,000, ותיארה זאת כ-"lxml silently loses the deepest content." רציתי להבין את המנגנון, אז הרצתי את המנתח ברירת המחדל מול huge_tree=True.

lxml default depth guard around 253 levels and huge_tree to 2045

עומק מבוקשהמנתח ברירת המחדל מגיעhuge_tree=True מגיע
300253 (היתר נופל)299 (משוחזר)
1000253 (היתר נופל)999 (משוחזר)
5000253 (היתר נופל)2045 (עדיין נופל)

המנתח ברירת המחדל קוטם סביב 253 רמות ומפיל בשקט כל דבר עמוק יותר. זה לא באג — זו הגנת DoS של libxml2, מגבלת קינון של בערך 256 רמות שמונעת ממסמך עוין להפיל את ה-stack, והיא מתועדת ב-שרשור ה-launchpad של lxml על XML_PARSE_HUGE. כשתגדירו huge_tree=True, עומקים של 300 ו-1,000 חוזרים במלואם. אבל בעומק 5,000 עדיין מגיעים רק ל-2,045 גם עם huge_tree מופעל — יש סף recursion שני, קשיח יותר, מעל הסף הניתן להגדרה, ו-huge_tree לא עוקף אותו.

אז המשימה המעשית ברורה: כשאתם מנתחים markup עמוק ממקור שאתם סומכים עליו, השתמשו ב-lxml.html.HTMLParser(huge_tree=True). מה שחבילה זו מוסיפה מעל התצפית הממוחזרת הוא המנגנון (מגבלת בטיחות, לא השחתת נתונים), התיקון (huge_tree), והעובדה שיש סף שני שהפתרון לא מגיע אליו.

DOM לקריאה/כתיבה, סריאליזציה, וקידוד

lxml הוא עץ מלא לקריאה/כתיבה, לא מחלץ לקריאה בלבד, ואימתתי את משטח העריכה מקרה אחר מקרה. כל שמונה פעולות ה-DOM עברו: SubElement, ‏insert, ‏remove, ‏replace, ‏strip_tags (מסיר תגיות ומשאיר את הטקסט), ‏strip_elements (מסיר תגיות וגם את הטקסט שלהן), ‏drop_tree (ייחודי ל-lxml.html), ומודל הטקסט/זנב עם שני החריצים שמבלבל מצטרפים חדשים — ב-<p>head<b>bold</b>tail</p>, ‏p.text הוא "head", ‏b.text הוא "bold", ו-b.tail הוא "tail".

הסריאליזציה עברה חמש מתוך חמש: tostring במצבי XML ו-HTML (HTML משאיר כראוי אלמנטים ריקים בלי self-closing), ‏pretty_print, ‏C14N canonicalization (method="c14n", עוד ייחוד של lxml), ו-round-trip נקי.

קידוד הוא המקום שבו lxml נפרד בשקט מאחרים. תנו לו בייטים שאינם UTF-8 — למשל "<p>café éè</p>".encode("latin-1") דרך lxml.html.fromstring — והוא משחזר את café éè שלם, בלי תווי החלפה U+FFFD ובלי בייטים שנפלו. זה משחזר ישירות את תפקידו כ-"reference נקי" בחבילת selectolax, שבה אותו קלט הושחת בשקט תחת שני המנועים האחרים (Lexbor הפיק תווי החלפה, Modest פשוט הפיל בייטים). זיהוי התווים של lxml על בסיס libxml2 פשוט יציב יותר כאן.

הצד השני הוא קשיחות באופן שבו מכריזים על הקידוד. encoding="latin-1" בהצהרת XML מרים XMLSyntaxError: Unsupported encoding: latin-1, בעוד שהשם הקנוני לפי IANA ‏encoding="ISO-8859-1" מנתח כראוי ומחזיר café. ‏libxml2 מקבל רק שמות קידוד קנוניים, לא כינויים — פרט שתועד כבר ב-launchpad #613302. מעצבן אם לא יודעים, שולי כשכן יודעים.

ולבסוף, מחזור חיים של צמתים. הרצתי שלושה תרחישי handle מיושן בתהליכי subprocess מבודדים לחלוטין (קריסה קשה הייתה נראית כיציאה לא-אפסית): החזקת צומת אחרי שהעץ שלו נאסף ב-garbage collection, קריאת handle אחרי drop_tree(), ושימוש בצומת אחרי remove(). לא היו segfaults באף אחד מהם — lxml שומר על ההפניה של צומת לעץ שלו בחיים כדי למנוע use-after-free. אותו ציון עובר נקי גם selectolax קיבל בבדיקה הזו.

מהירות וזיכרון (ממוחזר, ובכנות מלאה לגבי זה)

כל מה שבסעיף הזה ממוחזר מחבילת selectolax, נכון ל-2026-07-13. החבילה הזו הפיקה אפס מספרי זמן משל עצמה, ואני מעדיף לומר את זה פעמיים מאשר שתחשבו שמדדתי משהו מחדש.

ממדערך lxmlקריאה
p50 ניתוח גולמי (10 MB)77.9 msמהיר בכ-33-34% מ-selectolax-Lexbor
p50 ניתוח מלא + חילוץ (1 MB / 10 MB)14.18 ms / 172.9 msבערך שוויוני מול Lexbor בגדלים קטנים
תפוקת CSS ל-100k צמתים3,002,646 צמתים/שנייהמהטווח המהיר ביותר מבין שלושת מנועי ה-C
שינוי RSS ב-10 MB128.9 MBהחסכוני ביותר מבין שישה מנתחים, כ-1.7x חסכוני יותר מ-BeautifulSoup
זמן עלייה קרה של import14.1 msמהיר פי ~2.3 מ-importים בסגנון parsel

מספרי הניתוח הגולמי והתפוקה חזקים, ו-lxml הוא גם החסכוני ביותר בזיכרון מבין ששת המנתחים שנמדדו. תמונת ה-threading דורשת הסתייגות, עם זאת. הנתונים הממוחזרים מראים האצה של 1.21x בלבד ב-4 threads, שסומנה כלא חד-משמעית — אבל זו הדרך עם parser משותף כברירת מחדל. ‏שאלות ותשובות של lxml ברורות לגבי כך שה-GIL משתחרר בזמן ניתוח רק כשכל thread משתמש ב-parser משלו (או ב-default שהועתק); parser משותף מסדר גישה בטור. אימתתי מבנית את משטח ה-API כדי לעשות זאת נכון (XMLParser.copy() קיים, ‏get/set_default_parser קיימים, ‏XPathEvaluator נושא lock פנימי), אבל לא מדדתי את ההאצה של parser נפרד לכל thread — זו הייתה דורשת מדידת זמן חדשה, והחבילה הזו לא מפיקה כאלה. לכן קראו את "1.21x" כ-"בנתיב המשותף והנאיבי", לא כתקרת ה-threading של lxml.

וכוכבית אחת על כל זה: אלו מספרים מפלטפורמה יחידה, macOS arm64. הטענה שהניתוח הגולמי של lxml מהיר מ-Lexbor עומדת מול הקונצנזוס הרגיל שלפיו המנתח שמבוסס Lexbor הוא המהיר ביותר, ולכן באמת צריך עליה אימות נוסף ב-Linux x86_64 לפני שמישהו מתייחס לזה כמסוכם.

רישוי: הניצחון המשעמם

lxml משוחרר תחת BSD-3-Clause, וספריות ה-C שהוא אורז איתו — libxml2 ו-libxslt — הן שתיהן MIT. זו שרשרת permissive לחלוטין, בלי copyleft בשום מקום, וזה חשוב ברגע שמפיצים מחדש. להשוואה, ה-wheel של selectolax אורז את Modest תחת LGPL-2.1 ואת Lexbor תחת Apache-2.0, כך של-lxml יש סיפור נקי יותר למוצר סגור.

יש גם יתרון פרקטי בהתקנה: lxml מפרסם wheels בנויים מראש שמקשרים סטטית את libxml2 ו-libxslt, כך ש-pip install lxml בדרך כלל לא צריך libxml2 של המערכת ולא קומפיילר במכונה שלכם — חוויה אחרת לגמרי מבנייה מהמקור.

איפה lxml משתלב — ואיפה שכבת חילוץ מבוססת AI לוקחת פיקוד

כדאי להיות חד לגבי הגבול, כי קל לטעות בקטגוריה כאן. lxml הוא ספריית ניתוח. הוא מוסר לכם עץ ומנוע שאילתות מעולה, וכל מה שסביב העץ הזה עדיין באחריותכם: למשוך את הדף, לרנדר JavaScript, לעבור דרך הגנות נגד בוטים, לכתוב ולתחזק את ה-XPath, ולבנות את התוצאה. זו שכבה אחרת משירות חילוץ מתארח, והשתיים פחות מתחרות ויותר שכנות.

למפתח שלא רוצה להחזיק בעצמו את כל ה-stack של fetch-render-select-maintain, השכבה העליונה הזו היא המקום שבו משהו כמו Thunderbit חי — ולסוג קהל כזה, זה ה-API, שרת MCP וה-CLI, לא תוסף הדפדפן. ה-Thunderbit Open API חושף את POST /distill כדי להפוך דף ל-Markdown נקי, ואת POST /extract כדי לשלוף נתונים מובנים לפי JSON Schema, עם מתג renderMode ומשימות batch לנפח גדול. אותו מנוע זמין גם כשרת MCP ‏(thunderbit_suggest_fields, ‏thunderbit_distill, ‏thunderbit_extract) עבור agents ועוזרי קוד, וגם כ-CLI שאפשר להריץ ישירות מהטרמינל דרך npx @thunderbit/thunderbit-cli. הוא מטפל ברינדור JS, בהגנות נגד בוטים וב-CAPTCHAs כברירת מחדל, ומחזיר JSON שתואם לסכימה — כלומר השכבה שמעל הניתוח, לא תחליף לו.

נסו את Thunderbit לחילוץ נתוני רשת

המסגור הוא פיצול פשוט. בחרו ב-lxml כשאתם שולטים בצינור העבודה ורוצים שליטה כירורגית ב-XPath על עץ שאתם מבינים. בחרו ב-API של חילוץ מבוסס AI כשאתם מעדיפים לא לתחזק בכלל selectors ורינדור. לא מעט מערכות אמיתיות משתמשות בשניהם — lxml לפידים המובנים שהן שולטות בהם, ושירות חילוץ לדפים המלוכלכים והארוכים שהן לא שולטות בהם.

מה הסקירה הזו לא בדקה

זו סקירה זמנית, לא טבלת ציונים סופית, אז הנה מה שהיא לא מכסה.

כל מספרי הזמן והזיכרון ממוחזרים, מפלטפורמה יחידה (macOS arm64, Python 3.14), ומורישים את ההסתייגויות של החבילה ההיא — התוצאה של "lxml מהיר יותר בניתוח גולמי" סותרת את הקונצנזוס וצריכה אימות חוזר על Linux x86_64. ההאצה של threading עם parser נפרד לכל thread לא נבדקה (היא הייתה דורשת מדידת זמן חדשה). מדדתי זיכרון של iterparse על 300k רשומות אבל לא XML אמיתי בגודל ג׳יגה-בייט, לא iterparse על HTML מול XML, ולא הרצת soak של שעות. ‏XSLT 1.0 של lxml, וכן אימותי RelaxNG / XMLSchema / DTD והרחבות EXSLT, לא נבדקו כאן בכלל — משטח יכולות גדול, אבל מעבר לליבה של ניתוח ובחירה. ראיתי את סף העומק השני ב-2,045 אבל לא מיפיתי את קבוע ה-recursion המדויק של libxml2. נבדקה רק גרסה יציבה 6.1.1, לא alpha של 7.0.0. ‏Windows, בניות מקור, ו-build חסר-נעילה של 3.14t לא נבדקו. ובתוך XPath עצמו, כיסיתי פונקציות מובנות אבל לא משתני XPath, פונקציות הרחבה מותאמות Python, או שימוש חוזר באובייקטי etree.XPath מוכללים מראש.

פסק הדין

lxml הוא לא הדבר החדש והמהיר, וזו בדיוק הסיבה להמליץ עליו. זהו binding בן שני עשורים ל-libxml2 עם מנוע XPath 1.0 מלא שלא קיים לו מתחרה mainstream ב-Python, שלושה הילוכים צפויים של קשיחות בניתוח עם error log באמצע, parser סטרימינג אמיתי למסמכים שלא נכנסים לזיכרון, טיפול נכון בכמה namespaces ובקידוד, ורישיון permissive לחלוטין. זוג הקצוות החדים — סף העומק של ~253 רמות ומספרי ה-threading עם parser משותף — מתועדים, ניתנים להגדרה, וכעת גם מוסברים.

אם אתם שולטים בצינור הסקרייפינג שלכם ונשענים על XPath, ‏lxml עדיין המנתח שאליו פונים. אם אתם מעדיפים לא לתחזק selectors ורינדור, לשם נועדה שכבת חילוץ מבוססת AI כמו ה-Thunderbit API, MCP ו-CLI — חלוקת עבודה נקייה, לא תחרות. בכל מקרה, התייחסו למספרים האלה כזמניים ובדקו שוב את מדידת הזמן בפלטפורמה שלכם לפני שאתם מצטטים אותם במסמך תכנון.

נסו את Thunderbit לחילוץ נתוני רשת Get Started Free

שאלות נפוצות

האם lxml הוא web scraper? לא. lxml הוא parser ו-serializer — binding של Python ל-libxml2/libxslt שהופך markup לעץ שאפשר לערוך ולשאול עליו. הוא לא מביא דפים, לא מרנדר JavaScript ולא מטפל בהגנות נגד בוטים; אתם מספקים את שכבת הבקשות (באמצעות requests, ‏httpx, דפדפן headless או שירות scraping) ומעבירים את הבייטים ל-lxml.

מתי להשתמש ב-lxml במקום BeautifulSoup או selectolax? פנו ל-lxml כשאתם צריכים XPath. ‏BeautifulSoup יכול להשתמש ב-lxml כמנתח backend, אבל לא מציע XPath מקורי, ו-selectolax הוא CSS-only ומהיר יותר בנישה הצרה שלו. אם לוגיקת הבחירה שלכם צריכה סינון לפי תוכן טקסט, ניווט להורה או לאב קדמון, חילוץ תכונה/צומת טקסט, או predicates של count, מנוע ה-XPath של lxml הוא האפשרות המיינסטרימית היחידה ב-Python שמבטאת אותם ישירות.

למה lxml מפיל בשקט תוכן שמקונן לעומק רב? מנתח ברירת המחדל שלו מגביל קינון לכ-253 רמות — הגנת DoS של libxml2 נגד מסמכים עוינים, לא באג. הגדירו huge_tree=True (למשל lxml.html.HTMLParser(huge_tree=True)) והוא משחזר לעומק של 300 ו-1,000 במלואם. שימו לב לסף recursion שני, קשיח יותר, סביב 2,045 רמות ש-huge_tree לא עוקף.

האם lxml משחרר את ה-GIL בניתוח מרובה threads? רק בתנאים הנכונים. ‏FAQ של lxml קובע שה-GIL משתחרר בזמן ניתוח כשכל thread משתמש ב-parser משלו או ב-default parser שהועתק; parser משותף מסדר את הגישה בטור. ההאצה הממוחזרת של 1.21x ב-4 threads משקפת את הנתיב הנאיבי עם parser משותף, לא את התקרה עם parser נפרד לכל thread, שלא נמדדה כאן.

האם lxml עדיין מתוחזק ב-2026? כן. הגרסה היציבה 6.1.1 יצאה ב-2026-05-18, המאגר קיבל דחיפה אחרונה ב-2026-07-02, ויש alpha של 7.0.0 בתהליך. עם בערך 3,000 כוכבי GitHub ו-libxml2 שמתוחזקת באופן פעיל מתחתיו, זו עדיין ספרייה עדכנית ומגובה היטב — לא ירושה עתיקה.

Ke
Ke
CTO ב-Thunderbit | מדען נתונים בכיר ומומחה ב-ML עם כמעט עשור של ניסיון בלמידת מכונה ובמדע הנתונים, קה שן הוא בוגר אוניברסיטת קולומביה ולשעבר מדען נתונים בכיר ב-Walmart Labs. עם מומחיות עמוקה ומוכרת בקרב עמיתים ב-Python, R, Java וסטטיסטיקה, הוא משתף תובנות מוכחות-בקרב על המעבר מאלגוריתמי AI מורכבים מתיאוריה לארכיטקטורה ברמת production.
תוכן עניינים

גרדו דף אינטרנט פשוט על ידי בקשה

תגידו מה צריך באנגלית פשוטה. או אפילו לא צריך להגיד כלום.

נסו את Thunderbit חינם
חילוץ נתונים באמצעות AI
העבירו נתונים בקלות ל-Google Sheets, Airtable או Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week