BeautifulSoup هي المكتبة التي يلجأ إليها معظم الناس أول مرة عندما يبدأون في استخراج بيانات الويب من صفحة ويب باستخدام Python، وهي فعلًا الأبطأ بين محلل HTML الجاد. هاتان الحقيقتان صحيحتان، ولا يوجد في ذلك أي انتقاص. والجزء المثير هنا أن كلمة "الأبطأ" ليست مجرد إحساس، بل رقم دقيق يمكن قياسه واحتسابه.
اختبرتُ bs4 (أي beautifulsoup4، الإصدار 4.15.0، الصادر في يونيو 2026، بترخيص MIT) عبر مزيج من اختبارات قدرات حديثة وبيانات توقيت معاد استخدامها من نفس منصة القياس، وكانت النتيجة ثابتة: أنت تدفع ثمنًا يقارب رتبة كاملة من حيث السرعة مقابل واجهة ألطف وتحمل أعلى للأخطاء في هذا المجال. وهل هذا التبادل ذكي أم لا، فهذا يعتمد بالكامل على طبيعة حملك البرمجي، لذلك ستجد في هذه المراجعة جانبي المعادلة معًا.
ما هي BeautifulSoup فعلًا، وما الذي ليست عليه
كثير من الشروحات تتجاوز أهم نقطة: BeautifulSoup لا تُحلّل HTML بنفسها، بل هي غلاف (wrapper). في الخلفية، تسلّم المستند إلى واحد من ثلاثة محللات حقيقية — html.parser المدمج في Python، أو lxml، أو html5lib — ثم تغلف الشجرة الناتجة بواجهة تنقّل وبحث ودودة جدًا. مهمة bs4 ليست التحليل نفسه؛ بل جعل التجوال داخل الناتج سهلًا ومريحًا.
ويصفها مؤلفها نفسه بأنها "مكتبة لاستخراج البيانات من الصفحات"، وكانت الفكرة دائمًا بسيطة: وجّهها إلى 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 يعتمد على المحرك الخلفي الذي تثبّته. html.parser جزء من مكتبة Python القياسية (برخصة PSF، ومن دون أي اعتمادات إضافية). أما lxml فمرخّص BSD، لكنه يعتمد على libxml2/libxslt — وهي اعتمادات C خارجية إمّا تبنيها بنفسك أو تأتي بها كعجلة جاهزة. وhtml5lib مكتبة Python خالصة وترخيصها MIT. إذا أردت أخف بصمة اعتمادية، فالمحلل المدمج html.parser هو الخيار الأبسط — وهو، بالمصادفة، أيضًا المحرك الذي يحمل أكبر فخ. سنعود إلى ذلك بعد قليل.
كلفة السرعة، بالأرقام
لنضع الرقم مباشرة في البداية، لأن هذا هو العنوان الأبرز وإخفاؤه سيكون غير صريح. في مهمة واقعية من نوع parse-then-extract — أي تحليل السلسلة ثم استخراج كل <h3 class="title"> وكل <a href> — تكون BeautifulSoup الأبطأ في هذه المقارنة، والفارق ليس بسيطًا.

