Mozilla's Readability هو إصدار JavaScript مستقل لأداة الاستخراج التي تقف وراء Firefox's Reader View. ويُنشر كحزمة Apache-2.0 باسم @mozilla/readability، ويختار محتوى المقال من DOM حيّ. لذلك فهو يحتاج في Node إلى تنفيذ DOM مثل jsdom. وهو لا يجلب الصفحات، ولا يشغّل JavaScript الخاص بها، ولا ينفذ استخراجًا قائمًا على المخطط.
الإصدار الذي خضع للاختبار هنا هو npm latest عند 0.6.0، والمنشور في 3 مارس 2025، وكان عدد نجوم المستودع 11,361 عندما راجعته في 27 يوليو 2026 (وكان آخر دفع في 9 يوليو 2026، لذا فإن main متقدم كثيرًا على الحزمة المنشورة). شغّلته على jsdom 29.1.1 وNode v22.22.3 على macOS arm64، مقابل 22 عينة HTML مُعلّمة بُنيت لهذا الغرض، وكل القياسات الواردة هنا جاءت من هذه البيئة. ومن ناحية الاستخدام العملي، فهو أقل الأدوات تطلبًا في هذه الفئة: دقيقتان للتثبيت، بلا ملفات ثنائية، بلا متصفح مقيم في ذاكرة التخزين المؤقت، وبمخرجات متطابقة في كل إعادة تشغيل. وما يجعله مثيرًا للاهتمام ليس تشغيله نفسه، بل أن الإخفاقات فيه يمكن التنبؤ بها من عدد قليل من الثوابت في المصدر، وأحد تلك الثوابت يحدد أكثر مما توحي به الوثائق.
عبر 22 عينة اصطناعية مضبوطة، استعاد Readability جميع كتل المقال الموسومة وعددها 74. لكن هذه النتيجة المحدودة لا تعني أنه لا يفقد نص المقال أبدًا: فمؤشر الاسترجاع في اختبار الصفحات الحقيقية العام هو 0.982، ولم تتكرر هنا أنماط الأخطاء المعروفة. وكان أوضح فشل في العينة هو قبول محتوى شقيق إضافي بسبب بوابة كثافة الروابط في المصدر عند 0.25. وتختلف شدة هذا الأثر الظاهر بحسب بيئة الاختبار.
ما هو readability.js فعليًا، وثلاثة أشياء ليس كذلك
Readability هو مرحلة تصنيف قائمة على القواعد فوق DOM. فهو يمر على العناصر المرشحة، ويمنح كل عنصر درجة للمحتوى، ثم يرفع هذه الدرجات إلى العناصر الأبوية، ويختار الشجرة الفرعية الأعلى درجة، ثم يجري تمريرات تنظيف لإزالة ما يبدو كأنه عناصر واجهة الصفحة. هذه هي استراتيجية استخراج المقال كاملة — بلا نموذج، بلا بيانات تدريب، وبلا قواعد خاصة بكل موقع. وهذا التصميم هو سبب نجاحه على صفحة لم يرها من قبل — وهو أيضًا سبب أن إخفاقاته يمكن التنبؤ بها من المصدر، وهذه هي النقطة الممتعة.
وثلاثة أشياء ليس هو، وهي ثلاث نقاط تربك الناس:
- ليس أداة جلب. فهو يأخذ
documentوليس عنوان URL. الجلب، وإعادة المحاولة، ومكافحة الحظر الآلي، والرؤوس هي مسؤوليتك. - ليس أداة عرض. لا يوجد تنفيذ JavaScript. ما يحتويه DOM الذي تمرره إليه هو ما يراه.
- ليس أداة استخراج بنيوي. ستحصل على
titleوbylineوexcerptوcontent(HTML) وtextContentوlengthوsiteName. لا مخطط، ولا صفوف نوعية، ولا{name, price}.
أربعة ثوابت تقوم بمعظم العمل

