انتقل إلى المحتوى الرئيسي

الوحدة 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]))

يُصان بذلك «هذا العمود جدول، ولا يُقطَع في منتصفه»، وهي الغلطة الأكثر تكرارًا في الوحدة التالية.

PDF ممسوح: التعرّف الضوئي على الحروف

بعض المذكّرات القديمة موجودة فقط كصور. الاختبار السريع: إن كان len(page.get_text().strip()) صفرًا في PDF ذي صفحات كثيرة، فأنت أمام مسح ضوئي. الأداة المرجعية هي Tesseract عبر غلاف pytesseract.

import pytesseract
from pdf2image import convert_from_path

pages = convert_from_path("procedures/note-1998.pdf", dpi=300)
texts = []
for i, page_img in enumerate(pages):
# ara للعربية، fra للفرنسية، eng للإنجليزية
text = pytesseract.image_to_string(page_img, lang="ara+fra")
texts.append(text)

full_text = "\n\n".join(texts)

نقطتان تُفسدان الاستخراج الضوئي عادةً: دقّة صور منخفضة (أقلّ من 200 نقطة في البوصة تعطي أخطاء كثيرة، ودرجة 300 توصية معقولة)، واللغة غير المعلَنة، حيث يفترض الافتراضي الإنجليزية فيهلوس أمام العربية.

HTML: تنظيف ما ليس محتوى

نصّ HTML خام يتضمّن عادة قوائم تنقّل وروابط اجتماعية ونصّ تذييل. readability-lxml وtrafilatura مكتبتان تُستخرجان «المحتوى الرئيسي» بحُسن.

import trafilatura

html = open("portail/reglement-interieur.html").read()
text = trafilatura.extract(
html,
include_tables=True,
include_links=False,
include_comments=False,
)
print(text)

بديل يدوي بـBeautifulSoup مقبول للمشاريع الصغيرة:

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, "html.parser")
for tag in soup(["nav", "footer", "script", "style", "aside"]):
tag.decompose()
main = soup.select_one("main") or soup.body
text = main.get_text("\n", strip=True)

المستندات المكتبية والجدولية

Unstructured مكتبة موحّدة تعالج التنسيقات كلّها بواجهة واحدة، وهي مسار العمل الشائع في مشاريع RAG المؤسّسية.

from unstructured.partition.auto import partition

# يكتشف التنسيق تلقائيًا: pdf, docx, pptx, xlsx, html, eml…
elements = partition(filename="procedures/politique-conges.docx")
for el in elements:
print(f"[{el.category}] {el.text[:80]}")

خرج partition كائنات مصنَّفة: Title, NarrativeText, ListItem, Table… وهذا التصنيف ثمين للتقطيع البنيوي (الوحدة القادمة).

ما وراء البيانات: ما يُحفَظ بجوار النصّ

استخراج النصّ دون ما وراء البيانات يُفقد نصف قيمته. لكلّ مقطع نستخرجه يجب أن نحفظ على الأقلّ:

الحقلالمثالفائدته
sourceprocedures/reglement-interieur.pdfالاستشهاد بالمصدر في الإجابة
page12إحالة دقيقة يستطيع المستخدم فتحها
sectionالمادّة 14 — إشعار الاستقالةتصفية أو ترجيح لاحق
updated_at2026-05-14تفضيل النسخة الأحدث عند تعارض
access_levelRHفرض الأذونات في وقت الاسترجاع

هذه الحقول هي التي تسمح لاحقًا بفلاتر مثل «لا ترجع لغير مسؤول الموارد البشرية مقاطع مصنَّفة RH»، وستعود إليها الوحدة 4.

خصوصية العربية: اتّجاه القراءة والتشكيل

النصّ العربي يُقرأ من اليمين إلى اليسار، لكنّه يُخزَّن منطقيًّا في الذاكرة من اليسار إلى اليمين. أدوات الاستخراج الجيّدة تحترم هذا التمييز، لكنّ بعض ملفّات PDF القديمة تُخزّن الحروف حسب الترتيب المرئي، فيَخرج النصّ مقلوبًا. أداة python-bidi تعالج هذه الحالة:

from bidi.algorithm import get_display

# إن كان الاستخراج يعطي "لأا يبرعلا صنلا" بدل "النصّ العربيّ الأوّل"
corrected = get_display(text)

نقطة ثانية: التشكيل (الفتحة، الضمّة، الكسرة، الشدّة). حضوره أو غيابه يغيّر المطابقة النصّية، وقد يُشوّش نماذج التضمين المُدرَّبة على نصوص غير مشكَّلة. التطبيع (إزالة التشكيل، توحيد أشكال الألف والياء والهاء) قبل الفهرسة قرار منهجي تُتّخذ بوعي، وتُطبَّق على السؤال أيضًا وقت الاستفسار حتّى تبقى المقارنة نزيهة.

اختلاف طفيف يكسر كلّ شيء

إن طبّعت النصّ في الفهرسة ولم تطبّعه في السؤال، فسؤال المستخدم بتشكيل لن يطابق مقاطعك بلا تشكيل. القاعدة: كلّ تحويل يُطبَّق في الفهرسة يُعاد تطبيقه بحذافيره وقت الاستفسار، ويُغلَّف في دالّة واحدة تُستدعى في الجانبين.

قياس النجاح بلا تعقيد

لا تنتظر التقييم الكامل. بعد استخراج ملفّ، افتحه، اختر خمس فقرات عشوائيًا، وقارن النصّ المستخرَج بها. إن ظهر عمودان مختلطان، أو جدول تحوَّل إلى «كلمات مبعثرة»، عالج قبل المضيّ. إفلات هذا الخطأ إلى الفهرس يعني تكرار كلّ الحلقة.

في الخلاصة

  • كلّ تنسيق يفرض معالجة خاصّة: PyMuPDF لترتيب القراءة، pdfplumber للجداول، Tesseract للممسوح، trafilatura لتنظيف HTML، Unstructured كواجهة موحّدة.
  • الجداول تُحفَظ بنيةً (Markdown مثلًا) لأنّ الوحدة التالية ستقطّع دون أن تفهم أنّ هذه الأسطر جدول.
  • ما وراء البيانات (source, page, section, access_level, updated_at) لا يقلّ أهمّية عن النصّ نفسه، وسيمكِّن الاستشهاد والفلاتر والأذونات لاحقًا.
  • العربية تستلزم اهتمامًا خاصًّا بـاتّجاه القراءة والتطبيع (تشكيل، أشكال الألف)، مع تطبيق التحويل ذاته في الفهرسة والاستفسار.

الوحدة التالية: تقطيع النصّ المُستخرَج إلى مقاطع بحجم مناسب، حيث الاختيار بين قطع صارم مسطَّح وتقطيع يحترم البنية يُغيّر جودة الاسترجاع تغييرًا ملموسًا.