هذه الأرقام مأخوذة من منصة قياس selectolax نفسها (نفس الجهاز، ونفس منهجية التشغيل 3 مرات، حتى تاريخ 2026-07-13)؛ وهذه المراجعة لم تُعد تشغيل أي اختبار أداء من جانبها، لتجنب تشويش المعالج وتكرار العمل. أدنى زمن وسيط 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 مرة — أي ما تزال متأخرة بفارق يقارب رتبة كاملة. والسبب بنيوي وليس عيبًا برمجيًا: بغض النظر عن المحرك الذي يقوم بالتحليل، تبني bs4 كائن Python كاملًا (Tag أو NavigableString) لكل عقدة. وهذه الطبقة من تحويل كل عقدة إلى كائن هي ضريبة لا يدفعها محللو C.
لاحظ أن المضاعف يزداد كلما كبرت الصفحات — 12.0x عند 1 KB، و17.4x عند 10 MB. هذا يخبرك أن المسألة ليست كلفة بدء ثابتة يمكن امتصاصها مع الوقت، بل ضريبة لكل عقدة تتوسع خطيًا مع عدد العقد.
والآن لنعد تأطير الفكرة، لأن عبارة "أبطأ 10 مرات" تبدو مرعبة أكثر مما هي عليه غالبًا. على صفحة بحجم 1 MB، نتحدث عن 232 ms مقابل 15 ms. إذا كانت مهمتك هي "استخراج بيانات من بضع مئات إلى بضعة آلاف من الصفحات، وكل صفحة بحجم بضع مئات من الكيلوبايت"، فالفارق المطلق هنا غير ملحوظ — لن تشعر به، وتحسينه لن يغيّر كثيرًا. أما إذا كانت مهمتك خط معالجة لمليون صفحة، فالنسبة نفسها تصنع الفرق بين مهمة تنتهي وأخرى لا تنتهي. الرقم نفسه، لكن الحكم يختلف تمامًا. قِس ذلك مقابل حجمك الفعلي، لا مقابل رقم benchmark.
لا، تغيير المحرك الخلفي لا يحل المشكلة
هناك خرافة شائعة مفادها أنك إذا مررت إلى bs4 محرك lxml فستحصل على سرعة lxml. هذا غير صحيح، ومن المهم فهم السبب. في استعلام CSS على دفعة من 100,000 عقدة (اختر كل <a> واقرأ href الخاص به، مع كون الشجرة مبنية مسبقًا)، يظهر الفرق في throughput بوضوح:
| المحلل | p50 للاستعلام | العقد/ثانية |
|---|---|---|
| lxml | 33.30 ms | 3,002,646 |
| selectolax-Modest | 34.19 ms | 2,924,550 |
| selectolax-Lexbor | 39.46 ms | 2,534,027 |
| bs4 (lxml) | 250.56 ms | 399,111 |
تصل bs4(lxml) إلى نحو 399,000 عقدة/ثانية — أي أبطأ بنحو 6.3–7.5 مرات من محركات C الثلاثة، رغم أن محركها الخلفي نفسه هو lxml. السبب أن المحرك الخلفي يسرّع مرحلة بناء الشجرة فقط. أما الاستعلام والتجوال فيمران عبر soupsieve إلى كائنات bs4 من نوع Tag، وتبقى كل عقدة مطابقة محصورة داخل Python. لذلك فالتصور الذهني القائل: "أعطِ bs4 محرك lxml وستصبح بسرعة lxml" تصور خاطئ؛ المحرك الخلفي يسرّع مرحلة واحدة، وليست هي المرحلة الأبطأ هنا.
الذاكرة ووقت التشغيل البارد يضيفان إلى الكلفة. في مستند حجمه 10 MB، تستخدم bs4 تقريبًا 1.5–1.75x من الذاكرة المقيمة التي تستخدمها selectolax أو lxml (218–226 MB مقابل 129–145 MB) — والسبب نفسه: كائن Python واحد لكل عقدة. كما أن استيراد bs4 يستغرق نحو 33.4 ms مقابل 14.1 ms لـ lxml.html، أي أنه أبطأ 2.36x عند الاستيراد. هذا فرق صغير في برنامج طويل العمر، لكنه يصبح كلفة حقيقية إذا كان لديك أداة CLI أو دالة serverless تبدأ من الصفر كثيرًا.
لماذا لن تنقذك خيوط التنفيذ المتعددة
إذا كان حدسك عند مواجهة مهمة CPU-bound بطيئة هو "ألقِ عليها مزيدًا من الـ threads"، فـ bs4 ستعاقبك على هذا الحدس. على صفحة بحجم 1 MB تم تحليلها 48 مرة، وعند المقارنة بين خيط واحد وأربعة خيوط:
| المحلل | خيط واحد | 4 خيوط | مقدار التسريع |
|---|---|---|---|
| selectolax-Lexbor | 0.563 s | 0.159 s | 3.54x |
| lxml | 0.459 s | 0.378 s | 1.21x |
| bs4 (lxml) | 6.945 s | 26.842 s | 0.26x |
اقرأ السطر الأخير مرتين. أربعة خيوط جعلت bs4 أبطأ بنحو 3.9 مرات، وليس أسرع. والإشارة التجريبية هنا تقول على الأرجح إن GIL ما يزال ممسوكًا: بناء الشجرة في bs4 يتم بالكامل في Python، ولذلك يتسلسل تحت Global Interpreter Lock، وإضافة الخيوط تزيد فقط كلفة الجدولة إلى مهمة لا يمكنها فعليًا أن تعمل بالتوازي. أما selectolax فحقق تسارعًا يقارب 3.5x لأن نواته المكتوبة بـ C تفرج القفل؛ bs4 لا تملك هذه المساحة.
وفي عصر free-threading، فالمغزى العملي هو: إذا أردت موازاة BeautifulSoup، فاتجه إلى multiprocessing (ProcessPoolExecutor) لا إلى threads. أما selectolax وlxml فبإمكانهما الاستفادة من الخيوط؛ bs4 لا تستطيع. وملاحظة منهجية مهمة: هذا استنتاج من ملاحظة واحدة عند عدد خيوط واحد (4) وعلى حجم صفحة واحد (1 MB)، وآلية "يمسك GIL" هنا هي فرضية مستنتجة من السلوك الزمني لا شيئًا أثبته قياس مباشر لمسار القفل. الاتجاه واضح، لكن الآلية الدقيقة ما تزال تقديرية.
المحرك الافتراضي هو الفخ. اقرأ هذا أولًا
إذا أخذت شيئًا واحدًا من هذه المراجعة، فليكن هذا. استدعاء BeautifulSoup(html) من دون وسيط ثانٍ يستخدم html.parser، وhtml.parser لا يطبق قواعد HTML5 الخاصة بالعناصر ذات الوسم الختامي الاختياري. قد يبدو هذا تفصيلًا نظريًا حتى يفسد بياناتك بصمت.