قراءة Readability.js داخل node_modules تكشف لك عن السلوك أكثر مما تفعل أي صفحة وثائق. هناك أربع آليات مسؤولة عن معظم ما تفعله المكتبة:
- درجة المحتوى لكل فقرة مُقيّمة:
1 + (commaCount + 1) + min(floor(len / 100), 3). الفقرات الأقل من 25 حرفًا لا تُحتسب إطلاقًا. وتنتقل الدرجات إلى الأبوية عبر معاملات تقسيم: يحصل الأصل على الدرجة كاملة، والجد على النصف، والأجداد الأبعد علىlevel · 3. DEFAULT_CHAR_THRESHOLD = 500— الحد الأدنى لطول المقال من أجل تحليل "ناجح". إذا كان أقل من ذلك، تُعاد عملية الالتقاط بمرشح إزالة العلامات مع تمريرات تنظيف أقل.- التعبير النمطي
unlikelyCandidates— يطابق مقاطع مثلcommentوfooterوmenuوrelatedوsidebarوsocialوsponsorفي الفئة وid. العقد المطابقة تُحذف قبل التقييم. - بوابة إلحاق الأشقاء في
grabArticle— بعد اختيار المرشح الأعلى، تُفحص عناصره الشقيقة لاحتمال ضمّها. يُضمّ الشقيق إذا تجاوزت درجته العتبة، أو إذا كانnodeLength > 80 && linkDensity < 0.25، أو إذا كانnodeLength < 80 && nodeLength > 0 && linkDensity === 0 && it contains a period.
كثافة الروابط هي Σ(linkText.length · coef) / textLength, حيث coef = 0.3 للروابط ذات # فقط، و1 لغير ذلك. وهذه البوابة تفسر تسرب العناصر الشقيقة الذي قِيس هنا؛ لكنها ليست الخوارزمية الكاملة للاستخراج.
الإعداد، والتبعية غير المذكورة في العرض التسويقي
npm install @mozilla/readability jsdom وستكون جاهزًا. دقيقتان، بلا ملفات ثنائية، بلا تنزيل بعد التثبيت، بلا متصفح يعيش في الكاش. من ناحية التثبيت، هذا قريب جدًا من المثالي.
لكن عبارة "بلا تبعيات" تتعلق بالخوارزمية لا ببيئة التشغيل. Readability يعمل على document حيّ، وهذا في Node يعني أنك توفر تنفيذ DOM بنفسك — وهنا كان jsdom، بالإصدار 29.1.1. وjsdom ليس صغيرًا، وفي معظم خطوط المعالجة سيكون كلفته هي الأكبر في الحلقة، لا الاستخراج. احسب له حسابه.
وهناك مصيدة صغيرة كلفتني إعادة تشغيل: Readability.parse() يغيّر DOM الممرر إليه. إذا حللت مستند jsdom نفسه مرتين، فسترى في الاستدعاء الثاني مستندًا كانت الأول قد مزقته بالفعل. في كل مرة في بيئتي الاختبارية كان يتم بناء jsdom جديد. إذا كنت تكرر على الصفحات وتعيد استخدام كائن document لتوفير الوقت، فهذه هي العلة التي ستفتح لها تذكرة لاحقًا.
كيف اختبرته
لم أوجهه إلى مواقع أخبار حية. الصفحات الحية تمنحك نتيجة بلا وسيلة لمعرفة لماذا — ومع أسلوب heuristic، فإن "السبب" هو القيمة الأساسية كلها. لذلك أنشأت 22 عينة HTML تحتوي على 91 كتلة معلّمة (74 مقالًا، 17 عنصرًا تمهيديًا/حشويًا)، بحيث تُسبق كل كلمة في كل كتلة بسلسلة sentinel فريدة تخص تلك الكتلة. وتكون القواميس بين الكتل منفصلة، لذا فإن أي رمز مستخرج يطابق كتلة واحدة فقط، وتصبح عمليتا "استرجاع" أو "تسرب" اختبار انتماء دقيقًا بدلًا من مطابقة ضبابية.
تم فصل خطوة الاستخراج عن خطوة التقييم عمدًا. فبرنامج Node يُخرج فقط النص الخام المستخرج، وisProbablyReaderable القيم المنطقية، وكثافات الروابط المقاسة. ثم تُحسب جميع مقاييس precision وrecall لاحقًا من ذلك النص الخام مقارنة بالوسوم، بواسطة سكربت منفصل. لا توجد أي ثابتات للمقاييس مكتوبة يدويًا في أي موضع داخل harness، وهذه هي الطريقة الوحيدة التي أثق بها بأرقامي.
ثم مررت البيانات ذاتها تمامًا إلى trafilatura 2.1.0 للمقارنة على البيئة نفسها. تم تحليل كل عينة ثلاث مرات؛ وجميع الـ 22 أعادت نصًا متطابقًا على مستوى البايت في كل تشغيل.
يمكنك فحص كل مرحلة بدلًا من الاكتفاء بجدول الملخص. يقوم tests/build_fixtures.mjs بإنشاء HTML المشروح والحقيقة الأرضية؛ ويسجل tests/run_readability.mjs الاستخراج ومخرجات المتنبئ؛ ويقيس tests/metrics.py تلك السجلات بعد التنفيذ. وتُحفظ مخرجات Readability الخام، والمقاييس المحسوبة، والمقارنة على نفس المدخل في artifacts/raw/. هذا الفصل مهم عندما تبدو النتيجة مشبوهة: يمكنك حينها معرفة ما إذا كان المحلل قد أرجع نصًا غير متوقع، أو كانت مجموعة الوسوم خاطئة، أو أن كود القياس صنّفها بشكل غير صحيح. التكرار على هذه الحزمة يختبر الادعاءات الواردة هنا، لكنه يظل اختبارًا للحarness — وليس دليلًا على أن صفحات النشر الواقعية ستملك نفس توزيع الأخطاء.
والحدّ النطاقي هنا حقيقي وأساسي: هذه صفحات اصطناعية مضبوطة، وليست corpus واقعية. أما أرقام الصفحات الحقيقية المعتمدة فمصدرها العام article-extraction-benchmark، الذي يقيس readability_js 0.6.0 — وهو نفس الإصدار المختبر هنا — عند word-F1 0.947 ± 0.005 (precision 0.914 ± 0.008, recall 0.982 ± 0.003) عبر نحو 181 صفحة حقيقية. أذكر ذلك ولا أعيد إنتاجه هنا.
استُخدمت هنا صفوف benchmark الخاصة بالإصدارات الحالية؛ أما الصفوف التاريخية المستبدلة فقد استُبعدت. وتضيف العيّنات المضبوطة تفكيكًا لكل كتلة يوضح أي نمط محتوى يفعّل أي قاعدة، بدلًا من أن تحل محل corpus الصفحات الحقيقية العام.
كان الاسترجاع مثاليًا في حزمة العينات الاصطناعية
74 من 74. عبر جميع الـ 22 عينة، لم يُسقط Readability كتلة مقال موسومة واحدة — وفي العينات الاصطناعية النظيفة الأحد عشر التي مزجت بين المقال والعناصر الحشوية، بلغ recall على مستوى الرموز الميكروي 1.000. لم يغب أي سطر من نص المقال.
لكن يرافق ذلك تنبيهان:
هذه صفحات اصطناعية نظيفة أحادية العمود. في المقالات الواقعية تكون البنية أعمق، والإعلانات قد تتداخل في منتصف النص، وأحيانًا تُفقد الفقرة الافتتاحية بسبب أثر من آثار التقييم — وهذه الفئة من الفقد موثقة في tracker (#437، #901، وكذلك فقدان المحتوى قبل الجدول في #922). عيناتي لم تُحفّز أيًا من ذلك، لذلك لا أزعم أنه أصلح — بل أقول إن اختباري لم يصل إليه. وعلى الصفحات الواقعية يبلغ recall لهذا الإصدار 0.982 وليس 1.000.
ومع ذلك، فالجهة المفيدة في النتيجة هي الاتجاه. مشكلة Readability ليست أنه يرمي مقالك. المشكلة فيما يجلبه معه.
رقم الدقة، ولماذا يحتاج ثلاثة أوصاف

من السهل اقتباس رقم واحد وصعب الدفاع عنه. في العينات المختلطة الأحد عشر، أبقى Readability 5 من 17 كتلة تمهيدية/حشوية — بمعدل تسرب 0.294.
هذا ليس معدل تسرب حقيقيًا في العالم الواقعي. هناك ثلاث بيئات مختلفة تقيس ثلاث أشياء مختلفة، وواحدة فقط منها تصف الصفحات العادية:
| ما الذي يقيسه الرقم | النتيجة |
|---|---|
| مجموعة عينات موزونة هجوميًا — 6 من أصل 11 صفحة مختلطة صُممت خصيصًا لكسر بوابة الأشقاء | الاحتفاظ بـ 5 من 17 كتلة تمهيدية (0.294) |
الصفحة الواقعية الواحدة — جسم <article> محاط بـ nav وشريط إعلاني وsidebar وتعليقات وتذييل، إضافة إلى ترويج محايد الفئة | حُذفت 5 من 6 كتل الواجهة؛ وبقيت 1 |
| حوالي 181 صفحة حقيقية، benchmark العام (ليس تشغيلي هنا) | precision 0.914، recall 0.982، word-F1 0.947 — readability_js 0.6.0 |
اقرأ الصف الأول على أنه اختبار تحمّل، لا توقعًا. Readability لا يتسرب منه 29% من العناصر الحشوية في الواقع. وعلى الصفحة الواقعية، أُزيلت كل الكتل التي تحمل فئة يطابقها التعبير النمطي unlikelyCandidates — nav-menu وad-banner وsidebar وcomments وsite-footer — بشكل نظيف، أي الخمسة جميعًا. والناجي الوحيد كان الكتلة التي صممتها لتتجنب ذلك التعبير.
بوابة 0.25: حيث يتوقف حذف الحشو
قاعدة إلحاق الأشقاء موثقة في المصدر. لكن ما لم يجرِ قياسه، في حدود ما وجدت، هو النقطة الدقيقة التي تنقلب عندها. لذلك بنيت تدرجًا: عنصر <p class="teaser-block"> محايد الفئة يقف خارج <article>، ومقالًا حاسمًا من أربع فقرات مضمونًا أن يفوز كمرشح أول، ولا شيء يتغير سوى طول الترويج وكثافة روابطه. وتُحسب الكثافات بصيغة Readability نفسها، مقاسة وقت التشغيل لا مفترضة:
| كتلة الترويج | طول النص الداخلي | أكثر من 80 حرفًا | كثافة الروابط المقاسة | النتيجة |
|---|---|---|---|---|
| بلا روابط إطلاقًا | 126 | نعم | 0.000 | أُبقي عليها |
| رابط قصير واحد | 126 | نعم | 0.143 | أُبقي عليها |
| رابط أطول واحد | 126 | نعم | 0.278 | أُزيلت |
| نصف النص مرتبط | 126 | نعم | 0.476 | أُزيلت |
| جملة واحدة تنتهي بنقطة | 60 | لا | 0.000 | أُبقي عليها |
| النص نفسه، بلا نقطة | 59 | لا | 0.000 | أُزيلت |
شرط المصدر يستخدم عتبة 0.25؛ وقد أحاطت به العينات المقاسة، حيث بقيت قيمة 0.143 وأُزيلت 0.278. وفرع آخر أبقى جملة بطول 60 حرفًا تنتهي بنقطة وأزال نسخة بطول 59 حرفًا بلا نقطة. وظل استرجاع المقال 4/4 في كل ذراع، لذا تعزل هذه العينات أثرًا على الدقة فقط.
خارج harness الاختبار، تعني هذه البوابة فعليًا: النص الطويل المحايد ذو الروابط القليلة المجاور للمقال هو مقال. وهذا يصف أشياء كثيرة ليست المقال نفسه — فقرة "قراءات ذات صلة" مكتوبة كنص، أو دعوة للاشتراك في النشرة، أو ملاحظة تحريرية، أو teaser مدفوع كتبه فريق التسويق بجمل كاملة ثم أزيلت الروابط منه لأسباب تتعلق بالتتبع.
وفي فهرس RAG، يمكن أن تتحول فقرة ترويجية منخفضة الروابط إلى مقطع مستخرج، فتخلق خطرًا بأن يعاملها الاسترجاع أو التوليد كمحتوى مقال. وتُظهر قاعدة المصدر كيف يصبح هذا النمط ممكنًا؛ أما هذه المراجعة فلم تشغّل تقييمًا كاملًا للاسترجاع أو اقتباس النموذج.
وللفلاتر الصلبة الخاصة بالموقع، أزل مسبقًا الحاويات المعروفة في DOM المصدر، واحتفظ بسلسلة الأبوين للعقد المصدرية للمقارنة قبل التسلسل، أو طبّق بعد ذلك فلترة نصية مضبوطة ومتحققًا منها بعناية. فقد لا يحفظ HTML العائد وحده بعد الآن ما إذا كانت العقدة كانت أصلًا خارج الحاوية الرئيسية. ولا يمكن ضبط بوابة الأشقاء عبر الخيارات العامة.
ثلاث افتراضات ناقضتها العينات
ناقضت العينات ثلاثة افتراضات: أن charThreshold يرفض المقالات القصيرة، وأن الوسوم الدلالية ضرورية، وأن المحتوى القصير غير النثري يُحذف. وما يلي هو الجزء المهم من الأدلة؛ لا حاجة لادعاء preregistration.
charThreshold = 500 ليس منحدرًا حادًا
القراءة الشائعة تقول إن المقال الذي يقل عن 500 حرف يُعيد null. هذا غير صحيح. جربت طول الجسم من 120 إلى 1500 حرفًا مقابل قيم charThreshold 200 و500 و1000:
| طول النص | نجح التحليل عند كل العتبات | طول النص المستخرج |
|---|---|---|
| 120 | نعم | 161 |
| 300 | نعم | 342 |
| 460 | نعم | 509 |
| 520 | نعم | 569 |
| 800 | نعم | 841 |
| 1500 | نعم | 1555 |
ثبات كامل. طول مستخرج متطابق عبر جميع إعدادات العتبة الثلاثة، في كل حجم نص. العتبة لا تتحكم في قيمة الإرجاع — بل تقرر ما إذا كان سيُعاد التقاط المحتوى مع إزالة بعض علامات التنظيف، وعلى صفحة نظيفة لا يوجد ما يُزال، لذلك تعود النتيجة نفسها في الحالتين. والحد الحقيقي لـ null هو: "لا يوجد نص قابل للاستخراج أصلًا".
وهذا يخلق الفشل الحقيقي هنا، وهو أسوأ من null الزائف. أعطيته صفحة شبه فارغة — شريط تنقل ونبذة من أربع كلمات. فنجح في الإرجاع، و"المقال" الذي أرجعه تضمن شريط التنقل. عندما لا يوجد مقال حقيقي، يسلمك Readability عناصر حشوية موسومة على أنها المقال. وإذا كنت تزحف على نطاق واسع وتتعامل مع النتيجة غير الفارغة على أنها "هذه الصفحة كان فيها محتوى"، فذلك افتراض خاطئ.
الوسوم الدلالية ليست هي التي تقوم بالعمل
كنت أتوقع هبوط الاسترجاع عند إزالة الهيكل الداعم. نفس نص المقال، بغطاءين: واحد فيه <main><article><h1> وأسماء فئات وصفية، وآخر فيه <div class="x1"> وفقرات على شكل <div> عادي. النتيجة: استُعيدت 4 من 4 كتل مقال في الحالتين، ولم يتسرب أي حشو في الحالتين. عندما يكون المقال هو بوضوح الكتلة النصية الأكثر كثافة في الصفحة، فإن نظام التقييم القائم على الطول والفواصل يلتقطه دون مساعدة دلالية. مقولة "Readability يحتاج وسوم <article>" هي مجرد فولكلور.
والحد الصريح لهذا الادعاء: كانت صفحتي تحتوي على كتلة محتوى واضحة واحدة. أما حيث قد تملك الدلالات قيمة فعلية فهو في صفحة فيها شجرتان فرعيتان كثيفتان متنافستان، ولم أختبر حسم هذا التعادل.
المحتوى غير النثري يبقى كاملًا
جعلتني قاعدة "الفقرات الأقل من 25 حرفًا لا تُحتسب" أتوقع خسائر في الجداول والتعليقات التوضيحية. لكن ذلك كان خاطئًا مرة أخرى — فهذه القاعدة تؤثر على التقييم لا على الاحتفاظ. وما إن تفوز الحاوية، حتى يأتي كل ما بداخلها معها:
| نوع المحتوى داخل المقال | Readability | trafilatura |
|---|---|---|
| فقرات نثرية (×2) | أُبقي عليها | أُبقي عليها |
| خلايا جدول بيانات (×2) | أُبقي عليها | أُبقي عليها |
كتلة كود <pre> | أُبقي عليها | أُبقي عليها |
فقرة <p> قصيرة جدًا تحت 25 حرفًا (×2) | أُبقي عليها | أُبقي عليها |
<figcaption> | أُبقي عليها | حُذفت |
| الإجمالي | 8/8 | 7/8 |
وهذا محور يخسر فيه المنظف الأكثر قسوة. إذا كانت صفحاتك توثيقًا أو شروحات أو أي شيء فيه كتل كود وصور مع تعليقات توضيحية، فإن سلوك Readability في الاحتفاظ بكل الشجرة الفائزة هو ميزة.
isProbablyReaderable تقول لا بينما parse() يقول نعم

يوحي ملف README باستدعاء isProbablyReaderable(doc) كفحص سريع قبل الالتزام بتحليل كامل. في اختباراتى، رفضت هذه البوابة ثلاثة أشكال مختلفة من الصفحات تعامل معها parse() لاحقًا بلا مشكلة:
| شكل الصفحة | حكم المتنبئ | parse() | ما الذي يصلحه |
|---|---|---|---|
المحتوى موجود فقط داخل عناصر <li> | false | ينجح | لا شيء — false عند كل minScore من 1 إلى 80 وعند كل minContentLength من 40 إلى 200 |
| عشر فقرات، كل منها أقل من 140 حرفًا | false | ينجح | minContentLength ≤ 100 (minScore لا يفعل شيئًا) |
| فقرة واحدة بطول 408 أحرف | false | ينجح | minScore ≤ 10 (الدرجة ≈16.4) |
| مقال عادي (ضبط) | true | succeeds | — |
الإخفاقات الثلاثة لها ثلاثة أسباب مختلفة، واثنان فقط منها قابلان للضبط. حالة <li> بنيوية: فالمتنبئ لا يمنح درجات إلا لعقد p وpre وarticle (إضافة إلى الآباء div > br)، لذا فصفحة يعيش محتواها داخل عناصر قائمة لا تطابق شيئًا، وتكون درجاتها صفرًا، ولا يعيد أي ضبط للعتبة حلّها — وهذا شكل موثق بالفعل في issue #662. أما حالة الفقرات القصيرة الكثيرة فهي بوابة minContentLength تتجاوز كل فقرة قبل تقييمها، لذا فإن عشر فقرات معتبرة مجموعها يساوي صفرًا؛ وخفض هذه القيمة يصلح الأمر، بينما تعديل minScore لا يفعل. أما حالة الفقرة الواحدة فهي مسألة حسابية: الدرجة هي sqrt(408 − 140) ≈ 16.4، أي دون minScore الافتراضي 20 — وتحتاج الفقرة الوحيدة إلى 540 حرفًا (140 + 20²) لتتجاوز العتبة بمفردها.
ويحذّر README بالفعل من أن المتنبئ يعطي سلبيات كاذبة. وما أضيفه هو القاعدة العملية: لا تستخدمه بوصفه الحارس الوحيد. إذا كانت الصفحة مهمة، فحللها ثم افحص طول النتيجة. فعملية التحليل نفسها ليست مكلفة جدًا مقارنة ببناء jsdom الذي دفعت ثمنه أصلًا.
نفس البيانات، مستخرجان مختلفان
تشغيل trafilatura 2.1.0 على نفس العينات يعطي قراءة أوضح من مقارنة رقمين قيسا على بيئتين مختلفتين، لأن المدخل نفسه متطابق على مستوى البايت:
| المقياس (11 عينة مختلطة) | @mozilla/readability | trafilatura |
|---|---|---|
| استرجاع كتل المقال | 1.000 | 1.000 |
| الكتل الحشوية المحتفظ بها | 5/17 (0.294) | 1/17 (0.059) |
| Token F1 (ميكروي) | 0.948 | 0.969 |
| استرجاع المحتوى غير النثري | 8/8 | 7/8 |
| مقال قصير جدًا (120 حرفًا)، Token F1 | 0.800 | 0.571 |
ولا يتفوق أحدهما بشكل مطلق على هذه العينات. فقد احتفظ trafilatura بعدد أقل من الكتل الشقيقة، بينما احتفظ Readability بمحتوى قصير وغير نثري أكثر. وتتأثر الدقة المطلقة النصية في كلا الأداتين بانحياز وجود عناوين غير موسومة، لذا فإن عدّ التسرب على مستوى الكتل هو الإشارة المباشرة الأنظف. أما benchmark العام للصفحات الحقيقية فيرتب word-F1 بينهما بشكل مشابه، لكن corpora والمقاييس مختلفة، وهذا ليس تحققًا عبر بيئات الاختبار.
أما من ناحية المتانة، فقد شغّلت نسخة متعمدة التشوه من الصفحة المرجعية (وسوم <p> غير مغلقة، ووسوم <b>/<i> متداخلة خطأً، و</div> شاردة) وكان الاسترجاع 3/3 بلا أي تسرب، مطابقًا للنسخة السليمة. والفضل في ذلك يعود إلى jsdom وبنّاء شجرة HTML5 الخاص به، الذي يصلح الفوضى قبل أن يراها Readability أصلًا. ولم تتسبب أي عينة في انهيار المحلل.
الإيجابيات والسلبيات
الإيجابيات
- استرجاع المقال هو الجانب الأقوى: 74/74 من الكتل الموسومة تم استردادها عبر 22 عينة اصطناعية، وrecall على مستوى الرموز 1.000 في المجموعة المختلطة.
- إزالة عناصر الصفحة ذات الفئات المطابقة للتعبير النمطي تتم بثبات — اختفى nav وشريط الإعلانات وsidebar والتعليقات والتذييل كلها في الصفحة الواقعية (5 من 6).
- يُحتفظ بالمحتوى غير النثري بالكامل: الجداول و
<pre>والكابتions وحدود الأسطر القصيرة تحت 25 حرفًا نجت جميعًا (8/8)، بينما حذفت trafilatura أحد التعليقات التوضيحية. - لا يعتمد على وسوم دلالية — فقد حصل
<div>محايد للمقال على الدرجة نفسها التي حصل عليها إصدار<article>/<main>. - لا تُرفض المقالات القصيرة بشكل خاطئ: استُعيد محتوى نظيف بطول 120 حرفًا، وبنفس الطول المستخرج عبر قيم
charThreshold200/500/1000. - حتمي بالكامل: أعادت جميع العينات الـ 22 نصًا متطابقًا عبر ثلاث تشغيلات.
- التثبيت خلال دقيقتين، وApache-2.0، والإصدار على npm هو نفسه الذي اختبرته (0.6.0)، لذا لا شيء هنا قديم.
السلبيات
- بوابة إلحاق الأشقاء قابلة للاستغلال: النص الترويجي الطويل قليل الروابط والمحايد الفئة لا يمكن تمييزه عن نص المقال ويلتحق به عند
linkDensity < 0.25. - في الصفحات الفقيرة بالمحتوى، قد يعيد الحشو بوصفه المقال بدلًا من
null— فالعينة شبه الفارغة عادت بشريط التنقل كجسم المقال. isProbablyReaderableينتج سلبيات كاذبة على ثلاثة أشكال مختلفة من الصفحات، وأحدها لا يصلحه أي ضبط.- يحتاج إلى DOM كامل وقت التشغيل — وصياغة "بلا تبعيات" تخفي كلفة jsdom، وهي التي تهيمن على الحلقة.
parse()يغير المستند الممرر إليه، لذا يجب إعادة بناء DOM لكل صفحة.- لا جلب، لا عرض JavaScript، ولا مخرجات بنيوية. إنه مرحلة واحدة من خط معالجة، وليس الخط كله.
- حالات الفقد في الصفحات الحقيقية المبلغ عنها في tracker (الفقرة الافتتاحية وما قبل الجدول) لم تتكرر على عيناتي، لذلك لا أستطيع أن أقول إنّها نادرة أو أن صفحاتي ببساطة لم تصل إليها.
من ينبغي أن يستخدمه، ومن ينبغي أن يتجاوزه
اختر Readability إذا كان HTML لديك جاهزًا بالفعل وتريد استخراج المقال منه بحتًا في JavaScript، داخل خدمة Node حيث قد يكون إدخال تبعية Python أمرًا مزعجًا. (لم أجمع التوقيت كتوزيع إحصائي صحيح، لذا لا أقدّم أي ادعاء سرعة يتجاوز "بناء jsdom هو العنصر المهيمن في الحلقة، لا الاستخراج.") ميزات وضع القراءة، وأرشفة المقالات بلا اتصال، ونشرات البريد، وأزرار "عرض نظيف"، وإضافات المتصفح، وخطوط معالجة التوثيق التي تحتوي على كتل كود وتعليقات توضيحية — هذا هو مجاله، وأرقام الاسترجاع تؤكد أنه مجال جيد. وسلوكه أيضًا قابل للقراءة مباشرة من المصدر، وهذا أهم مما يبدو عندما تحتاج إلى شرح سبب وصول كتلة معينة بالذات إلى زميلك.
وتجاوزه إذا كانت دقة إزالة الحشو هي المعيار الذي تُقاس عليه، وخاصة إذا كنت تغذي فهرس LLM حيث تتحول الفقرة الترويجية الشاردة إلى مقطع يمكن استرجاعه. وتجاوزه إذا كانت صفحاتك تعرض المحتوى من جهة العميل، لأنه يقرأ الـ DOM الذي تمنحه إياه فقط ولا يشغّل JavaScript. وتجاوزه إذا كنت تحتاج إلى {title, price, sku} بدلًا من النثر — فلا إعداد يحول مستخرج محتوى إلى مستخرج مخططات. وإذا كنت تعالج صفحات يكون فيها السؤال الحقيقي هو "هل احتوت هذه الصفحة على مقال أصلًا؟"، فلا تثق بقيمة غير فارغة كجوابك.
البدائل، وموقع Thunderbit في هذا المشهد
لا شيء هنا ينتقص من مكتبة مجانية Apache-2.0 صونتها Mozilla — Readability بنية تحتية، وهو يعمل داخل Firefox منذ سنوات، ويمثل مرجعًا في استخراج وضع القراءة لسبب وجيه. وإذا أردت أوسع المجال، فأنا أحتفظ بمقارنة مستمرة في open-source scrapers roundup واستعراض أوسع في the best web scraping tools.
وللحصول على نفس العينات عبر جميع المستخرجات الستة، راجع six-library extraction comparison.
ملاحظة من المؤلف: Thunderbit هو خيارنا المُدار لعمليات العرض والاستخراج عندما يكون الإدخال URL. لم يُشغَّل على هذه العينات، لذا لا يُفهم من ذلك أي ادعاء جودة مطابق. والحدّ المهم هنا هو: هل لديك أصلًا DOM وتريد استخراجًا محليًا للمقال، أم أنك تريد الجلب/العرض والمخرجات البنيوية كخدمة مُدارة؟ الاستضافة الذاتية تتجنب رسوم استخدام المورّد، لكنها تبقى مع تكاليف البنية والصيانة.
والصفقة الصادقة هنا: Readability مجاني، شفاف، ومملوك لك عند التشغيل — يمكنك قراءة البوابة الدقيقة التي قررت مخرجاتك، وهذا ليس شيئًا تمنحك إياه واجهة API مُدارة. أما الحزمة المُدارة فتكلّف مالًا وتخفي الآلية، لكنها تغطي مراحل الجلب/العرض/البنية التي ستضطر لبنائها بنفسك. وإذا كنت تريد الطرف المدعوم بالذكاء الاصطناعي من هذا الطيف، فقد كتبت عن scraping أي موقع باستخدام AI وعن AI crawlers في مواضع أخرى. اختر بحسب المراحل التي تريد امتلاكها فعلًا.
جرّب Thunderbit لاستخراج بيانات الويب
الحكم النهائي
Readability خيار مناسب عندما يكون لديك DOM بالفعل، وتعمل بـ JavaScript/Node، وتفضّل أحيانًا وجود محتوى شقيق إضافي على أن تفقد نصًا مهمًا. في هذه الحزمة من العينات استعاد جميع الكتل المقالية الـ 74، واحتفظ بالجداول والكود والتعليقات التوضيحية. وتبقى هذه النتيجة محدودة بصفحات اصطناعية أحادية العمود؛ فالاسترجاع في الصفحات الحقيقية العامة هو 0.982، ولم تتكرر الأخطاء المعروفة المرتبطة بالفقرة الافتتاحية/ما قبل الجدول، كما أن الصفحات الفقيرة بالمحتوى قد تعيد الحشو على أنه المقال.
فقط قدّر نقطة الضعف بدقة. فمساحة الفشل الرئيسية لديه في هذه العينات هي الدقة، وهي تقع عند سطر محدد وموثق في المصدر: أي شقيق يزيد طوله عن 80 حرفًا وتقل كثافة روابطه عن 0.25 يُلحق بمقالك، سواء أكان جزءًا منه أم لا. وقد رأيت هذا يتحول عند 0.143 مقابل 0.278 على نص متطابق. ويبلغ benchmark الصفحات الحقيقية precision 0.914 وrecall 0.982. وإذا كنت تدفع النص المستخرج إلى فهرس سيقتبس منه نموذج لاحقًا، فافحص كلًا من الحشو المحتفظ به والنص المفقود بدلًا من افتراض غياب أي من الخطأين.
جرّب Thunderbit لاستخراج بيانات الويب Get Started Free
الأسئلة الشائعة
هل يزيل Mozilla Readability كل الحشو؟
لا، والأمر يعتمد كثيرًا على ما الذي تقيسه. في benchmark الصفحات الحقيقية العام، يسجل readability_js 0.6.0 دقة 0.914 — أي إن نحو 8.6% مما يخرجه ليس من جسم المقال. وفي صفحة اختبار واقعية صممتها أنا أزال 5 من 6 كتل الواجهة (اختفاء nav وشريط الإعلانات وsidebar والتعليقات والتذييل جميعًا)، ولم يحتفظ إلا بفقرة ترويجية محايدة الفئة. أما في مجموعة عينات وزنتها عمدًا بكتل صُممت لكسر الهيوريستك، فقد أبقى 5 من 17 — وهذا الرقم الأخير اختبار ضغط، وليس معدلًا واقعيًا.
هل أحتاج إلى jsdom لاستخدام readability.js في Node؟
نعم، أو أي تنفيذ DOM آخر. Readability مكتوب بـ JavaScript خالص، لكنه يعمل على كائن document حي، لذا فأنت في Node توفر DOM بنفسك — وكان ذلك jsdom 29.1.1 في إعدادي. ووصف "بلا تبعيات" يشير إلى الخوارزمية، لا إلى بيئة التشغيل. ولاحظ أيضًا أن parse() يغير المستند الذي يُمرر إليه، لذا ابنِ DOM جديدًا لكل صفحة بدلًا من إعادة استخدام واحد.
ماذا تفعل option charThreshold فعليًا؟
ليس ما يعتقده معظم الناس. فهي لا تجعل المقالات القصيرة تعود null — فقد استعدت مقالات نظيفة بطول 120 حرفًا، وبنفس الطول المستخرج عبر قيم charThreshold 200 و500 و1000. تتحكم العتبة في ما إذا كان المحلل سيعيد تنفيذ الالتقاط مع إزالة بعض علامات التنظيف؛ وعلى صفحة نظيفة لا يوجد ما يُزال، لذا تكون النتيجة نفسها في الحالتين. أما حالة null الحقيقية فهي صفحة لا تحتوي أي نص قابل للاستخراج أصلًا، وحتى صفحة لا تحتوي سوى شريط تنقل عادت غير فارغة، وأرجعت شريط التنقل بوصفه المقال.
هل ينبغي أن أستدعي isProbablyReaderable قبل parse()؟
استخدمه كإشارة، لا كحاجز. فقد أعاد false على ثلاثة أشكال من الصفحات نجح معها parse() لاحقًا: محتوى داخل عناصر <li>، وعشر فقرات كل منها أقل من 140 حرفًا، وفقرة واحدة بطول 408 أحرف. حالة <li> لا تُحل بالضبط، لأن المتنبئ لا يقيّم إلا عقد p وpre وarticle؛ أما حالة الفقرات القصيرة الكثيرة فتحتاج minContentLength أقل؛ وحالة الفقرة الواحدة تحتاج minScore أقل، لأن الفقرة الوحيدة يجب أن تصل إلى 540 حرفًا لتتجاوز العتبة الافتراضية. إذا كانت الصفحة مهمة، فحللها ثم افحص النتيجة.
Readability أم trafilatura لاستخراج المقال؟
على نفس البايتات من العينات، احتفظ trafilatura بحشو أقل (1/17 كتلة مقابل 5/17)، بينما استعاد Readability محتوى أقصر أكثر، واحتفظ بـ <figcaption> الذي حذفته trafilatura. اختر بناءً على تحمل الخطأ والزمن. أما benchmark العام فهو سياق منفصل، وليس تحققًا من نتيجة هذه العينات.


