lxml تحت المجهر: محرك XPath الذي ما يزال يتفوّق على معظم محللات Python

آخر تحديث في August 12, 2026
lxml تحت المجهر: محرك XPath الذي ما يزال يتفوّق على معظم محللات Python
ملخص AI
تضع هذه المراجعة lxml بوصفه ربط Python طويل الأمد مع libxml2 وlibxslt، مع ميزة رئيسية لا تزال المحللات الأحدث نادرًا ما تضاهيها: محرك XPath حقيقي. تختبر المقالة تغطية XPath، ودرجات صرامة parser، وسلوك الذاكرة في البث، وقوة التعبير بين CSS وXPath، وحدود العمق في libxml2. وتُظهر lxml كأداة سريعة، اقتصادية في الذاكرة، وقادرة بشكل لافت على مهام XML وHTML التي تحتاج axes وpredicates وfunctions وstreaming أو أوضاع استعادة متينة. كما تشرح المراجعة حدود العمق المصممة للأمان ومتى يغيّر huge_tree هذا الحد.

كل بضعة أشهر يظهر محلل HTML أسرع، وتنتشر benchmark جديد، ثم يعلن أحدهم أن الأدوات القديمة صارت من الماضي. وبعدها مباشرةً تحاول اختيار كل فقرة تحتوي على كلمة معيّنة، أو تجلب العنصر الأب لعقدة مطابقة، فتتذكر بسرعة لماذا ما يزال lxml مفتوحًا في التبويب الثاني عندك.

lxml هو ربط Python قديم عمره 20 سنة مع libxml2. ليس لامعًا، وليس جديدًا. لكن في مهمة واحدة محددة — أي شيء يحتاج XPath حقيقيًا — لا يوجد في Python السائد شيء ينافسه فعلًا. هذه مراجعة عملية لما يفعله، وأين يثبت نفسه بهدوء، وأين قد تفاجئك إعداداته الافتراضية إذا لم تكن تعرفها.

lxml في سطر واحد: ما هو فعليًا؟

lxml هو ربط Python لمكتبات C ‏libxml2 وlibxslt. هو محلل ومحوّل تسلسلي، وليس أداة scraping ولا متصفحًا — يحوّل الوسوم إلى شجرة تقدر تستعلم عنها وتعدلها، ثم يعيد تحويل الشجرة إلى bytes. يقدّم واجهة متوافقة مع ElementTree، ومحرك XPath 1.0 كاملًا، وXSLT 1.0، والتحقق من المخططات، ويُدار بواسطة Stefan Behnel تحت الشعار "المكتبة الأكثر غنى بالميزات وسهولة في الاستخدام لمعالجة XML وHTML في لغة Python".

وهنا مكانه، وفق لقطة من GitHub وPyPI أُخذت بتاريخ 2026-07-14:

الحقلالقيمة
المستودعlxml/lxml
النجوم3,043
النسخ المتشعبة620
المشكلات المفتوحة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 خرج من سكربت، وليس مكتوبًا يدويًا. اختبارات القدرات حتمية ونتائجها منطقية/عددية ثابتة، لذلك تشغيل واحد يكفي — ضغط الجهاز لا يغيّر ما إذا كان //a/@href سيعيد قيمة attribute أم لا.

أما أرقام الزمن والبصمة الذاكرية فليست من هذه الحزمة. بل أُعيد استخدامها حرفيًا من حزمة benchmarking السابقة الخاصة بـ selectolax — على الجهاز نفسه، وفي البيئة الافتراضية نفسها، وبنفس بناء lxml وlibxml2، وبمعايير حتى 2026-07-13 — ولم أعد تشغيلها هنا. وهذا مقصود. إعادة تشغيل اختبارات الزمن بالتوازي مع مجموعة سكربتات القدرات يفتح الباب لتزاحم CPU يلوّث الأرقام المعاد استخدامها، كما أنه عمل مكرر: lxml كان أصلًا مكتبة مُقاسة بالكامل في تلك الحزمة. إعادة استخدام نفس bench تجعل المقارنة عادلة ومباشرة بدل إدخال قياس ثانٍ مختلف قليلًا. لذلك عندما ترى رقمًا بالميلي ثانية أدناه، فاقرأه على أنه "نفس منصة الاختبار، حتى 2026-07-13"، لا "أعدت قياسه اليوم".

