بناء الخطوة الأولى في RAG: استخراج النصوص من ملفات PDF برمجياً
بناء الخطوة الأولى في RAG: استخراج النصوص من ملفات PDF برمجياً
أي نظام RAG ناجح لا يبدأ من النموذج نفسه، بل من جودة البيانات الداخلة إليه. قبل الوصول إلى مرحلة Embeddings أو التخزين داخل Vector DB، يجب أولاً تحويل المستندات الخام إلى نص قابل للمعالجة. وهنا تظهر أهمية ملفات PDF، لأنها الصيغة الأكثر شيوعاً في الأدلة الفنية، العقود، التقارير، الكتالوجات، والوثائق المؤسسية.
المشكلة الهندسية أن ملف PDF ليس دائماً “نصاً” بالمعنى البرمجي. أحياناً يكون نصاً قابلاً للاستخراج مباشرة، وأحياناً يكون صفحات مصورة تحتاج إلى OCR. لذلك فإن مرحلة الاستخراج ليست خطوة ثانوية، بل طبقة Ingestion تؤثر مباشرة في دقة الاسترجاع لاحقاً.
لماذا استخراج النص هو الأساس الحقيقي في معمارية RAG؟
في أي خط تدفق بيانات خاص بالأنظمة التوليدية، تمر الوثيقة غالباً بالمراحل التالية:
- تحميل الملف من التخزين المحلي أو السحابي.
- استخراج النص والبيانات الوصفية
Metadata. - تنظيف المحتوى وإزالة التشويش.
- تقطيع النص إلى أجزاء
Chunks. - تحويل الأجزاء إلى
Embeddings. - حفظها داخل قاعدة متجهية.
- استرجاع الأجزاء الأنسب عند وصول سؤال المستخدم.
إذا كانت نتيجة الاستخراج رديئة، فإن كل المراحل التالية ترث هذا الخلل. مثلاً، لو تم دمج الفقرات بشكل خاطئ أو ضاعت عناوين الصفحات أو اختلط ترتيب الأعمدة، فإن Semantic Search سيصبح أضعف حتى لو استخدمت أفضل نموذج تضمين في السوق.
الخيارات العملية لاستخراج النص من PDF
1) استخدام مكتبة pypdf
هذا الخيار ممتاز كبداية لأنه خفيف ومباشر، ويصلح للملفات النصية القياسية. وإذا لم تكن قد جهزت بيئة العمل بعد، فارجع إلى مقال إعداد بيئة العمل الذكية: تثبيت مكتبات Python الأساسية للتعامل مع الذكاء الاصطناعي.
from pypdf import PdfReader
pdf_path = "documents/rag_guide.pdf"
reader = PdfReader(pdf_path)
all_text = []
for page_number, page in enumerate(reader.pages, start=1):
text = page.extract_text() or ""
all_text.append({
"page": page_number,
"text": text.strip()
})
for item in all_text[:3]:
print(f"Page {item['page']}")
print(item["text"][:500])
print("-" * 80)
هذا السكربت يقرأ الملف صفحة صفحة، ثم يحفظ النص المستخرج مع رقم الصفحة. من الناحية الهندسية، حفظ رقم الصفحة مهم جداً لأنه يتحول لاحقاً إلى Metadata مفيد عند إرجاع المقتطفات للمستخدم أو عند بناء مراجع داخلية موثوقة.
2) استخدام LangChain PDF Loader
عند الانتقال من سكربتات بسيطة إلى خطوط معالجة أكبر، يصبح الاعتماد على مقدمة في إطار عمل LangChain: ثورة ربط وتطوير تطبيقات الذكاء الاصطناعي أكثر عملية، لأنك ستحصل على كائنات Document جاهزة للمعالجة والتقسيم والتخزين.
from langchain_community.document_loaders import PyPDFLoader
pdf_path = "documents/rag_guide.pdf"
loader = PyPDFLoader(pdf_path)
documents = loader.load()
print(f"Loaded documents: {len(documents)}")
print(documents[0].page_content[:700])
print(documents[0].metadata)
الفرق الجوهري هنا أن كل صفحة غالباً تُحمَّل كوثيقة مستقلة تحتوي على:
- النص داخل الخاصية
page_content. - بيانات وصفية مثل المصدر ورقم الصفحة داخل
metadata.
وهذا يجعل الخطوة التالية، أي إنشاء أول سلسلة Chain أو تمرير النص إلى أدوات التقسيم، أكثر نظافة وتنظيماً.
تنظيف النص بعد الاستخراج
الاستخراج الخام من PDF نادراً ما يكون مثالياً. ستجد فراغات زائدة، أسطر مكسورة، تكراراً في الترويسات، أو أرقام صفحات داخل النص. لهذا نحتاج طبقة تنظيف بسيطة قبل إدخال البيانات في بقية خط RAG.
import re
from langchain_community.document_loaders import PyPDFLoader
def clean_text(text: str) -> str:
text = re.sub(r'\n+', '\n', text)
text = re.sub(r'[ \t]+', ' ', text)
text = re.sub(r'\n\s+\n', '\n\n', text)
return text.strip()
loader = PyPDFLoader("documents/rag_guide.pdf")
documents = loader.load()
cleaned_documents = []
for doc in documents:
doc.page_content = clean_text(doc.page_content)
cleaned_documents.append(doc)
print(cleaned_documents[0].page_content[:700])
هذه الخطوة تبدو بسيطة، لكنها عملية جداً. عندما يُنظَّف النص قبل التقطيع، تكون الأجزاء الناتجة أكثر تماسكاً، ويصبح تمثيلها في Embeddings أفضل من حيث المعنى والاسترجاع.
بناء خط معالجة تمهيدي جاهز للربط مع Chunking
من الأفضل أن تفكر في الاستخراج كمرحلة مستقلة داخل معمارية التطبيق. لا تخلط بين تحميل الملف، التنظيف، التقطيع، والتضمين في دالة واحدة. الفصل بين المسؤوليات يسهل الاختبار والصيانة والتوسع.
import re
from langchain_community.document_loaders import PyPDFLoader
def clean_text(text: str) -> str:
text = re.sub(r'\n+', '\n', text)
text = re.sub(r'[ \t]+', ' ', text)
return text.strip()
def extract_pdf_documents(file_path: str):
loader = PyPDFLoader(file_path)
documents = loader.load()
for doc in documents:
doc.page_content = clean_text(doc.page_content)
doc.metadata["source_type"] = "pdf"
return documents
docs = extract_pdf_documents("documents/rag_guide.pdf")
print("Total pages loaded:", len(docs))
print(docs[0].metadata)
print(docs[0].page_content[:500])
بهذه البنية يمكنك لاحقاً تمرير docs إلى مقسم نصوص، ثم إلى قاعدة متجهية مثل ChromaDB أو Pinecone.
ماذا لو كان PDF عبارة عن صور ممسوحة؟
هنا يجب التمييز بين نوعين من الملفات:
- ملفات نصية رقمية: يمكن استخراجها مباشرة عبر
pypdfأوPyPDFLoader. - ملفات ممسوحة ضوئياً: تحتاج إلى
OCRمثلTesseractأو خدمات سحابية متخصصة.
من الناحية المعمارية، من الذكاء أن تضيف مرحلة فحص أولية: إذا كان النص المستخرج فارغاً أو قصيراً جداً بشكل غير منطقي، فحوّل الملف تلقائياً إلى مسار OCR Pipeline. هذا القرار المبكر يوفر عليك تخزين بيانات عديمة الفائدة داخل قاعدة المتجهات.
إذا كان عدد الأحرف المستخرجة من الصفحة أقل من حد معين، فاعتبر الصفحة مرشحاً لكونها صورة، ثم أرسلها إلى مسار OCR قبل المتابعة إلى Chunking و Embeddings.
أفضل الممارسات قبل الانتقال إلى التضمين والاسترجاع
- احتفظ دائماً برقم الصفحة واسم الملف داخل
metadata. - نظّف النص قبل التقطيع، لا بعده.
- اختبر ملفات متعددة البنية: صفحة واحدة، أعمدة، جداول، وترويسات متكررة.
- افصل منطق الاستخراج عن منطق الإرسال إلى النماذج أو قواعد البيانات.
- راقب الحجم وعدد الرموز
Tokensعند الانتقال للمراحل التالية، ويمكنك مراجعة مقال حساب التكلفة وإدارة الرموز.
الخلاصة الهندسية
استخراج النص من ملفات PDF ليس مجرد خطوة إدخال بيانات، بل هو نقطة تأسيس لأي تطبيق RAG احترافي. عندما تبني طبقة Ingestion بشكل صحيح، فإنك تضمن أن المراحل اللاحقة مثل البحث الدلالي، التضمين، والاسترجاع، ستعمل على مادة نصية متماسكة وموثوقة. وهذه هي القاعدة التي تُبنى فوقها الأنظمة القادرة فعلاً على الإجابة من مستندات الشركات لا من الذاكرة العامة للنموذج.
في الدرس التالي منطقياً، ستنتقل إلى تقسيم النصوص المستخرجة إلى أجزاء ذكية قابلة للتحويل إلى Embeddings، لأن النص الخام الطويل لا يدخل إلى قاعدة المتجهات بكفاءة إلا بعد تصميم استراتيجية Chunking مناسبة.
5 comments