شغّلتُ 15 عينة HTML متعمدة الفساد عبر المحركات الثلاثة، مع فرضية بنيوية مستقلة عن المحرك لكل عينة قبل التشغيل (حتى لا يختار أحد الفائز بعد ظهور النتائج). وكانت الدرجات كالتالي:
| المحرك الخلفي | مطابق للتوقع / 15 |
|---|---|
| lxml | 15 |
| html5lib | 15 |
| html.parser | 12 |
وتشترك حالات الفشل الثلاث في سبب واحد. خذ جدولًا غير مغلق: <table><tr><td>a<td>b<tr><td>c<td>d</table>. مع html.parser، يخرج نص الخلايا المستخرج على شكل ['abcd','bcd','cd','d'] — إذ تبتلع كل <td> ما يأتي بعدها، لأن المحلل يعشّش الخلايا بدل أن يغلقها. أما lxml وhtml5lib فيرجعان بشكل صحيح ['a','b','c','d']. والقوائم غير المغلقة تتصرف بالطريقة نفسها: <li>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 يقول احتفظ بالأول.
وهذا خطر، لا مجرد إزعاج، لأنه يحدث من دون رفع أي خطأ. أي أداة scraping تستخدم BeautifulSoup(html) بشكل مباشر وتصادف جدولًا أو قائمة غير مغلقة — وهو أمر شائع بشكل محبط في المواقع القديمة، وHTML المكتوب يدويًا، والقوالب التي نُسي فيها إغلاق وسم — ستخلط نصوص الخلايا المجاورة في حقل واحد، وتسلّمك بيانات ملوثة، ولن تشتكي أبدًا. والحل هو تمرير وسيط واحد: BeautifulSoup(html, "lxml") أو BeautifulSoup(html, "html5lib").
وللإنصاف مع html.parser، فإن 12 حالة أخرى من أصل 15 من عينات HTML المتعطلة خرجت متطابقة عبر المحركات الثلاثة — وسوم متداخلة بشكل خاطئ مثل <b><i></b></i>، وغياب هيكل html/body، وسمات بلا علامات اقتباس، ووسوم إغلاق يتيمة، وتعليقات غير مغلقة، ونماذج متداخلة، وحروف كبيرة وصغيرة مختلطة، وغير ذلك. تحمل bs4 للأخطاء قوي جدًا فعلًا بشكل عام؛ والاختلاف يتركز تقريبًا بالكامل في فئة العناصر ذات الوسم الختامي الاختياري. وليس أي من هذا اكتشافًا جديدًا — فتوثيق bs4 نفسه في قسم "Differences between parsers" يذكر أصلًا أن html.parser "أقل تساهلًا" بعبارات واضحة. لكن مصفوفة HTML المتعطلة هنا تضيف الحالات المحددة والقابلة لإعادة الإنتاج التي يتحول فيها "أقل تساهلًا" إلى مخرجات خاطئة.
ما الذي لا تفقده: واجهة البرمجة وCSS هما أفضل جزء
إذًا bs4 بطيئة، وأحادية الخيط، وفيها فخ في المحرك الافتراضي. ومع ذلك ما يزال الناس يلجأون إليها، لأن نصف الصفقة المتعلق بالسهولة حقيقي تمامًا — وقد ثبت ذلك عند الاختبار.

