قلم (qalam): استخراج النص العربي من ملفات PDF بترتيبه الصحيح ودون OCR
تعيد معظم أدوات استخراج النصوص من ملفات PDF النصَّ العربي معكوسًا أو مشوّهًا دون أي تحذير. قلم (qalam) مكتبة مفتوحة المصدر من مسراج، نواتها بلغة Rust ولها واجهة Python، تستخرج النص العربي من ملفات PDF الرقمية بترتيبه الصحيح، وتخبرك بكل صفحة لم تستطع قراءتها.
خذ هذا السطر من دليل إرشادي صادر عن جهة حكومية سعودية، وهو التنبيه الذي تحمله هذه الأدلة عادة: «هذا الدليل إرشادي ولا يغني عن الرجوع إلى». اطلب من pdfium، المحرك الذي يعرض ملفات PDF داخل متصفح Chrome، أن يستخرجه، فيعيد إليك: «إلى الرجوع عن يغني ولا إرشادي الدليل هذا». كل كلمة مكتوبة صحيحة، وكل كلمة في غير موضعها، ولا رسالة خطأ واحدة.
لهذه المشكلة بنينا قلم (qalam)، وهي مكتبة مفتوحة المصدر من مسراج لاستخراج النص العربي من ملفات PDF الرقمية، أي الملفات التي أُنشئت على الحاسوب ولم تُمسح ضوئيًا. تعيد المكتبة النص بترتيب القراءة الصحيح وبحروفه الأساسية، وتحافظ على «لا» وعلى التشكيل، وإذا لم تجد في الصفحة طبقة نص صالحة قالت ذلك صراحة. ولا تستخدم أداة قلم تقنية OCR. نواتها مكتوبة بلغة Rust، ولها واجهة Python وأداة لسطر الأوامر، وتُثبَّت بأمر واحد: pip install qalam.
إن كنت تبني مدوّنات نصية عربية أو فهارس بحث أو أنظمة RAG، فالمسألة أخطر مما تبدو. النص العربي التالف لا يعلن عن نفسه: يمر عبر المقطِّع (tokenizer)، ويتحول إلى متجهات، ويُفهرس، ثم لا يطابق أبدًا ما يبحث عنه الناس فعلًا.
لماذا يفشل استخراج النص العربي من PDF؟
ملف PDF لا يخزّن نصًا، بل يخزّن تعليمات رسم: استخدم هذا الخط بهذا الحجم، وانتقل إلى هذه الإحداثيات، وارسم الرمز رقم 276. أرقام رموز بدل الحروف، ومواقع بدل الأسطر. وعلى أداة الاستخراج أن تعيد بناء النص من هذه التعليمات، مع أن ثلاثة أشياء تحتاجها لذلك اختيارية في هذا التنسيق: جدول تحويل الرموز إلى Unicode (/ToUnicode)، والبنية المنطقية للمستند، وترتيب القراءة الذي لا يُخزَّن أصلًا في الملفات غير الموسومة.
مع الإنجليزية تنجح الطريقة البسيطة في أغلب الأحيان، أما العربية فتصيب كل نقاط الضعف في وقت واحد:
- تُرسم الرموز من اليسار إلى اليمين بالترتيب المرئي، فإذا جُمعت بترتيب رسمها انعكست كل كلمة.
- كثير من برامج إنشاء PDF تخزّن الحروف بصيغ العرض (Presentation Forms)، أي أشكال الحرف في أول الكلمة ووسطها وآخرها، وهذه لا تشترك في أي رمز Unicode مع النص العربي العادي.
- الحرف المركّب مثل «لا» رمز واحد يمثّل حرفين.
- تُرسم علامات التشكيل داخل حدود الحرف الذي تتبعه، فإذا رُتّبت الرموز حسب مواقعها قد تنتقل العلامة إلى حرف آخر.
- الأعمدة المتجاورة تتداخل سطرًا بسطر إذا جُمع النص حسب خط الأساس.
- قد يكون جدول
/ToUnicodeمفقودًا أو خاطئًا، فيخرج نص تالف بصمت أو لا يخرج شيء على الإطلاق.
قبل وبعد: مخرجات حقيقية من ملف حكومي
كل الأمثلة التالية مخرجات فعلية من ملف PDF حكومي حقيقي، ويعيد إنتاجها السكربت scripts/compare.py في المستودع.
| المشكلة | أدوات الاستخراج الأخرى | قلم (qalam) |
|---|---|---|
| ترتيب القراءة | إلى الرجوع عن يغني ولا إرشادي الدليل هذا كل الكلمات معكوسة | هذا الدليل إرشادي ولا يغني عن الرجوع إلى ترتيب منطقي و«لا» سليمة |
| صيغ العرض | ﺍﻟﺪﻟﻴﻞ ﺍﻹﺭﺷﺎﺩﻱ لا يشترك في أي رمز مع النص الحقيقي | الدليل الإرشادي حروف أساسية |
| التشكيل | تتضم َّن مسافة في وسط الكلمة | تتضمَّن العلامات ملتصقة بحروفها |
| صفحة بلا طبقة نص | ""لا فرق بينها وبين صفحة فارغة | page.verdict == "needs_ocr"يخبرك بما لم يستطع قراءته |
الصف الثاني هو الأخطر لأنه لا يُرى. يبدو النصان متطابقين تقريبًا لعين القارئ، لكنهما لا يشتركان في رمز واحد عند الحاسوب، فلا يطابقهما المقطِّع ولا يجدهما محرك البحث. وفي مستند واحد من 44 صفحة أحصى المقال المرافق للمشروع 31,080 حرفًا بصيغ العرض في مخرجات مكتبة شائعة.
أما الصف الثالث فقد تتسبب فيه بنفسك: تطبيع NFKC لصيغة منفردة تحمل علامة تشكيل يضيف مسافة قبل العلامة، فحتى الاستخراج السليم ينتهي بمسافة داخل الكلمة إذا تلاه تطبيع غير مدروس.
كيف يعالج قلم (qalam) استخراج النص العربي؟
تعمل أداة قلم (qalam) على الرموز ومواقعها في الصفحة، لا على سلاسل نصية جاهزة. فبمجرد أن يتحول النص إلى سلسلة بترتيب خاطئ تضيع المعلومة اللازمة لإصلاحه، وهي موضع كل رمز في الصفحة. لذلك تقرأ الأداة تعليمات الرسم، وتحوّل كل رمز إلى Unicode، ثم تبني الأسطر بعد ذلك.
الترتيب أولًا، ثم التطبيع
تخطئ معظم الأدوات في ترتيب عمليتين. تطبيع NFKC يعيد صيغ العرض إلى الحروف الأساسية، وهذا مطلوب، لكنه يفكّك أيضًا الحرف المركّب «ﻻ» إلى حرفين. فإذا طبّعت أولًا ثم عكست الترتيب تبادل الحرفان موضعيهما، فتصير «ولا» «وال». الكلمتان عربيتان صحيحتان، والجملة تُقرأ بسلاسة، لكن كلمة فيها تغيّرت.
يرتّب قلم (qalam) النص والحرف المركّب ما زال رمزًا واحدًا، ثم يطبّعه، فتعبر «لا» عملية العكس سليمة وتتفكك في موضعها الصحيح. والقاعدة نفسها تشمل الخطوط التي تربط رمزًا واحدًا بعدة حروف، وقد وُجد منها أربعة عشر رمزًا في ملف واحد. تعامل أداة قلم كل رمز من هذا النوع وحدةً واحدة، ولا تعكس ما بداخلها أبدًا.
التشكيل والأعمدة والجداول
تبقى علامات التشكيل مرتبطة بحرفها طوال إعادة الترتيب، فتظل الشدّة والفتحة على الحرف الذي تنتمي إليه، ولا تظهر مسافة داخل الكلمة.
وفي الصفحات متعددة الأعمدة تستخدم أداة قلم (qalam) التقسيم المتكرر (Recursive XY-cut): يبحث عن أعرض فراغ في الصفحة، ويقسمها عنده، ثم يكرر العملية داخل كل جزء. بهذا يُفصل العنوان الممتد بعرض الصفحة قبل البحث عن الأعمدة تحته، ويخرج كل عمود كاملًا من اليمين إلى اليسار. وتتناسب عتبات الفراغ مع حجم الخط في كل موضع، ويُحدَّد اتجاه الأعمدة على مستوى الصفحة كلها، فلا يقلب عمودٌ من الأرقام اللاتينية ترتيبَ قراءتها.
أما الجداول المرسومة بخطوط فتعود شبكةً كما هي. وتُرقَّم أعمدتها بترتيب القراءة، فالخلية rows[0][0] في صفحة عربية هي الخلية اليمنى حيث يبدأ القارئ، ويمكن تمرير ناتج table.to_rows() مباشرة إلى csv.writer.
حكم واضح على كل صفحة
أخطر الأعطال هو الذي لا يُصدر أي خطأ. الصفحة الممسوحة ضوئيًا لا تحتوي رموزًا أصلًا، والصفحة التي تفتقد خطوطها جدول تحويل صالحًا لا يمكن فك رموزها. اطلب نص أيٍّ منهما من أداة استخراج شائعة وستحصل على سلسلة فارغة، وهي النتيجة نفسها التي تعطيها لصفحة فارغة فعلًا. وفي المستند التجريبي المذكور في المقال، وعدد صفحاته 44، توجد أربع صفحات لا يمكن استعادة نصها، ويعيد كلٌّ من pdfium وPyMuPDF سلسلة فارغة للصفحات الأربع دون أي إشارة.
مرّر عشرة آلاف مستند عبر خط معالجة كهذا، وستختفي الصفحات الممسوحة دون أثر. تنتهي المهمة بنجاح، ولا يستطيع أي نظام بعدها أن يميّز بين «هذا المستند لم يتناول الموضوع» و«فقدنا الصفحة 12».
لذلك تصدر أداة قلم (qalam) حكمًا على كل صفحة:
| الحكم | المعنى |
|---|---|
ok | تحوّلت كل الرموز، ويمكن الوثوق بالنص. |
degraded | خرج نص، لكن فيه خللًا، وتجد السبب في page.reasons. |
needs_ocr | لا توجد طبقة نص صالحة، وإخراج أي نص هنا أسوأ من عدم إخراجه. |
في هذا التقييم قراران مقصودان. الأول أن ok يشترط تحويل كل الرموز دون استثناء، لأن كل رمز لم يتحوّل حرفٌ ناقص من الناتج، ولهذا تبقى الصفحة التي تحوّل فيها 99.9% من رموزها degraded. والثاني أن الصفحة الخالية من الرموز تحصل على 0.0 لا 1.0، فغياب الأخطاء لا يعني أن كل شيء نجح، والمقياس البسيط كان سيمنح الصفحة الممسوحة درجة كاملة.
نفضّل أداة تخبرك أنها لم تستطع قراءة الصفحة 42 على أداة تعطيك نصًا يشبه الصفحة 42.
البدء السريع
pip install qalamimport qalam
text = qalam.extract_text("guide.pdf")تعيد الدالة extract_text نص المستند كاملًا، ولا تضيف الصفحات الخالية من طبقة نص صالحة أي شيء، فلا تظهر صفحة ممسوحة على أنها نص. وللاطلاع على تفاصيل كل صفحة استخدم Document:
import qalam
doc = qalam.Document("guide.pdf")
print(doc.confidence) # 0.0 to 1.0
print(doc.pages_needing_ocr) # [2, 3, 42, 43]
for page in doc:
if page.verdict != "ok":
print(page.number, page.verdict, page.reasons)وكل صفحة أيضًا قائمة مرتّبة من الكتل (فقرات وجداول وصور)، ويمكن تصدير النتيجة نفسها عبر doc.to_html() وdoc.to_json(). والحزم من نوع abi3، فبناء واحد يعمل مع Python 3.9 وما بعده. وتجد واجهة Rust وأداة سطر الأوامر موثقتين في ملف README.
قلم (qalam) مع بصير OCR: لكل صفحة الأداة المناسبة
لا تقرأ أداة قلم الصفحات الممسوحة، وهذا اختيار مقصود. لكن الأرشيفات الحقيقية تخلط الصفحات الرقمية بالممسوحة، أحيانًا داخل الملف الواحد، والحكم على كل صفحة يقسم العمل بوضوح: تستخرج الأداة نص الصفحات التي تملك طبقة نص حقيقية، وتذهب أرقام الصفحات الواردة في doc.pages_needing_ocr إلى بصير OCR، منتج مسراج الذي يقرأ الصفحات الممسوحة وصور المستندات العربية ويعيدها Markdown منظمًا.
import qalam
doc = qalam.Document("archive.pdf")
text = doc.text # every page with a usable text layer
to_ocr = doc.pages_needing_ocr # the page numbers to send to Baseer OCRبهذا لا تضيع صفحة دون أن تعلم بها: تأخذ النص مباشرة حيث يوجد في الملف، ولا تشغّل OCR إلا على الصفحات التي تحتاجه. وإن كنت تختار نموذج OCR للجزء الممسوح، فراجع نتائج مقارنة نماذج OCR العربية.
ما لا تفعله أداة قلم (qalam)
- التعرف الضوئي على النصوص (OCR): يكتشف الصفحات الممسوحة ويُبلغ عنها، لكنه لا يقرؤها.
- إنشاء ملفات PDF أو تعديلها.
- محاكاة تخطيط الصفحة بدقة البكسل: الناتج كتل مرتّبة ومصنّفة، لا نسخة بصرية من الصفحة.
- الجداول غير المرسومة بخطوط: يعيد بناء الجداول ذات الخطوط فقط في الوقت الحالي.
- دمج أقنعة الشفافية (Soft mask): الشعار الذي يقع شكله كله في قناع الشفافية يُستخرج صحيحًا على مستوى البايتات لكنه يبدو فارغًا، ويخبرك بذلك الحقل
dropped_transparency.
ساعدنا في العثور على الملفات التي تُربكه
مشروع قلم (qalam) حديث، اختُبر على مجموعة صغيرة من المستندات، وكل قاعدة في خط معالجته وُجدت لأن مستندًا حقيقيًا أثبت خطأ القاعدة التي سبقتها. لذلك فأنفع مساهمة هي ملف PDF عربي يُستخرج نصه بشكل سيئ: مطبوعات حكومية، وتقارير مالية، ورسائل جامعية، وأي ملف فيه جداول. وإن لم تستطع مشاركة الملف، فناتج الأمر qalam inspect yourfile.pdf مفيد أيضًا.
- وجدت استخراجًا خاطئًا؟ افتح بلاغًا (Issue) وأرفق الملف إن أمكنك مشاركته.
- لست متأكدًا من ملاءمة أداة قلم لخط معالجتك؟ اسأل في نقاشات GitHub.
- تريد المساهمة بالكود؟ ابدأ بملف CONTRIBUTING.md، واقرأ سجل الملاحظات في
PLAN.mdالذي يوثّق ما تعلّمناه من كل مستند حقيقي.
الكود المصدري على GitHub، والحزمة على PyPI، والقصة الكاملة لكل عطل في المقال المفصّل (بالإنجليزية).