النتائج تحمل وسم ثقة: single-observation لاختبارات القدرات الحتمية، وtriple-run لتوزيعات الزمن المعاد استخدامها، وhypothesis عندما أطرح آلية لم أعزلها.

XPath: الشيء الوحيد الذي لا يملكه selectolax وBeautifulSoup

هذا هو العنوان الرئيسي، لذلك سأبدأ به.

lxml XPath coverage moat with axes predicates and functions

اختبرت xpath() في lxml عبر مصفوفة من 37 حالة كانت مسجلة مسبقًا — أي أن النتيجة المتوقعة لكل حالة كُتبت في المصدر قبل تشغيل الاختبار، حتى لا أقيّم النتائج وفق انطباعي الشخصي. عشرة axes، وتسعة أنماط predicates، وعشر دوال مدمجة، وثلاثة أنواع إرجاع scalar، وخمس حالات فخ متعمّدة تستخدم صياغة XPath 2.0 التي يجب أن يرفضها محرك lxml 1.0.

الفئةالتغطيةالنتيجة
Axeschild / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding10/10 اجتياز
Predicates[1] / last() / position()<n / مساواة attribute / وجود attribute / and / or / [.//a] متداخل / not()9/9 اجتياز
Functionstext() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string()10/10 اجتياز
أنواع الإرجاعboolean / number scalars3/3 اجتياز
حالات الفخmatches() / sequences / if-then-else / except / خطأ صياغة5/5 رُفضت بشكل صحيح

النتيجة 37/37، والعمود الخاص بحالات الفخ هو الأهم. matches() وعبارات sequences وif/then/else وexcept كلها من XPath 2.0، ومحرك libxml2 1.0 لا يدعمها بشكل ناقص أو جزئي — بل يرفع XPathEvalError ويرفضها بدلًا من إرجاع مجموعة عقد خاطئة بصمت. لذلك هذه نتيجة كاملة بعد محاولة كسرها، لا نتيجة مثالية مجمّعة من أسئلة سهلة. كل سلوك هنا مطابق تمامًا لما يصفه توثيق lxml الخاص بـ XPath، وهذا هو المطلوب.

سأعترف بشيء واحد أخطأ فيه 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"))نعملا يوجد text predicate
اختيار الأب انطلاقًا من الابن (//b/parent::p)نعملا يوجد parent selector
إرجاع قيمة attribute (//a/@href)نعمعناصر فقط
إرجاع text node (//p/text())نعملا توجد text nodes
محور ancestor (//td/ancestor::div)نعملا توجد حركة للأعلى
تصفية الأب وفق عدد الأبناء (//ul[count(li)=4])نعملا يوجد count predicate
التصفية حسب طول النص (string-length(text())>5)نعملا يوجد length predicate
nth-child / last-child / sibling adjacentنعمنعم (3 أساسيات)

سبعة من أصل عشرة أهداف لا يوجد لها مكافئ CSS أصلًا. التصفية حسب نص المحتوى، والتنقل للأعلى نحو الآباء والأجداد، واستخراج قيمة attribute أو text node كقيمة نتيجة، والتصفية المبنية على العدّ — CSS لا يستطيع التعبير عن أي من ذلك. فقط ثلاثة (nth-child وlast-child وadjacent sibling) تعمل في الاثنين معًا. هذه هي الإجابة الرقمية على سؤال "ما الذي أكسبه فعلًا عندما ألجأ إلى lxml؟" selectolax يعمل بـ CSS فقط ولا يملك أصلًا method باسم xpath()، لذلك تصبح هذه الأنواع السبعة هناك إمّا حلقات Python متعددة الخطوات أو لا تحدث إطلاقًا. إذا كانت منطقية scraping لديك تعتمد على أي منها، فقرارك محسوم.

(ونعم، harness أمسك بي مرة ثانية هنا: كنت أتوقع مجموعة فارغة لـ string-length(text())>5، لكن ظهر تطابقان لنصّين بطول ستة أحرف. صححت التوقع، لا الأداة.)

ثلاث درجات للصرامة: etree مقابل recover مقابل lxml.html

XPath هو سبب اختيار lxml. لكن التحكم بثلاث درجات للصرامة هو سبب الاستمرار معه.

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

معظم المحللات تمنحك سلوكًا واحدًا فقط عند التعامل مع الإدخال التالف. lxml يمنحك ثلاثة، وهي متوقعة بما يكفي لأنني مررت ست فئات من الوسوم المعطوبة عبر كل واحدة منها وسجلت مسبقًا كيف ينبغي أن يتصرف كل مسار.

الإدخال المعطوبlxml.etree (صارم)etree + recover=Truelxml.html (متسامح)
وسم غير مغلق <root><a>x</root>يرفع خطأيستعيديقبل
تركيب متداخل خطأ <b><i></b></i>يرفع خطأيستعيديقبل
entity غير معرّف &nbsp;يرفع خطأيستعيديقبل
ampersand منفرد & (Tom & Jerry)يرفع خطأيستعيديقبل
عدة جذور <a>1</a><b>2</b>يرفع خطأيستعيديقبل
XML سليم البنيةيقبليقبل (0 أخطاء)يقبل
attribute منطقي <input disabled>يرفع خطأيستعيديقبل

طابقت الحالات السبع من أصل سبع التوقعات المسجّلة مسبقًا. lxml.etree يرفع XMLSyntaxError على جميع الفئات الست غير السليمة. أضف recover=True إلى parser نفسه فيبتلع الأخطاء ويعيد بناء شجرة قابلة للاستخدام — والجزء الذي يُستهان به هنا — أن parser.error_log عندها يسرد كل خطأ ابتلعه. أما lxml.html فيقبل كل شيء من دون اعتراض.

المصنّف الذي يقرر "رفع خطأ مقابل استعادة مقابل قبول" يعتمد بدوره على طول error_log وقت التشغيل، لا على قيمة ثابتة، ولهذا فإن وثيقة سليمة تُقرأ عبر recover=True تُصنّف بشكل صحيح على أنها "يقبل" (سجل فارغ) بدلًا من "يستعيد". النسخة الأولى من هذا المصنّف كانت تضع أي نتيجة recover=True تحت "يستعيد"، وبالتالي أساءت تصنيف الإدخال السليم؛ قراءة error_log الفعلي أصلحت ذلك.

ما الذي يقدمه هذا عمليًا؟ عند الحاجة إلى تحقق صارم بحيث يفشل feed المعطوب بصوت عالٍ، استخدم lxml.etree. عند التعامل مع HTML متسخ من الواقع وتريد فقط تجاوزه، استخدم lxml.html. والحالة الوسطى التي تعجز عنها معظم الأدوات — "كن متساهلًا، لكن أخبرني بالضبط ماذا كان معطوبًا كي أسجله" — استخدم recover=True واقرأ سجل الأخطاء. selectolax لديه الدرجة المتساهلة فقط، ولا يملك وضعًا صارمًا ولا سجل أخطاء.

iterparse: درجة البث الحي التي لا يملكها selectolax أصلًا

هذه ميزة قدرات، وليست مجرد ضابط سرعة. selectolax يستهلك النص كاملًا دفعة واحدة — لا توجد لديه واجهة incremental. أما iterparse في lxml (التوثيق) فيُخرج العناصر عند إغلاقها، ومع نمط fast_iter الكلاسيكي (استدعِ elem.clear() واحذف الإخوة السابقين أثناء التقدم) يبقي الذاكرة شبه ثابتة مهما كبر المستند.

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

قست سلوك الذاكرة مباشرة — عبر peak RSS باستخدام ru_maxrss، ولكل موضوع process جديد مستقل، على 300,000 عنصر <record> بإجمالي يقارب 26.7 MB (26,744,801 bytes).

الوضعفرق Peak RSSملاحظات
iterparse + clear (fast_iter)~1-2 MBيتحرر أثناء العمل؛ شبه ثابت مهما زاد العدد
iterparse بدون clear~386 MBيحتفظ بالمراجع؛ بوزن تحميل كامل
etree.parse (تحميل كامل، مرجع)~386 MBثقيل معروف؛ يثبت أن القياس يلتقط الفارق

الوضع المحدود يحافظ على فرق peak RSS عند نحو 1-2 MB فقط مقابل نحو 386 MB في التحميل الكامل — أي فرق في الحجم بنحو 0.3-0.4% — وحدث أول record قبل أن ينتهي قراءة الملف أصلًا، لذا فهو incremental فعلًا، لا بثًا زائفًا. والخط الأكثر تعليمًا هو الأوسط. شغّل نفس حلقة iterparse لكن اترك clear()، وستقفز الذاكرة مرة أخرى إلى حوالي 386 MB، لأنك تحتفظ بالمراجع لكل شيء. المكسب موجود في clear()، لا في iterparse وحده. كما أن قراءة مرجع التحميل الكامل الأعلى بكثير من الوضع المحدود تؤكد أن عداد RSS يلتقط فجوة الحجم فعلًا بدل أن يقرأ أرقامًا عمياء. (هذا اختبار الذاكرة قمت به داخل هذه الحزمة نفسها — فهو قياس footprint، مختلف عن أرقام الزمن المستعارة.)

النسخة الواقعية من هذا: ملف XML تصديري بحجم عدة غيغابايت لا يمكنه أن يدخل الذاكرة لا يملك مسارًا عبر selectolax أصلًا. إما أن تستخدم parser البثي في lxml أو تنتقل إلى لغة أخرى.

namespaces: RSS وSVG وفخ الـ default namespace

اثنا عشر حالة namespace، تشمل RSS عبر ثلاث namespaces، وSVG مع default namespace بالإضافة إلى xlink، وXML باستخدام default namespace. كلها اجتازت الاختبار.

lxml يستخرج //dc:creator/text() من feed RSS على هيئة ["Alice", "Bob"] بالضبط، ويتعامل مع //atom:link/@href و//content:encoded عبر ثلاث namespaces منفصلة في الوثيقة نفسها، ويعالج //s:rect و//s:use/@xlink:href في namespace الثاني لـ SVG، ويفك أسماء Clark-notation {uri}local عبر QName، ويفحص الخرائط عبر nsmap. هذا هو السلوك الموثّق والمدعوم، وهو بُعد كامل لا يلمسه selectolax لأنه HTML5 فقط ولا يعالج XML namespaces عشوائية.

هناك فخ واحد موثق يستحق أن يُحفظ. XPath لا يعرف مفهوم default namespace. إذا وجّهت //book إلى وثيقة تعلن xmlns="urn:..." فستحصل على صفر نتائج — prefix الفارغ غير معرّف في XPath، كما يوضح توثيق lxml. عليك أن تربط prefix اصطناعيًا (//c:book مع namespaces={"c": "urn:..."}، وقد وجد ثلاث نتائج) أو تلجأ إلى //*[local-name()='book'] (وأيضًا ثلاث نتائج). ليس خطأ، بل تنفيذ دقيق لمواصفة XPath. لكنه يفاجئ الجميع مرة واحدة فقط.

صفحات حقيقية متسخة: الدقة عبر 11 عملية scraping فعلية

الاختبارات الاصطناعية نظيفة؛ أما الويب فليس كذلك. أعدت استخدام 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

اجتازت جميع الصفحات الإحدى عشرة عبر lxml.html، وكانت أعداد الروابط والعناوين والصور مطابقة لأعداد lxml المعاد استخدامها من حزمة selectolax على جميع الصفحات الإحدى عشرة — تحقق متقاطع true. هذا التطابق هو ما يخبرني أن إعادة الاستخدام عادلة فعلًا وليست قياسين مختلفين يحملان نفس الاسم.

النتيجة الجانبية: محلل XML الصارم رفع خطأ في عشر صفحات من أصل إحدى عشرة. صفحات الويب الحقيقية ليست في الغالب XML سليم البنية، ولهذا يوجد وضع recovery في libxml2 لابتلاعها. الاستثناء الوحيد كان BBC News، المبني بـ Next.js، وكان منظمًا بما يكفي ليمر عبر XML الصارم. ليس كل ما يُسمى "HTML" يحتاج إلى مسار recovery.

هناك ملاحظة عدّ سهلة الوقوع في الخطأ. في docs_python.html، عدد //a[@href] (وجود attribute) كان 343، بينما حزمة selectolax سجلت if n.get("href") (قيمة truthy) عند 341. العنصران الزائدان هما روابط بـ href="" فارغ. هذا اختلاف في اتفاقية العدّ — وجود attribute مقابل كون قيمته غير فارغة — وليس اختلافًا في سلوك lxml، وتعود الأرقام إلى الاتساق بمجرد توحيد predicate. من المفيد معرفته عند scraping: هل الروابط الفارغة تُحسب أم لا، هذا قرار الفلتر وليس parser.

حد العمق الذي يبدو كأنه خطأ، لكنه ليس كذلك

كانت حزمة selectolax قد سجّلت أن lxml يفقد المحتوى الأعمق في وسوم <div> المتداخلة بعمق 1,000 و5,000 مستوى، وصاغت ذلك على أنه "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 مستوى ويهمل أي شيء أعمق بصمت. هذا ليس خطأ — إنه دفاع libxml2 ضد DoS، وهو حدّ تعشيق يقارب 256 مستوى يمنع وثيقة خبيثة من إسقاط stack، وهو موثق في نقاش Launchpad الخاص بـ lxml حول XML_PARSE_HUGE. إذا ضبطت huge_tree=True يعود العمق 300 و1,000 بالكامل. لكن العمق 5,000 يصل فقط إلى 2,045 حتى مع huge_tree مفعّلًا — هناك سقف recursion ثانٍ أصعب في libxml2 فوق السقف القابل للتعديل، وhuge_tree لا يرفعه.

إذًا الإجراء واضح: عندما تعالج وسومًا عميقة من مصدر موثوق، استخدم lxml.html.HTMLParser(huge_tree=True). وما تضيفه هذه الحزمة فوق الملاحظة المعاد استخدامها هو الآلية (حد أمان، لا تلف بيانات)، والحل (huge_tree)، وأن هناك سقفًا ثانيًا لا يصل إليه هذا الحل.

DOM للقراءة والكتابة، والتسلسل، والترميز

lxml شجرة كاملة للقراءة والكتابة، وليس مجرد extractor للقراءة فقط، وقد تحققت من سطح التحرير حالة بحالة. جميع عمليات DOM الثمانية اجتازت الاختبار: SubElement، وinsert، وremove، وreplace، وstrip_tags (إزالة الوسوم مع الإبقاء على نصها)، وstrip_elements (إزالة الوسوم ونصها)، وdrop_tree (ميزة حصرية في lxml.html)، ونموذج التخزين المزدوج text/tail الذي يربك المبتدئين — في <p>head<b>bold</b>tail</p> تكون p.text هي "head"، وb.text هي "bold"، وb.tail هي "tail".

التسلسل اجتاز خمسة من خمسة: tostring في وضعي XML وHTML (مع كون HTML يترك العناصر الفارغة كما يجب من دون إغلاق ذاتي)، وpretty_print، وcanonicalization وفق C14N (method="c14n"، وهي أيضًا ميزة حصرية لـ lxml)، وإعادة دورة نظيفة بالكامل.

الترميز هو المكان الذي يثبت فيه lxml نفسه بهدوء. إذا أرسلته bytes غير UTF-8 — مثل "<p>café éè</p>".encode("latin-1") عبر lxml.html.fromstring — فإنه يستعيد café éè كما هو، بلا محارف استبدال U+FFFD، ولا bytes مفقودة. وهذا يعيد مباشرة دوره كـ "المرجع النظيف" في حزمة selectolax، حيث كان نفس الإدخال يتلف بصمت في المحركين الآخرين (Lexbor أخرج محارف استبدال، وModest أسقط bytes بالكامل). إن اكتشاف charset المعتمد على libxml2 في lxml أكثر ثباتًا هنا.

والطرف المقابل هو الصرامة في كيفية تصريحك بالترميز. إذا كتبت encoding="latin-1" في XML declaration فسيُرفع XMLSyntaxError: Unsupported encoding: latin-1، بينما encoding="ISO-8859-1" المعياري وفق IANA ينجح ويعيد café. libxml2 يقبل فقط أسماء الترميز المعيارية، لا الأسماء البديلة — وهي ملاحظة موثقة منذ launchpad #613302. مزعجة إذا لم تكن تعرفها، وبسيطة جدًا بعد أن تعرفها.

أخيرًا دورة حياة العقد. شغّلت ثلاثة سيناريوهات لعقد stale داخل subprocess منفصلة (أي crash حاد سيظهر كخروج غير صفري): الاحتفاظ بعقدة بعد أن تُجمع شجرتها بواسطة garbage collector، وقراءة handle بعد drop_tree()، واستخدام عقدة بعد remove(). لا segfault في أي منها — lxml يحافظ على مرجع العقدة إلى شجرتها حيًا لمنع use-after-free. وهو نفس السجل النظيف الذي حصل عليه selectolax في هذا الاختبار.

السرعة والذاكرة (مستعارة، وبأمانة كاملة)

كل ما في هذا القسم معاد استخدامه من حزمة selectolax، حتى 2026-07-13. هذه الحزمة لم تُنتج أي أرقام زمنية خاصة بها، وأفضّل أن أقول ذلك مرتين بدل أن تظن أنني أعدت قياس أي شيء.

البعدقيمة lxmlالقراءة
Pure parse p50 (10 MB)77.9 msأسرع بنحو 33-34% من selectolax-Lexbor
Full parse + extract p50 (1 MB / 10 MB)14.18 ms / 172.9 msقريب جدًا من Lexbor عند الأحجام الصغيرة
throughput عند 100k node CSS3,002,646 node/sأسرع فئة بين محركات C الثلاثة
فرق RSS عند 10 MB128.9 MBالأكثر توفيرًا للذاكرة بين ستة محللات، وأقل بنحو 1.7x من BeautifulSoup
وقت بدء الاستيراد البارد14.1 msأسرع بنحو 2.3x من imports بأسلوب parsel

أرقام التحليل الخام والthroughput قوية، وlxml هو الأكثر اقتصادًا في الذاكرة بين المحللات الستة المقاسة. لكن صورة التعدد الخيطي تحتاج إلى تنبيه. البيانات المعاد استخدامها تُظهر تسريعًا زمنيًا عبر 4 خيوط بمقدار 1.21x فقط، وهو موسوم بأنه غير حاسم — لكن هذا يخص المسار الذي يستخدم parser مشتركًا افتراضيًا. FAQ الخاص بـ lxml واضح في أن GIL يُحرَّر أثناء التحليل فقط عندما يستخدم كل thread parserًا خاصًا به (أو نسخة من parser الافتراضي)، بينما parser مشترك يسبب تسلسل الوصول. تحققت بنيويًا من وجود واجهة API للقيام بذلك بالطريقة الصحيحة (XMLParser.copy() موجود، وget/set_default_parser موجودان، وXPathEvaluator يحمل قفلًا داخليًا)، لكنني لم أقس تسريع parser مستقل لكل خيط — فهذا سيكون قياسًا زمنيًا جديدًا، وهذه الحزمة لا تنتج مثل هذه القياسات. لذلك اقرأ "1.21x" على أنه "ضمن المسار المشترك الساذج"، لا أنه سقف lxml في التعدد الخيطي.

وملاحظة أخيرة على كل ذلك: هذه أرقام من منصة واحدة، macOS arm64. الادعاء بأن تحليل lxml الخام يتفوّق على Lexbor يصطدم عادةً بالإجماع السائد أن محلل Lexbor هو الأسرع، لذا يحتاج فعلًا إلى إعادة فحص على Linux x86_64 قبل أن يتعامل معه أحد كحقيقة مستقرة.

الترخيص: المكسب المملّ

lxml يأتي تحت رخصة BSD-3-Clause، والمكتبات C التي يضمها — libxml2 وlibxslt — كلاهما MIT. هذه سلسلة permissive بالكامل بلا copyleft في أي مستوى، وهذا مهم لحظة إعادة التوزيع. وللمقارنة، حزمة selectolax تضم Modest تحت LGPL-2.1 وLexbor تحت Apache-2.0، لذا فـ lxml هو القصة الأنظف عند الشحن ضمن منتج مغلق.

وهناك ميزة عملية عند التثبيت أيضًا: lxml ينشر wheels جاهزة تربط libxml2 وlibxslt بشكل ثابت، لذلك pip install lxml غالبًا لا يحتاج إلى libxml2 نظامي ولا إلى compiler على جهازك — تجربة مختلفة عن بنائه من المصدر.

أين يناسب lxml — وأين تتولى طبقة استخراج بالذكاء الاصطناعي المهمة

حان وقت توضيح الحدود، لأن الخلط هنا سهل جدًا. lxml مكتبة parsing. يمنحك شجرة ومحرك استعلام ممتازًا، لكن كل ما حول الشجرة ما يزال مسؤوليتك: جلب الصفحة، عرض JavaScript، تجاوز دفاعات anti-bot، كتابة XPath وصيانته، وتنظيم النتيجة. هذه طبقة مختلفة عن خدمة استخراج مستضافة، والاثنان ليسا خصمين بقدر ما هما جيران.

للمطوّر الذي لا يريد امتلاك stack الجلب-العرض-الاختيار-الصيانة، تكون الطبقة العليا هي المكان الذي يعيش فيه شيء مثل Thunderbit — وبالنسبة لهذا الجمهور فالمقصود هنا API وMCP server وCLI، لا إضافة المتصفح. Thunderbit Open API يوفّر POST /distill لتحويل الصفحة إلى Markdown نظيف، وPOST /extract لاستخراج بيانات منظمة وفق JSON Schema، مع خيار renderMode ومهام batch عند الحاجة إلى حجم كبير. ويُتاح المحرك نفسه كخادم MCP (thunderbit_suggest_fields وthunderbit_distill وthunderbit_extract) للوكلاء ومساعدي البرمجة، وكأداة CLI يمكن تشغيلها مباشرة من الطرفية عبر npx @thunderbit/thunderbit-cli. يتعامل مع عرض JavaScript، وanti-bot، وCAPTCHAs افتراضيًا، ويعيد JSON مطابقًا للمخطط — وهذه طبقة فوق parsing، لا بديلًا عنه.

جرّب Thunderbit لاستخراج بيانات الويب

والتقسيم هنا بسيط. اختر lxml عندما تملك خط الأنابيب وتريد تحكمًا دقيقًا عبر XPath على شجرة تفهمها. واختر API استخراج بالذكاء الاصطناعي عندما لا تريد أصلًا صيانة selectors أو rendering. كثير من الأنظمة الحقيقية تستخدم الاثنين معًا — lxml للـ feeds المنظمة التي تملكها، وخدمة استخراج للصفحات المتسخة ذات الذيل الطويل التي لا تملكها.

ما الذي لم تختبره هذه المراجعة

هذه مراجعة أولية وليست بطاقة نهائية، لذا إليك ما لا تغطيه.

كل أرقام الزمن والذاكرة معاد استخدامها، ومنصة واحدة فقط (macOS arm64، وPython 3.14)، وترث تحفّظات تلك الحزمة — نتيجة أن "lxml أسرع في pure parsing" تخالف الإجماع السائد وتحتاج إعادة فحص على Linux x86_64. لم أختبر تسريع parser مستقل لكل thread (سيحتاج قياسًا جديدًا). قست iterparse على 300 ألف record من حيث الذاكرة، لكن ليس على XML حقيقي بحجم غيغابايت، ولا iterparse على HTML مقابل XML، ولا تشغيلًا طويلًا لساعات. لم أختبر XSLT 1.0 في lxml، ولا RelaxNG / XMLSchema / DTD validation، ولا امتدادات EXSLT — وهي مساحة قدرات كبيرة، لكنها خارج جوهر parsing والاختيار. لاحظت سقف العمق الثاني عند 2,045، لكنني لم أحدد ثابت recursion الدقيق في libxml2. اختبرت الإصدار المستقر 6.1.1 فقط، لا alpha 7.0.0. كما أن Windows والبناء من المصدر ونسخة free-threaded 3.14t كلها غير مختبرة. وداخل XPath نفسه، غطيت الدوال المدمجة، لكن ليس متغيرات XPath، ولا دوال Python المخصصة، ولا إعادة استخدام كائن etree.XPath المسبق التجميع.

الحكم النهائي

lxml ليس الشيء الجديد السريع، وهذا تحديدًا سبب التوصية به. إنه ربط libxml2 عمره عقدان تقريبًا، مع محرك XPath 1.0 كامل لا يوجد له بديل في Python السائد، وثلاث درجات متوقعة من صرامة parsing مع سجل أخطاء في المنتصف، ومحلل بثي حقيقي للمستندات التي لا تتسع للذاكرة، وتعامل صحيح مع namespaces والترميز متعدد الأنواع، ورخصة permissive بالكامل. أما الحافتان الحادتان — حد العمق نحو 253 مستوى ورقم التعدد الخيطي مع parser مشترك — فهما موثقتان وقابلة للضبط ومفسّرتان الآن.

إذا كنت تملك خط scraping بنفسك وتعتمد على XPath، فما يزال lxml هو المحلل الذي يجب أن تلجأ إليه. وإذا كنت لا تريد أصلًا صيانة selectors أو rendering، فهذا ما صُممت له طبقة استخراج بالذكاء الاصطناعي مثل Thunderbit API وMCP وCLI — تقسيم واضح للأدوار، لا منافسة. وفي كلتا الحالتين، تعامل مع هذه الأرقام على أنها أولية، وأعد فحص الزمن على منصتك أنت قبل أن تقتبسها في وثيقة تصميم.

جرّب Thunderbit لاستخراج بيانات الويب Get Started Free

الأسئلة الشائعة

هل lxml أداة web scraper؟
لا. lxml هو parser ومحوّل تسلسلي — ربط Python مع libxml2/libxslt يحول الوسوم إلى شجرة قابلة للتعديل والاستعلام. لا يجلب الصفحات، ولا يعرض JavaScript، ولا يتعامل مع دفاعات anti-bot؛ أنت توفر طبقة الطلبات (عبر requests أو httpx أو متصفح headless أو خدمة scraping) ثم تمرر الـ bytes إلى lxml.

متى أستخدم lxml بدل BeautifulSoup أو selectolax؟
اختر lxml عندما تحتاج XPath. BeautifulSoup يمكنه استخدام lxml كمحلل خلفي لكنه لا يقدّم XPath أصليًا، وselectolax يعمل بـ CSS فقط وهو أسرع في نطاقه الضيق. إذا كانت منطقية الاختيار لديك تحتاج إلى التصفية حسب نص المحتوى، أو التنقل نحو الأب/الأجداد، أو استخراج attribute أو text node، أو predicates قائمة على العدّ، فمحرك XPath في lxml هو الخيار السائد الوحيد في Python الذي يعبّر عنها مباشرة.

لماذا يسقط lxml المحتوى المتداخل بعمق بصمت؟
لأن parser الافتراضي يضع حدًا للتعشيق عند نحو 253 مستوى — وهو دفاع libxml2 ضد المستندات الخبيثة، لا خطأ. اضبط huge_tree=True (مثل lxml.html.HTMLParser(huge_tree=True)) فيستعيد الأعماق 300 و1,000 بالكامل. لكن انتبه إلى سقف recursion ثانٍ أصعب عند نحو 2,045 مستوى لا يرفعه huge_tree.

هل يحرر lxml الـ GIL أثناء التحليل متعدد الخيوط؟
فقط في الظروف الصحيحة. يذكر FAQ الخاص بـ lxml أن الـ GIL يُحرَّر أثناء التحليل عندما يستخدم كل thread parserًا خاصًا به أو نسخة من parser الافتراضي؛ أما parser مشترك فيجعل الوصول متسلسلًا. تسريع 4 خيوط بمقدار 1.21x المعاد استخدامه هنا يعكس المسار الساذج ذي parser المشترك، لا سقف الأداء مع parser لكل thread، وهذا لم يُقَس هنا.

هل ما يزال lxml مُصانًا في 2026؟
نعم. الإصدار المستقر 6.1.1 صدر في 2026-05-18، وآخر دفع إلى المستودع كان في 2026-07-02، وهناك alpha 7.0.0 قيد العمل. ومع نحو 3,000 نجمة على GitHub وlibxml2 الذي ما يزال يُصان بنشاط تحته، فهو مكتبة حديثة ومدعومة، لا أثرًا قديمًا.

Ke
Ke
المدير التقني في Thunderbit | عالم بيانات أول وخبير في تعلّم الآلة بخبرة تقارب عقدًا من الزمن في تعلّم الآلة وعلم البيانات، كيه شين خريج جامعة كولومبيا وكان سابقًا عالم بيانات أول في Walmart Labs. وبفضل خبرته العميقة المعترف بها من قبل الأقران في Python وR وJava والإحصاء، يشارك رؤى مجرّبة حول نقل خوارزميات الذكاء الاصطناعي المعقدة من النظرية إلى بنية جاهزة للإنتاج.
فهرس المحتويات
Thunderbit · وكيل بيانات الويب بالذكاء الاصطناعي

استخرج البيانات من أي صفحة في بنقرة واحدة

موثوق لدى أكثر من 250,000 مستخدم
تتوفر خطة مجانية
من صفحة ويب إلى جدول بيانات
صف ما تحتاجه — ويقوم وكيل Thunderbit بالذكاء الاصطناعي باستخراجه وتصديره إلى Excel أو Google Sheets أو Airtable أو Notion. ابدأ مجانًا.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week