شغّلتُ 29 اختبارًا لواجهة برمجة التطبيقات تغطي البحث، وCSS، والتنقل داخل الشجرة، واستخراج النص، وتعديل DOM. اجتازت جميعها الـ 29، وكانت نتيجة كل اختبار محسوبة بمقارنة القيمة المرجعة الفعلية بالقيمة المتوقعة، لا بالتخمين البصري. ومن بين هذه القدرات ميزتان تتعلقان بسهولة الاستخدام لا توفرهما محركات C بنفس الشكل:
- المحددات الوظيفية داخل
find/find_all. يمكنك كتابةsoup.find(lambda t: t.name == "a" and "btn" in t.get("class", []))والتعبير عن شرط معقد في سطر Python واحد — من دون الحاجة إلى "اختر كل شيء ثم صفِّه" كخطوتين. - تنقل شجري مسمّى وفي الاتجاهين.
.parent، و.next_sibling، و.find_parent، و.stripped_strings، و.descendants— عمليات التجوال تُقرأ كأنها لغة طبيعية وتتحرك في الاتجاهين. بعض هذه الخطوات يتطلب عدة مراحل في selectolax، أو غير متاح أصلًا.
هذه هي بالضبط "توفير وقت المطور" بشكل ملموس. ليست دعاية، بل 29 إشارة نجاح خضراء.
ومع ذلك، هناك فخّان يستحقان الذكر، لأن أي مراجعة منصفة تذكر الجانبين. أولًا، السمات المنطقية: <input disabled> تُرجع في bs4 سلسلة فارغة "" عند قراءة disabled (بينما يعيد selectolax قيمة None). وكلتاهما قيم falsy، لذا فإن if node.get("disabled") قد يفوّت بصمت سمة منطقية موجودة فعلًا في كلتا المكتبتين — والاختبار الآمن هو "disabled" in tag.attrs. ثانيًا، get_text(strip=True) يدمج نصوص العقد بعد إزالة الفراغات من دون فاصل، لذا فإن "...with " + "link1" تصبح "withlink1". مرّر separator=" " عندما تحتاج إلى حدود كلمات. ولا واحدة من هاتين المشكلتين تخص bs4 وحدها؛ كلتاهما من فخاخ الاستخدام المشتركة بين المكتبات.
والآن الجزء الذي يفاجئ الكثيرين: اختيار bs4 لا يعني أنك تخسر تغطية CSS. فمحرك CSS الخاص بها، soupsieve، هو التنفيذ الأكثر اكتمالًا في هذه المقارنة كلها. في مصفوفة الأساس المكونة من 41 حالة (معاد استخدامها من منصة selectolax) سجل soupsieve 41/41 — وهو التقييم الكامل الوحيد هنا، متقدمًا على selectolax-Lexbor الذي سجل 39/41 وعلى cssselect (في lxml/parsel) الذي سجل 37/41. ثم شغّلت 20 حالة إضافية موسعة موثقة في soupsieve، وحقق 20/20، بما في ذلك محددات يرفضها Lexbor صراحة: :lang(en)، وsoupsieve-only :-soup-contains('featured')، و:is()، و:where()، و:has(> a). الفجوات الحقيقية الوحيدة هي XPath (soupsieve يعمل بـ CSS فقط) وpseudo-elements الخاصة بـ parsel مثل ::text و::attr()، وهي امتدادات Scrapy. إذا كنت تعيش داخل XPath، فالهجرة ستؤلمك.
الخلاصة في هذا القسم واضحة: ما تضحي به عند اختيار BeautifulSoup هو السرعة. أما سهولة الـ API، فلا تضحي بها. وكذلك لا تضحي بتغطية CSS.
فخّان إنتاجيان يستحقان إدراجهما في الميزانية
إلى جانب المحرك الافتراضي، هناك سلوكان سيضربانك خصوصًا في الأحمال طويلة العمر أو التي لا تستخدم UTF-8.
الحلقات المرجعية: استدعِ decompose() في الحلقات الطويلة
كل Tag في bs4 يحتفظ بمرجع إلى الأب وكذلك إلى الأبناء، وهذا يصنع حلقة مرجعية. عدّاد المراجع في CPython لا يستطيع التخلص من الحلقة وحده — هذه مهمة جامع القمامة generational GC. ولرؤية مدى أهمية ذلك، بنيتُ شجرة ثم حذفتها 300 مرة مع إيقاف GC، ثم أحصيت كائنات Tag التي بقيت في الذاكرة:

