الوحدة 2 — استخراج النصّ من ملفّات PDF وHTML والمستندات المكتبية
الوحدة السابقة رسمت المعمارية العامّة: مرحلة فهرسة ثم مرحلة استفسار. تبدأ الفهرسة بالخطوة الأكثر تواضعًا في المظهر والأكثر خطرًا في الأثر: استخراج نصّ نظيف من ثلاثمئة مستند بتنسيقات مختلفة. المقولة المعروفة «مدخلات سيّئة، مخرجات سيّئة» تنطبق هنا حرفيًّا: نصّ رديء الاستخراج يعطي فهرسًا رديئًا مهما بلغ ذكاء ما يليه.
الفيل الأحمر: ثلاثة تنسيقات، ثلاثة اعتلالات
في مجموعتنا نجد بالعادة ثلاث عائلات: PDF (أكثريّة الإجراءات والمذكّرات)، وHTML (لائحة داخلية منشورة في بوّابة المؤسّسة)، ومستندات مكتبية (.docx و.pptx و.xlsx). لكلّ تنسيق أعطاله الخاصّة، وتجاهل هذه الاعتلالات يقود إلى فهرس ينسى الجداول ويكرّر الترويسات ويخلط ترتيب القراءة.
- PDF نصّي: قابل للنسخ بالماوس. المشكل الأشيع فيه ترتيب القراءة حين تكون الصفحة على عمودين، حيث تُخلَط الأسطر ما بينهما.
- PDF ممسوح: صور فقط، بلا نصّ. لا سبيل إلى تفاديه إلّا بـالتعرّف الضوئي على الحروف (OCR).
- HTML: كثير التلوّث بشرائط تنقّل ونصوص تذييل وإعلانات جانبية لا علاقة لها بالمحتوى.
- DOCX/PPTX/XLSX: مبنيّ على XML. النصّ متاح، لكنّ جداول Excel بلا رأس واضح تُنتج مقاطع لا معنى لها بعد الاستخراج.
PDF: ترتيب القراءة والجداول
المكتبات الأولى بالنظر هي PyMuPDF (تعرف كذلك بـfitz) وpdfplumber. الأولى سريعة وموثوقة للنصّ العادي، والثانية أدقّ في التعرّف على الجداول لكنّها أبطأ.
import fitz # PyMuPDF
doc = fitz.open("procedures/reglement-interieur.pdf")
for page in doc:
# "blocks" يعيد كتلًا مرتَّبة قراءةً، أفضل من "text" الخام على عمودين
blocks = page.get_text("blocks")
blocks.sort(key=lambda b: (b[1], b[0])) # ترتيب من أعلى إلى أسفل ثم يسار إلى يمين
for b in blocks:
text = b[4].strip()
if text:
print(text)
للوثائق ذات العمودين، استخراج «الخام» يخلط سطرًا من العمود الأيسر بسطر من العمود الأيمن، فينتج نصّ لا يفهمه أحد. استعمال blocks أو dict ثمّ فرزها يدويًّا يعالج معظم الحالات.
الجداول تحتاج معاملة خاصّة. المُقتطع الآتي يحوّلها إلى صيغة Markdown لتُحفَظ بنيتها داخل المقطع:
import pdfplumber
with pdfplumber.open("procedures/tableau-conges.pdf") as pdf:
for page in pdf.pages:
for table in page.extract_tables():
# كلّ table قائمة أسطر، وكلّ سطر قائمة خلايا
header = "| " + " | ".join(table[0]) + " |"
sep = "| " + " | ".join(["---"] * len(table[0])) + " |"
rows = ["| " + " | ".join(r or "" for r in row) + " |" for row in table[1:]]
print("\n".join([header, sep, *rows]))
يُصان بذلك «هذا العمود جدول، ولا يُقطَع في منتصفه»، وهي الغلطة الأكثر تكرارًا في الوحدة التالية.