أتمتة استخراج الفواتير والبيانات من الصور الممسوحة ضوئياً وتحويلها لـ JSON
ما الفكرة الهندسية وراء تحويل صورة فاتورة ممسوحة إلى بيانات منظمة؟
أتمتة قراءة الفواتير ليست مجرد تشغيل محرك OCR ثم حفظ النص. عملياً نحن نبني خط معالجة متعدد المراحل يبدأ من صورة خام منخفضة الجودة وينتهي بكائن JSON صالح للاستهلاك داخل أنظمة ERP أو المحاسبة أو التخزين التحليلي.
الهيكلية الناجحة غالباً تتكوّن من أربع طبقات:
- طبقة إدخال المستند: رفع الصورة أو ملف
PDFالممسوح. - طبقة تحسين الصورة: إزالة الضوضاء، تدوير الصفحة، قص الحواف، ورفع التباين.
- طبقة الفهم: استخدام نماذج الرؤية
Vision Modelsمع أو بدونOCR. - طبقة التنظيم والتحقق: إجبار النموذج على إعادة البيانات في بنية
JSON Schemaثم التحقق من الحقول والقيم الحسابية.
هذه المقاربة أدق من استخراج النص الخام فقط، لأن الفاتورة مستند شبه منظم: توجد حقول متوقعة مثل رقم الفاتورة، اسم المورد، التاريخ، العملة، البنود، الإجمالي، والضريبة. لذلك يمكننا الجمع بين فهم بصري واستدلال لغوي وتحقق برمجي لاحق لتقليل الأخطاء والهلاوس.
المعمارية العملية لتدفق البيانات داخل النظام
عند تصميم خدمة إنتاجية، من الأفضل تصور التدفق كـ Pipeline واضح بدلاً من سكربت واحد ضخم. التسلسل المقترح كالتالي:
- استقبال الصورة وتوليد معرف معاملة
document_id. - تمرير الصورة إلى مرحلة المعالجة المسبقة عبر
OpenCVلتحسين قابلية القراءة. - تشغيل محرك بصري لاستخراج النصوص والتموضع أو إرسال الصورة مباشرة إلى نموذج متعدد الوسائط.
- بناء برومبت صارم يطلب حقولاً محددة فقط ويمنع التخمين.
- تحويل الناتج إلى مخرجات مهيكلة
JSON. - التحقق البرمجي: مطابقة المجموع الفرعي مع إجمالي البنود، والتأكد من صيغة التاريخ والعملة.
- حفظ النتيجة في قاعدة بيانات أو إرسالها إلى خدمة محاسبية.
هذا الفصل بين الطبقات يسهّل الاختبار والمراقبة. فإذا انخفضت الدقة، ستعرف سريعاً هل المشكلة في جودة الصورة، أم في البرومبت، أم في مرحلة Parsing.
لماذا لا يكفي OCR وحده؟
محركات OCR ممتازة في قراءة الحروف، لكنها لا تفهم دائماً دلالة النص. مثلاً قد تتعرف على عدة أرقام في الصفحة، لكن أيها يمثل invoice_number وأيها رقم ضريبي أو مرجع طلب شراء؟ هنا يأتي دور LLM لفهم السياق.
إذا كنت تريد أساساً مفاهيمياً أعمق لكيفية عمل النماذج لغوياً، راجع مدخل إلى هندسة الذكاء الاصطناعي التوليدي: كيف تعمل نماذج اللغة الكبيرة LLMs برمجياً؟. الفكرة هنا أن النموذج لا يقرأ الكلمات فقط، بل يطابق أنماطاً دلالية مثل “Invoice Date”، “Bill To”، “Tax”، “Total Due”، ثم يعيد بناء تمثيل منظم للمستند.
المعالجة المسبقة للصور قبل إرسالها للنموذج
الصور الممسوحة غالباً تعاني من ميلان، إضاءة غير متوازنة، أو ضوضاء ناتجة عن أجهزة المسح أو تصوير الهاتف. لذلك مرحلة Preprocessing ترفع الدقة النهائية بشكل ملموس.
import cv2
def preprocess_invoice_image(input_path: str, output_path: str) -> str:
image = cv2.imread(input_path)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
denoised = cv2.fastNlMeansDenoising(gray, None, 30, 7, 21)
thresh = cv2.adaptiveThreshold(
denoised, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 31, 11
)
cv2.imwrite(output_path, thresh)
return output_path
هذه الخطوة لا تعطيك بيانات بعد، لكنها تحسن قابلية القراءة للنموذج ومحرك التعرف. وفي البيئات الإنتاجية يمكن إضافة اكتشاف الدوران deskewing وقص الهوامش تلقائياً.
استخدام LLM لاستخراج الحقول بصيغة منظمة
يمكنك تنفيذ ذلك مباشرة مع نموذج متعدد الوسائط أو عبر نص مستخرج مسبقاً. الأهم هو أن البرومبت يحدد الحقول، وسياسة التعامل مع القيم المفقودة، ومنع التخمين. لفهم بناء البرومبتات ديناميكياً داخل الكود، يفيدك هذا الدرس: هندسة الأوامر Prompt Engineering داخل الكود: قوالب النصوص المتغيرة.
استخرج بيانات الفاتورة التالية فقط من الصورة أو النص المتاح. أعد النتيجة بصيغة JSON صالحة 100% دون أي شرح إضافي. إذا كانت قيمة غير موجودة بوضوح فأعدها null ولا تخمّن. الحقول المطلوبة:
invoice_number, invoice_date, vendor_name, vendor_tax_id, currency, subtotal, tax, total, line_items.
كل عنصر داخل line_items يجب أن يحتوي:
description, quantity, unit_price, line_total.
from openai import OpenAI
import json
client = OpenAI()
def extract_invoice_data(raw_text: str) -> dict:
prompt = f"""
Extract invoice fields from the following text.
Return valid JSON only.
If a field is missing, use null.
Text:
{raw_text}
"""
response = client.chat.completions.create(
model="gpt-4.1-mini",
temperature=0,
messages=[
{"role": "system", "content": "You extract structured invoice data."},
{"role": "user", "content": prompt}
]
)
content = response.choices[0].message.content
return json.loads(content)
استخدام temperature=0 مهم هنا لأننا لا نريد إبداعاً، بل استخلاصاً محافظاً. وإذا لم تكن قد أعددت مفاتيح الاتصال بعد، راجع الاتصال الأول: جلب مفاتيح API وكتابة أول سكربت اتصال.
دمج LangChain لبناء سلسلة معالجة قابلة للتوسع
عندما يتحول المشروع من تجربة إلى نظام فعلي، يبرز دور إطار LangChain في تنظيم التدفق بين الإدخال، البرومبت، النموذج، والمحلل. كما أن مفهوم السلاسل Chains مفيد لفصل المسؤوليات.
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import JsonOutputParser
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
parser = JsonOutputParser()
prompt = ChatPromptTemplate.from_template("""
Extract invoice data from the following content.
Return JSON only with these keys:
invoice_number, invoice_date, vendor_name, vendor_tax_id,
currency, subtotal, tax, total, line_items.
Content:
{invoice_text}
""")
chain = prompt | model | parser
result = chain.invoke({"invoice_text": "INVOICE #2048 ... Total 1250.00 USD"})
print(result)
هذه البنية أنظف من كتابة كل شيء يدوياً، وتسهّل استبدال النموذج أو إضافة سجل مراقبة logging أو طبقة تحقق لاحقة.
التحقق من صحة JSON ومنع الأخطاء المحاسبية
أخطر خطأ في هذا النوع من الأنظمة ليس فشل الاستخراج فقط، بل استخراج رقم غير صحيح واعتباره موثوقاً. لذلك بعد استلام JSON يجب فرض اختبارات تحقق:
- هل
totalيساويsubtotal + tax؟ - هل مجموع عناصر
line_itemsمتسق معsubtotal؟ - هل التاريخ بصيغة موحدة مثل
YYYY-MM-DD؟ - هل العملة مستخرجة من قائمة مسموحة مثل
USDأوEURأوSAR؟
def validate_invoice(data: dict) -> dict:
subtotal = float(data.get("subtotal") or 0)
tax = float(data.get("tax") or 0)
total = float(data.get("total") or 0)
calculated = round(subtotal + tax, 2)
data["is_total_consistent"] = abs(calculated - total) < 0.01
return data
ولزيادة الانضباط، يمكنك تطبيق أفكار إجبار الذكاء الاصطناعي على الرد بصيغة JSON فقط وربطها بتحقق مخططات Schema Validation.
متى نحتاج RAG أو Vector DBs في هذا المشروع؟
في النسخة الأساسية لا تحتاج عادةً إلى نظام RAG. لكن عند التعامل مع مورّدين متعددين لكل منهم تنسيق فاتورة مختلف، يصبح تخزين قوالب وأمثلة استخراج سابقة داخل قواعد بيانات متجهة Vector DBs مفيداً. يمكنك حفظ أمثلة ناجحة لكل مورد، ثم استرجاع أقرب أمثلة عبر التضمينات Embeddings لتغذية النموذج بسياق أفضل.
هذه فكرة متقدمة خاصة عندما تكون الفواتير متنوعة جداً أو متعددة اللغات، وتحتاج إلى رفع الدقة دون إعادة تدريب كامل أو Fine-tuning.
أفضل الممارسات الإنتاجية وتقليل التكلفة
- استخدم نموذجاً بصرياً فقط للحالات الصعبة، واعتمد
OCR + LLMللحالات العادية لتقليل السعر. - احسب الاستهلاك مسبقاً خصوصاً عند تمرير نصوص كبيرة، ويمكنك الرجوع إلى حساب التكلفة وإدارة الرموز
Tokens. - احتفظ بالعينة الأصلية، والنص المستخرج، ونسخة
JSONالنهائية لأغراض التدقيق. - فعّل مراجعة بشرية تلقائية إذا فشل التحقق الحسابي أو انخفضت الثقة.
- لا تمرر بيانات حساسة إلى خدمات خارجية دون سياسات خصوصية وتشفير واضحة.
الخلاصة الهندسية
بناء نظام لاستخراج الفواتير من الصور وتحويلها إلى JSON هو مثال ممتاز على دمج الرؤية الحاسوبية مع LLMs والتحقق البرمجي. القيمة الحقيقية لا تأتي من “قراءة النص”، بل من بناء خط معالجة موثوق يبدأ بالتحسين البصري، ثم الاستخراج الدلالي، ثم فرض بنية صارمة، ثم اختبارات اتساق محاسبية. بهذه الطريقة تنتقل من مستند ممسوح صعب القراءة إلى بيانات جاهزة للاندماج داخل الأنظمة المالية والتحليلية بأقل تدخل بشري ممكن.
2 comments