| السيناريو | عدد Tags المتبقية بعد del |
|---|---|
| GC متوقف | 120,900 (300 دورة، لم يُستعد شيء) |
| GC مفعّل | 26,598 (جامع القمامة فَعّل نفسه أثناء الحلقة) |
بعد gc.collect() الإجباري | 0 (استُعيدت كلها) |
| مجموعة ضبط بلا حلقة (قائمة سلاسل، GC متوقف) | الفرق 0 |
مع إيقاف GC، لم يستعد del soup شيئًا — بقيت كل الكائنات الـ 120,900 مقيمة، لأن الحلقة المرجعية تتغلب على عدّاد المراجع. أما gc.collect() واحد فمسحها كلها. ومجموعة الضبط الخالية من الحلقات (قائمة سلاسل بسيطة، معروف أنها بلا دورة) بقيت فروقها صفرًا، ما يثبت أن التراكم جاء من حلقة bs4 نفسها لا من ضجيج القياس. وتقول وثائق bs4 نفسها إن الكائنات "densely interconnected ... exactly the sort a garbage collector would have trouble with"، لذا فالسلوك هنا موثق؛ وما يضيفه هذا الاختبار هو عدد الكائنات المحتفظ بها ودليل أن collect() يصفرها.
والقاعدة العملية هي: في خط معالجة يحلل كثيرًا من الصفحات الكبيرة داخل حلقة ضيقة، إذا كان كودك (أو إعداد عالي الإنتاجية) يعطل 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، لأن الكاشف الإحصائي غير موثوق على المدخلات القصيرة، ولأن محارف رسم الصناديق في DOS تتداخل مع نقاط Unicode الخاصة بـ 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="...").
هل يختلف المحركان فعلًا على الصفحات الحقيقية؟
تُظهر مصفوفة HTML المتعطلة أن المحركات تختلف عند المدخلات المعطوبة عمدًا. والسؤال التالي المنطقي هو: هل يهم ذلك في العالم الحقيقي؟ لذلك شغّلت المحركات الثلاثة على 11 صفحة حقيقية مُجلبة — BBC، وWikipedia، وCraigslist، وMDN، وold.reddit، ووثائق Python، وHacker News، وBooks to Scrape، وwebscraper.io، وwhitehouse.gov، وصفحة اقتباسات معروضة عبر JavaScript — مع مقارنة عدد الروابط والعناوين والصور.
اتفقت المحركات الثلاثة في جميع الصفحات الـ 11. لا اختلاف. وهذا يعني أن الاختلاف الذي ظهر في قسم الفخ يظهر فقط على HTML المتعطل عمدًا؛ أما حين يكون الموقع الحديث منظمًا جيدًا بما يكفي — حتى لو كان "فوضويًا" — فإن اختيار المحرك لا يغيّر ما تستخرجه. والمعنى العملي: في المواقع السائدة والمرتبة جيدًا، html.parser كافٍ تمامًا ويوفر عليك الاعتماد الإضافي. فقط عندما تكون الصفحات واضحة الشذوذ، أو مكتوبة يدويًا، أو قديمة جدًا، يبدأ اختيار المحرك في تغيير النتائج، وهنا تنتقل إلى lxml أو html5lib.
وتفصيل جانبي ظهر في تلك الجولة، لأنه حالة حافة حقيقية. تحتوي صفحة MDN على عنصر <template>، وجميع محركات bs4 أعادت 508 روابط — أي أن bs4 تفرد محتوى <template> داخل الشجرة الرئيسية. وهذا يضع bs4 في نفس جهة lxml، وعلى الجهة المقابلة لـ selectolax-Lexbor، الذي يتبع مواصفة HTML5 بحرفيتها (فـ <template> هو DocumentFragment خامل) ويعيد 497، متجاهلًا بصمت 11 رابطًا داخل القالب. لذلك ستلتقط bs4 البيانات داخل <template> — وهذا مفيد، لكنه أيضًا قد يجلب "محتوى وهميًا" لا يمكن للمتصفح أن يعرضه. ولا واحد من السلوكين خاطئ؛ إنهما تفسيران مختلفان للمواصفة، ومن الأفضل أن تعرف أيهما تحصل عليه.
أين تناسب BeautifulSoup — وأين لا تناسب
بدلًا من ضغط كل هذا في درجة واحدة من 0 إلى 100 (وهو ما كان سيخفي بالضبط المفاضلات المهمة)، إليك بطاقة تقييم حسب البعد، مع ملاحظة على كل سطر:
| البعد | ما الذي وجدته الاختبارات | ملاحظة للقارئ |
|---|---|---|
| التثبيت / التشغيل الأول | غلاف خالص، بلا متصفح أو إعداد؛ html.parser بلا اعتمادات؛ جميع العجلات جاهزة | محرك lxml الخلفي يحتاج اعتماد C |
| السرعة مقابل محركات C | أبطأ 12–17x (html.parser) / 10.5–14x (lxml backend)، على كل الأحجام | منصة واحدة؛ بيانات selectolax معاد استخدامها |
| throughput لاستعلام CSS | أبطأ بنحو 6–7.5x على 100k عقدة؛ محرك lxml الخلفي لا ينقذه | معاد الاستخدام؛ يدفع ضريبة كائن Python Tag |
| الذاكرة | 1.5–1.75x من selectolax/lxml؛ الأثقل | معاد الاستخدام؛ مقاس عبر RSS |
| بدء التشغيل البارد للاستيراد | أبطأ 2.36x (33.4 مقابل 14.1 ms) | معاد الاستخدام؛ بند صغير |
| التوسع مع الخيوط | bs4-lxml أبطأ بنحو 3.9x مع 4 خيوط (يمسك GIL) | ملاحظة واحدة؛ استخدم multiprocessing |
| سهولة الـ API | 29/29 اختبارًا؛ find بالمحددات الوظيفية + تنقل ثنائي الاتجاه | فخ السمة المنطقية الفارغة وحدود الكلمات في strip |
| تغطية CSS | soupsieve الأقوى: 41/41 أساسي + 20/20 موسع؛ يدعم :lang | لا XPath، ولا ::text |
| تحمل 3 محركات | lxml/html5lib 15/15؛ html.parser 12/15 | الاختلاف فقط على HTML المتعطل |
| الاتساق في الصفحات الحقيقية | المحركات الثلاثة متفقة 11/11؛ كلها تفرد <template> (508) | في المواقع جيدة التشكيل: لا يهم المحرك |
| حلقات المراجع / GC | الشجرة حلقة؛ 300 دورة أبقت 120,900 كائن، وcollect صفّرها | الحلقات الطويلة تحتاج decompose() |
| الترميز | UnicodeDammit استرد 5/8؛ يخطئ في العينات القصيرة، ويتبع التصريح الخاطئ | ملاحظة واحدة |
| الصيانة | نشطة (4.15.0، يونيو 2026)؛ MIT | الصفحة على crummy/Launchpad، وليس GitHub |
إذًا لمن تكون BeautifulSoup مناسبة؟ لأي شخص يقدّر واجهة مقروءة وتحليلًا متسامحًا أكثر من أقصى throughput، ويعمل على حجم متوسط — النماذج الأولية، عمليات scraping لمرة واحدة، الأدوات الداخلية، والفرق التي تكون فيها كلفة وقت التطوير أعلى من كلفة وقت التشغيل. ومن ينبغي أن ينظر إلى مكان آخر؟ خطوط معالجة بمليون صفحة حيث تتحول ضريبة السرعة إلى مال حقيقي، وأحمال تحتاج توازيًا على مستوى الخيوط، وأي شخص مرتبط بـ XPath.
وملاحظة مهمة عن موضعها في منظومة scraping حقيقية، ودور أداتنا نحن هنا. BeautifulSoup تفترض أنك تمتلك HTML أصلًا. هي لا تجلب الصفحات، ولا تعرض JavaScript، ولا تفعل شيئًا تجاه دفاعات anti-bot أو CAPTCHAs — وهذه مهمة منفصلة تمامًا، وصعبة فعلًا على الويب الحديث. وهنا تأتي طبقة مختلفة: Thunderbit تملك stack للمطورين — REST API، وMCP server، وCLI — تتولى الجلب، وعرض JavaScript، ومشكلة anti-bot، ثم تعيد إمّا Markdown نظيفًا (POST /distill) أو JSON بنية مطابقًا للمخطط (POST /extract) من دون أن تكتب selectors أصلًا. الاثنان ليسا متنافسين؛ بل متكاملان. bs4 تحلل HTML الذي لديك بالفعل؛ أما API وMCP وCLI الخاصة بـ Thunderbit فتجلب لك HTML الذي يصعب الوصول إليه من البداية. إذا كانت عنق الزجاجة لديك هي التحليل، فـ bs4 إجابة جيدة. وإذا كانت عنق الزجاجة هي الحصول على البيانات، فهذه طبقة أخرى.
جرّب Thunderbit لاستخراج بيانات الويب
الخلاصة
تعطيك BeautifulSoup أكثر واجهة ودية، وأقوى تحمل لـ HTML المتعطل، وأكثر محرك CSS اكتمالًا في هذه المقارنة — مقابل ضريبة سرعة تقارب رتبة كاملة وبصمة ذاكرة أثقل. هذه هي الصفقة كاملة بوضوح. المحرك الافتراضي html.parser هو الفخ الحقيقي الوحيد: فهو يشوّه بصمت الجداول والقوائم غير المغلقة، لذا مرّر "lxml" أو "html5lib" كلما كان المحتوى قد يكون غير نظيف. الخيوط لن تسرّعها — لكن multiprocessing سيفعل. وفي الحلقات الطويلة، استدعِ decompose() لكل صفحة حتى لا تتراكم الحلقات المرجعية.
وهناك قيدان أخيران مهمان. كل ما هنا قيس على منصة واحدة (macOS arm64، وPython 3.14، وعجلات جاهزة)، ومضاعفات التوقيت معادة الاستخدام من منصة selectolax (نفس الاختبار، حتى 2026-07-13) بدلًا من إعادة التشغيل — لذا فهي ترث قيد المنصة الواحدة، وقد تغيّر بيئة Linux x86_64 أو البناء من المصدر الأرقام قليلًا. كما أن لا شيء هنا يُعد اكتشافًا جديدًا؛ فـ bs4 مكتبة عمرها 20 عامًا، وكل سلوك اختبرناه إما موثق أو مسجل علنًا. القيمة ليست في سبق صحفي، بل في وضع رقم حقيقي على مفاضلات كان التوثيق يصفها نوعيًا فقط.
الأسئلة الشائعة
هل BeautifulSoup بطيئة؟
نعم، وبشكل يمكن قياسه. في مهمة تحليل + استخراج تعمل بنحو 12–17 مرة أبطأ من محلل C مثل selectolax-Lexbor مع المحرك الافتراضي html.parser, ونحو 10.5–14 مرة أبطأ مع محرك lxml، لأنها تبني كائن Python لكل عقدة. وهل هذا مهم أم لا يعتمد على الحجم: على صفحة 1 MB نتحدث عن 232 ms مقابل 15 ms، وهو فرق لا يُرى في بضعة آلاف من الصفحات لكنه حاسم في خط معالجة لمليون صفحة.
أي محلل BeautifulSoup أستخدم: html.parser أم lxml أم html5lib؟
للمواقع السليمة والشائعة، المحرك الافتراضي html.parser جيد ولا يضيف اعتمادات. لكنه لا يطبق HTML5 للوسوم الختامية الاختيارية، لذا عند الجداول أو القوائم غير المغلقة يخلط النص المجاور من دون خطأ. وعندما يكون المدخل متعطلًا أو مكتوبًا يدويًا أو قديمًا، مرّر "lxml" أو "html5lib" صراحة — فهما حصلا على 15/15 كاملة في مصفوفة HTML المتعطلة، بينما حصل html.parser على 12/15.
هل يمكن تشغيل BeautifulSoup بالتوازي عبر الخيوط؟
لا. بناء الشجرة في bs4 يتم بالكامل في Python ويمسك GIL، لذلك إضافة الخيوط تجعلها أبطأ لا أسرع — وفي الاختبار، أربعة خيوط جعلت تحليل صفحة 1 MB أبطأ بنحو 3.9 مرات من خيط واحد. إذا أردت موازاة bs4، فاستخدم multiprocessing (ProcessPoolExecutor). أما المكتبات ذات النوى المكتوبة بـ C، مثل selectolax وlxml، فهي التي تستفيد من التوازي على مستوى الخيوط.
هل تتعامل BeautifulSoup جيدًا مع HTML المتعطل؟
نعم، على نطاق واسع — عبر مجموعة من العينات المتعطلة (وسوم متداخلة خطأ، غياب الهيكل الأساسي، سمات بلا اقتباس، وغير ذلك)، استعاد المحركات الثلاثة النتائج بشكل نظيف. نقطة الضعف الوحيدة هي html.parser الافتراضي مع العناصر ذات الوسم الختامي الاختياري: فـ <td> و<li> غير المغلقة تُعشّش بدل أن تُغلق، ما يفسد النص المستخرج. بدّل إلى lxml أو html5lib ويزول هذا النوع من المشكلة.
BeautifulSoup مقابل lxml — أيهما أفضل؟
هما أداتان مختلفتان. lxml أسرع بكثير في بناء الشجرة والاستعلام، ويدعم XPath. أما BeautifulSoup فتغلف lxml (وغيره) بواجهة أكثر ودًا، كما أن لديها تغطية CSS أوسع عبر soupsieve. فقط لا تتوقع أن يجعل محرك lxml bs4 بسرعة lxml — فالمحرك الخلفي يسرّع التحليل فقط، بينما يظل الاستعلام والتنقل يدفعان كلفة كائن Python لكل عقدة، ما يبقيها أبطأ بنحو 6–7.5 مرات في اختيارات الدُفعات الكبيرة.
جرّب Thunderbit لاستخراج بيانات الويب Get Started Free


