استخراج بيانات الفواتير من الصور وPDF باستخدام بايثون وOCR | مشروع عملي

استخراج بيانات الفواتير من الصور وPDF باستخدام بايثون وOCR

في هذا المشروع سنبني برنامجًا كاملًا باستخدام بايثون وOCR لقراءة صور الفواتير وملفات PDF، ثم استخراج رقم الفاتورة والتاريخ واسم المورد والإجمالي والضريبة والعملة، وأخيرًا حفظ النتائج في Excel وCSV.

سنبدأ من إنشاء البيئة الافتراضية وتثبيت المكتبات، ثم نثبت Tesseract OCR، ونوضح متى نحتاج Poppler، وبعد ذلك نبني ملفات المشروع واحدًا واحدًا حتى يصبح قابلًا للتشغيل.

{alertWarning} استخدم فواتير تجريبية أثناء التعلم. نتائج OCR وRegex آلية وقد تخطئ، لذلك يجب مراجعة المبالغ والتواريخ يدويًا قبل استخدامها في أي عمل مالي.

تحميل مشروع استخراج بيانات الفواتير

حمّل ملفات المشروع كاملة وجربها مباشرة، ثم تابع خطوات الشرح لبنائها وفهم كل جزء فيها.

تحميل ملفات المشروع

{getToc} $title={محتوى المقال}

ماذا يفعل المشروع؟

  • قراءة صور JPG وPNG وJPEG وBMP وTIFF.
  • قراءة ملفات PDF النصية.
  • تحويل PDF المصور إلى صور وتشغيل OCR عليه.
  • قراءة العربية والإنجليزية عند توفر بيانات اللغة المناسبة في Tesseract.
  • استخراج رقم الفاتورة والتاريخ واسم المورد والإجمالي والضريبة والعملة.
  • تصدير النتائج إلى Excel وCSV.
  • نسخ الملفات التي تحتاج مراجعة إلى مجلد مستقل.
  • حفظ سجل التشغيل داخل ملف Log.

1. إنشاء مجلد المشروع والبيئة الافتراضية

mkdir invoice_extractor
cd invoice_extractor
python -m venv venv

في Windows:

venv\Scripts\activate

في Linux وmacOS:

source venv/bin/activate

2. تثبيت مكتبات Python

لنسخة الصور وPDF النصي والتصدير:

python -m pip install pytesseract pillow pdfplumber pandas openpyxl

ولإضافة PDF المصور والاختبارات وواجهة Streamlit:

python -m pip install pdf2image pytest streamlit

وقد جهزنا ملفين للتثبيت دفعة واحدة:

python -m pip install -r requirements.txt
python -m pip install -r requirements-advanced.txt

3. تثبيت Tesseract OCR

مكتبة pytesseract ليست محرك OCR مستقلًا؛ بل تستدعي برنامج Tesseract المثبت على الجهاز. بعد تثبيت Tesseract اختبره:

tesseract --version

إذا أردت قراءة الفواتير العربية، ثبت بيانات اللغة العربية واستخدم:

OCR_LANGUAGE=ara+eng

في Windows، إذا لم يكن Tesseract في PATH، عيّن متغير البيئة TESSERACT_PATH إلى المسار الكامل، مثل:

C:\Program Files\Tesseract-OCR\tesseract.exe

4. متى نحتاج Poppler؟

لا تحتاج Poppler عند قراءة صور JPG وPNG، ولا عند قراءة PDF يحتوي نصًا حقيقيًا يمكن لـpdfplumber استخراجه. نحتاج Poppler عند تحويل PDF المصور إلى صور بواسطة pdf2image.

في Windows، إذا لم يكن Poppler متاحًا من PATH، عيّن:

POPPLER_PATH=C:\path\to\poppler\Library\bin
مخطط مشروع استخراج بيانات الفواتير باستخدام OCR في بايثون

5. هيكل المشروع النهائي

invoice_extractor/
├── main.py
├── config.py
├── ocr_reader.py
├── pdf_reader.py
├── image_preprocessor.py
├── field_extractor.py
├── exporter.py
├── logger_config.py
├── make_demo_invoice.py
├── app.py
├── requirements.txt
├── requirements-advanced.txt
├── tests/
│   └── test_field_extractor.py
├── input_files/
├── output/
├── failed/
└── logs/

ضع الفواتير داخل input_files. أما مجلدات النتائج والسجل والملفات التي تحتاج مراجعة، فينشئها البرنامج تلقائيًا.

6. ملف config.py

import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent

INPUT_DIR = BASE_DIR / "input_files"
OUTPUT_DIR = BASE_DIR / "output"
FAILED_DIR = BASE_DIR / "failed"
LOG_DIR = BASE_DIR / "logs"

EXCEL_OUTPUT = OUTPUT_DIR / "invoice_results.xlsx"
CSV_OUTPUT = OUTPUT_DIR / "invoice_results.csv"

SUPPORTED_IMAGE_EXTENSIONS = {
    ".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff"
}
SUPPORTED_EXTENSIONS = SUPPORTED_IMAGE_EXTENSIONS | {".pdf"}

OCR_LANGUAGE = os.getenv("OCR_LANGUAGE", "ara+eng")
OCR_PSM = os.getenv("OCR_PSM", "6")
TESSERACT_PATH = os.getenv("TESSERACT_PATH", "tesseract")
POPPLER_PATH = os.getenv("POPPLER_PATH") or None


def create_directories():
    INPUT_DIR.mkdir(parents=True, exist_ok=True)
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    FAILED_DIR.mkdir(parents=True, exist_ok=True)
    LOG_DIR.mkdir(parents=True, exist_ok=True)

هذا الملف يجمع المسارات والإعدادات في مكان واحد. استخدمنا متغيرات البيئة حتى لا نربط المشروع بمسارات ثابتة لجهاز واحد.

7. ملف logger_config.py

import logging
from config import LOG_DIR


def setup_logging():
    log_file = LOG_DIR / "invoice_extractor.log"

    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s | %(levelname)s | %(message)s",
        handlers=[
            logging.FileHandler(log_file, encoding="utf-8"),
            logging.StreamHandler(),
        ],
        force=True,
    )

    return logging.getLogger("invoice_extractor")

سيحفظ سجلًا داخل logs/invoice_extractor.log لتعرف أي ملف نجح وأي ملف فشل ولماذا.

8. ملف image_preprocessor.py

from pathlib import Path
from PIL import Image, ImageEnhance, ImageFilter, ImageOps


def open_image(image_path):
    image_path = Path(image_path)

    if not image_path.exists():
        raise FileNotFoundError(f"الملف غير موجود: {image_path}")

    try:
        image = Image.open(image_path)
        image.load()
        return image
    except Exception as error:
        raise ValueError(f"تعذر فتح الصورة: {error}") from error


def preprocess_image(image):
    image = ImageOps.exif_transpose(image)

    if image.mode not in ("L", "RGB"):
        image = image.convert("RGB")

    gray_image = ImageOps.grayscale(image)
    gray_image = ImageEnhance.Contrast(gray_image).enhance(2.0)
    gray_image = ImageEnhance.Sharpness(gray_image).enhance(2.0)
    gray_image = gray_image.filter(ImageFilter.SHARPEN)

    return gray_image

قبل OCR نصحح اتجاه الصورة، ثم نحولها إلى Grayscale، ونرفع التباين والحدة. هذه الخطوة لا تضمن الدقة لكنها تساعد كثيرًا عندما تكون الصورة ضعيفة.

تحسين صورة الفاتورة قبل OCR باستخدام Pillow

9. ملف ocr_reader.py

from pathlib import Path
import shutil
import pytesseract

from config import OCR_LANGUAGE, OCR_PSM, TESSERACT_PATH
from image_preprocessor import open_image, preprocess_image


def configure_tesseract():
    configured_path = str(TESSERACT_PATH).strip() or "tesseract"
    path_object = Path(configured_path)

    if path_object.is_file():
        pytesseract.pytesseract.tesseract_cmd = str(path_object)
        return

    command_path = shutil.which(configured_path)
    pytesseract.pytesseract.tesseract_cmd = command_path or configured_path


def extract_text_from_pil_image(image, language=OCR_LANGUAGE):
    configure_tesseract()
    processed_image = preprocess_image(image)

    try:
        text = pytesseract.image_to_string(
            processed_image,
            lang=language,
            config=f"--psm {OCR_PSM}",
        )
    except pytesseract.TesseractNotFoundError as error:
        raise RuntimeError(
            "لم يتم العثور على Tesseract. ثبّته وأضفه إلى PATH "
            "أو عيّن TESSERACT_PATH إلى ملف tesseract التنفيذي."
        ) from error
    except Exception as error:
        raise RuntimeError(
            f"فشل استخراج النص من الصورة: {error}"
        ) from error

    return text.strip()


def extract_text_from_image(image_path, language=OCR_LANGUAGE):
    image = open_image(image_path)

    try:
        return extract_text_from_pil_image(image, language=language)
    finally:
        image.close()

هذا الملف يضبط مسار Tesseract ويشغّل OCR على الصورة بعد تحسينها. الإعداد الافتراضي هو --psm 6 ويمكن تغييره بواسطة OCR_PSM.

10. ملف pdf_reader.py

from pathlib import Path
import pdfplumber

from config import OCR_LANGUAGE, POPPLER_PATH
from ocr_reader import extract_text_from_pil_image


def extract_text_from_pdf(pdf_path):
    pdf_path = Path(pdf_path)

    if not pdf_path.exists():
        raise FileNotFoundError(f"ملف PDF غير موجود: {pdf_path}")

    pages_text = []

    try:
        with pdfplumber.open(pdf_path) as pdf:
            for page in pdf.pages:
                pages_text.append(page.extract_text() or "")
    except Exception as error:
        raise RuntimeError(f"فشل قراءة ملف PDF: {error}") from error

    return "\n".join(pages_text).strip()


def extract_text_from_scanned_pdf(pdf_path, dpi=300):
    try:
        from pdf2image import convert_from_path
    except ImportError as error:
        raise RuntimeError(
            "ثبّت pdf2image باستخدام: python -m pip install pdf2image"
        ) from error

    options = {"dpi": dpi}

    if POPPLER_PATH:
        options["poppler_path"] = POPPLER_PATH

    try:
        pages = convert_from_path(pdf_path, **options)
    except Exception as error:
        raise RuntimeError(
            "تعذر تحويل PDF إلى صور. إذا كنت تستخدم Windows فتأكد "
            "من تثبيت Poppler وضبط POPPLER_PATH عند الحاجة."
        ) from error

    pages_text = []

    for page_image in pages:
        try:
            pages_text.append(
                extract_text_from_pil_image(
                    page_image,
                    language=OCR_LANGUAGE,
                )
            )
        finally:
            page_image.close()

    return "\n".join(pages_text).strip()


def extract_text_from_any_pdf(pdf_path):
    text = extract_text_from_pdf(pdf_path)

    if text.strip():
        return text

    return extract_text_from_scanned_pdf(pdf_path)

نجرب أولًا pdfplumber. إذا لم نستخرج أي نص، ينتقل البرنامج إلى مسار PDF المصور: يحول الصفحات إلى صور ثم يمرر كل صفحة إلى OCR.

11. ملف field_extractor.py

import re
from datetime import datetime

ARABIC_DIGITS = str.maketrans(
    "٠١٢٣٤٥٦٧٨٩",
    "0123456789",
)


def normalize_text(text):
    if not text:
        return ""

    text = text.translate(ARABIC_DIGITS)
    text = text.replace("\u200f", " ")
    text = text.replace("\u200e", " ")
    text = text.replace("\xa0", " ")
    return text


def clean_value(value):
    if value is None:
        return None

    value = " ".join(value.split())
    value = value.strip(" :-|")
    return value or None


def extract_invoice_number(text):
    text = normalize_text(text)

    patterns = [
        (
            r"(?:invoice\s*(?:no|number|#)|invoice\s*id)"
            r"\s*[:#\-]?\s*([A-Z0-9][A-Z0-9\/\-_]*)"
        ),
        (
            r"(?:رقم\s*الفاتورة|فاتورة\s*رقم)"
            r"\s*[:#\-]?\s*([A-Z0-9][A-Z0-9\/\-_]*)"
        ),
    ]

    for pattern in patterns:
        match = re.search(pattern, text, re.IGNORECASE)
        if match:
            return clean_value(match.group(1))

    return None


def normalize_date(value):
    if not value:
        return None

    value = value.strip().replace(".", "/").replace("-", "/")

    formats = [
        "%d/%m/%Y",
        "%Y/%m/%d",
        "%m/%d/%Y",
        "%d/%m/%y",
    ]

    for date_format in formats:
        try:
            parsed_date = datetime.strptime(value, date_format)
            return parsed_date.strftime("%Y-%m-%d")
        except ValueError:
            continue

    return value


def extract_invoice_date(text):
    text = normalize_text(text)

    pattern = (
        r"(?:invoice\s*date|date|التاريخ|تاريخ\s*الفاتورة)"
        r"\s*[:\-]?\s*"
        r"(\d{1,4}[\/\-.]\d{1,2}[\/\-.]\d{1,4})"
    )

    match = re.search(pattern, text, re.IGNORECASE)
    if match:
        return normalize_date(match.group(1))

    general = r"\b(\d{1,4}[\/\-.]\d{1,2}[\/\-.]\d{1,4})\b"
    match = re.search(general, text)

    if match:
        return normalize_date(match.group(1))

    return None


def clean_number(value):
    if not value:
        return None

    value = normalize_text(value)
    value = value.replace(",", "")
    value = value.replace("،", "")
    value = value.replace(" ", "")

    try:
        return float(value)
    except ValueError:
        return None


def extract_amount_by_labels(text, labels):
    text = normalize_text(text)
    label_pattern = "|".join(re.escape(label) for label in labels)

    pattern = (
        rf"(?:{label_pattern})"
        r"\s*[:\-]?\s*"
        r"(?:[A-Z]{3}|[$€£ر.سد.إ]*)?"
        r"\s*"
        r"([0-9]+(?:[,.][0-9]+)*)"
    )

    matches = re.findall(pattern, text, re.IGNORECASE)

    if not matches:
        return None

    return clean_number(matches[-1])


def extract_total_amount(text):
    return extract_amount_by_labels(
        text,
        [
            "grand total",
            "total amount",
            "total",
            "الإجمالي",
            "المجموع",
            "المبلغ الإجمالي",
        ],
    )


def extract_tax_amount(text):
    return extract_amount_by_labels(
        text,
        [
            "tax",
            "vat",
            "sales tax",
            "الضريبة",
            "ضريبة القيمة المضافة",
        ],
    )


def extract_currency(text):
    text = normalize_text(text)
    currencies = [
        ("SAR", ["SAR", "ر.س", "ريال سعودي"]),
        ("USD", ["USD", "$", "دولار"]),
        ("EUR", ["EUR", "€", "يورو"]),
        ("GBP", ["GBP", "£", "جنيه إسترليني"]),
        ("AED", ["AED", "د.إ", "درهم"]),
        ("YER", ["YER", "ريال يمني"]),
    ]

    lowered = text.lower()

    for code, symbols in currencies:
        for symbol in symbols:
            if symbol.lower() in lowered:
                return code

    return None


def extract_vendor_name(text):
    text = normalize_text(text)

    patterns = [
        r"(?:vendor|supplier|seller|المورد|البائع)\s*[:\-]\s*(.+)",
        r"(?:company|company\s*name|اسم\s*الشركة)\s*[:\-]\s*(.+)",
    ]

    for pattern in patterns:
        match = re.search(pattern, text, re.IGNORECASE)
        if match:
            return clean_value(match.group(1).splitlines()[0])

    lines = [
        clean_value(line)
        for line in text.splitlines()
        if clean_value(line)
    ]

    return lines[0] if lines else None


def extract_invoice_fields(text):
    return {
        "invoice_number": extract_invoice_number(text),
        "invoice_date": extract_invoice_date(text),
        "vendor_name": extract_vendor_name(text),
        "total_amount": extract_total_amount(text),
        "tax_amount": extract_tax_amount(text),
        "currency": extract_currency(text),
    }

هنا نحول النص الخام إلى حقول منظمة باستخدام Regex. كما نحول الأرقام العربية إلى إنجليزية ونوحد صيغة التاريخ قدر الإمكان.

{alertInfo} أنماط Regex هنا عامة وتعليمية. لن تطابق جميع تصاميم الفواتير بنفس الدقة. إذا كنت تتعامل مع مورد أو قالب ثابت، عدّل الأنماط وفق ذلك القالب.

12. ملف exporter.py

from pathlib import Path
import pandas as pd


COLUMNS = [
    "file_name",
    "file_type",
    "invoice_number",
    "invoice_date",
    "vendor_name",
    "total_amount",
    "tax_amount",
    "currency",
    "status",
    "error",
]


def export_records(records, excel_path, csv_path):
    dataframe = pd.DataFrame(records)

    for column in COLUMNS:
        if column not in dataframe.columns:
            dataframe[column] = None

    dataframe = dataframe[COLUMNS]

    excel_path = Path(excel_path)
    csv_path = Path(csv_path)

    excel_path.parent.mkdir(parents=True, exist_ok=True)
    csv_path.parent.mkdir(parents=True, exist_ok=True)

    dataframe.to_excel(excel_path, index=False)
    dataframe.to_csv(csv_path, index=False, encoding="utf-8-sig")

    return dataframe

هذا الملف ينظم النتائج داخل DataFrame ثم يحفظها في:

output/invoice_results.xlsx
output/invoice_results.csv

13. ملف main.py

import logging
import shutil

from config import (
    CSV_OUTPUT,
    EXCEL_OUTPUT,
    FAILED_DIR,
    INPUT_DIR,
    SUPPORTED_EXTENSIONS,
    create_directories,
)
from exporter import export_records
from field_extractor import extract_invoice_fields
from logger_config import setup_logging
from ocr_reader import extract_text_from_image
from pdf_reader import extract_text_from_any_pdf


logger = logging.getLogger("invoice_extractor")


def get_input_files():
    return sorted(
        file_path
        for file_path in INPUT_DIR.iterdir()
        if file_path.is_file()
        and file_path.suffix.lower() in SUPPORTED_EXTENSIONS
    )


def extract_text_from_file(file_path):
    if file_path.suffix.lower() == ".pdf":
        return extract_text_from_any_pdf(file_path)

    return extract_text_from_image(file_path)


def is_record_incomplete(record):
    important_fields = [
        "invoice_number",
        "invoice_date",
        "total_amount",
    ]

    return [
        field
        for field in important_fields
        if record.get(field) is None
    ]


def get_unique_failed_path(file_path):
    destination = FAILED_DIR / file_path.name

    if not destination.exists():
        return destination

    counter = 1

    while True:
        destination = FAILED_DIR / (
            f"{file_path.stem}_failed_{counter}{file_path.suffix}"
        )
        if not destination.exists():
            return destination
        counter += 1


def move_to_failed(file_path):
    shutil.copy2(file_path, get_unique_failed_path(file_path))


def process_file(file_path):
    logger.info("بدء معالجة الملف: %s", file_path.name)

    record = {
        "file_name": file_path.name,
        "file_type": file_path.suffix.lower(),
        "invoice_number": None,
        "invoice_date": None,
        "vendor_name": None,
        "total_amount": None,
        "tax_amount": None,
        "currency": None,
        "status": "success",
        "error": "",
    }

    try:
        text = extract_text_from_file(file_path)

        if not text.strip():
            raise ValueError("لم يتم استخراج أي نص من الملف.")

        record.update(extract_invoice_fields(text))
        missing_fields = is_record_incomplete(record)

        if missing_fields:
            record["status"] = "review_required"
            record["error"] = "حقول ناقصة: " + ", ".join(missing_fields)
            move_to_failed(file_path)
            logger.warning(
                "الملف يحتاج مراجعة: %s | %s",
                file_path.name,
                record["error"],
            )
        else:
            logger.info("تمت معالجة الملف بنجاح: %s", file_path.name)

    except Exception as error:
        record["status"] = "failed"
        record["error"] = str(error)

        try:
            move_to_failed(file_path)
        except Exception as move_error:
            logger.error(
                "تعذر نسخ الملف الفاشل %s: %s",
                file_path.name,
                move_error,
            )

        logger.exception("فشل الملف: %s", file_path.name)

    return record


def main():
    create_directories()
    setup_logging()

    global logger
    logger = logging.getLogger("invoice_extractor")

    files = get_input_files()

    if not files:
        logger.warning(
            "لم يتم العثور على ملفات داخل المجلد: %s",
            INPUT_DIR,
        )
        return

    records = [process_file(file_path) for file_path in files]

    dataframe = export_records(
        records,
        excel_path=EXCEL_OUTPUT,
        csv_path=CSV_OUTPUT,
    )

    success_count = int((dataframe["status"] == "success").sum())
    review_count = int(
        (dataframe["status"] == "review_required").sum()
    )
    failed_count = int((dataframe["status"] == "failed").sum())

    logger.info("انتهت المعالجة.")
    logger.info("عدد الملفات: %s", len(files))
    logger.info("ناجح: %s", success_count)
    logger.info("يحتاج مراجعة: %s", review_count)
    logger.info("فشل: %s", failed_count)
    logger.info("ملف Excel: %s", EXCEL_OUTPUT)
    logger.info("ملف CSV: %s", CSV_OUTPUT)


if __name__ == "__main__":
    main()

هذا هو الملف الرئيسي. يقرأ الملفات من input_files، ويختار قارئ الصور أو PDF، ثم يستخرج الحقول ويحدد حالة كل فاتورة ويصدر النتائج.

استخراج رقم الفاتورة والتاريخ والمبلغ وتصديرها إلى Excel

14. إنشاء فاتورة تجريبية بدل استخدام بيانات حقيقية

أضفت ملفًا يساعدك على إنشاء صورة فاتورة تجريبية إنجليزية بدون استخدام بيانات شخصية:

from PIL import Image, ImageDraw, ImageFont
from config import INPUT_DIR, create_directories


DEMO_TEXT = [
    "INVOICE",
    "Invoice Number: INV-1001",
    "Invoice Date: 18/10/2026",
    "Vendor: Python Store",
    "Total: 125.50 USD",
    "Tax: 18.83 USD",
]


def get_font(size=34):
    for font_name in ("DejaVuSans.ttf", "Arial.ttf"):
        try:
            return ImageFont.truetype(font_name, size=size)
        except OSError:
            continue

    return ImageFont.load_default()


def create_demo_invoice():
    create_directories()

    image = Image.new("RGB", (1200, 800), "white")
    draw = ImageDraw.Draw(image)
    font = get_font()
    y = 90

    for line in DEMO_TEXT:
        draw.text((100, y), line, fill="black", font=font)
        y += 90

    output_path = INPUT_DIR / "invoice_demo.png"
    image.save(output_path)
    print(f"تم إنشاء فاتورة تجريبية: {output_path}")


if __name__ == "__main__":
    create_demo_invoice()

شغله:

python make_demo_invoice.py

وسيُنشئ:

input_files/invoice_demo.png

15. تشغيل المشروع

بعد وضع صورة أو PDF داخل input_files شغّل:

python main.py

بعد انتهاء التشغيل ستجد:

output/invoice_results.xlsx
output/invoice_results.csv
logs/invoice_extractor.log

أما الملفات التي فشلت أو لم تكتمل حقولها الأساسية فستنسخ إلى:

failed/

16. ما الحقول التي يعتبرها البرنامج أساسية؟

في هذه النسخة نعتبر الحقول التالية أساسية:

  • invoice_number
  • invoice_date
  • total_amount

إذا فقد أحدها تصبح حالة السجل review_required. وإذا حدث خطأ تقني أثناء القراءة تصبح الحالة failed.

17. اختبار Regex باستخدام pytest

from field_extractor import (
    extract_currency,
    extract_invoice_date,
    extract_invoice_number,
    extract_tax_amount,
    extract_total_amount,
    extract_vendor_name,
)


SAMPLE_TEXT = """
Invoice Number: INV-2026-100
Invoice Date: 18/10/2026
Vendor: Python Store
Grand Total: 1,250.75 USD
Tax: 187.61 USD
"""


def test_extract_invoice_number():
    assert extract_invoice_number(SAMPLE_TEXT) == "INV-2026-100"


def test_extract_invoice_date():
    assert extract_invoice_date(SAMPLE_TEXT) == "2026-10-18"


def test_extract_total_amount():
    assert extract_total_amount(SAMPLE_TEXT) == 1250.75


def test_extract_tax_amount():
    assert extract_tax_amount(SAMPLE_TEXT) == 187.61


def test_extract_currency():
    assert extract_currency(SAMPLE_TEXT) == "USD"


def test_extract_vendor_name():
    assert extract_vendor_name(SAMPLE_TEXT) == "Python Store"

شغّل الاختبارات:

python -m pytest -q

18. تحسين OCR للفواتير العربية

ابدأ بالإعداد:

OCR_LANGUAGE=ara+eng
OCR_PSM=6

إذا كانت الفاتورة متعددة الأعمدة أو النص موزعًا، جرّب قيمًا مثل 4 أو 11. لا يوجد إعداد واحد مثالي لجميع الفواتير.

19. واجهة Streamlit الاختيارية

إذا أردت رفع الفاتورة من المتصفح بدل وضعها داخل المجلد، استخدم ملف app.py:

import tempfile
from pathlib import Path

import pandas as pd
import streamlit as st

from field_extractor import extract_invoice_fields
from ocr_reader import extract_text_from_image
from pdf_reader import extract_text_from_any_pdf


st.set_page_config(
    page_title="استخراج بيانات الفواتير",
    page_icon="🧾",
    layout="centered",
)

st.title("استخراج بيانات الفواتير باستخدام بايثون")
st.write("ارفع صورة فاتورة أو ملف PDF لاستخراج البيانات.")

uploaded_file = st.file_uploader(
    "اختر الفاتورة",
    type=["png", "jpg", "jpeg", "bmp", "tif", "tiff", "pdf"],
)

if uploaded_file:
    suffix = Path(uploaded_file.name).suffix.lower()

    with tempfile.NamedTemporaryFile(
        delete=False,
        suffix=suffix,
    ) as temporary_file:
        temporary_file.write(uploaded_file.getbuffer())
        temporary_path = Path(temporary_file.name)

    try:
        with st.spinner("جارٍ استخراج البيانات..."):
            if suffix == ".pdf":
                text = extract_text_from_any_pdf(temporary_path)
            else:
                text = extract_text_from_image(temporary_path)

            fields = extract_invoice_fields(text)

        st.success("تمت معالجة الملف.")

        dataframe = pd.DataFrame(
            [
                {"الحقل": key, "القيمة": value}
                for key, value in fields.items()
            ]
        )

        st.dataframe(
            dataframe,
            use_container_width=True,
            hide_index=True,
        )

        with st.expander("عرض النص المستخرج"):
            st.text_area("النص", text, height=300)

        csv_data = dataframe.to_csv(
            index=False,
            encoding="utf-8-sig",
        )

        st.download_button(
            label="تحميل النتائج CSV",
            data=csv_data,
            file_name="invoice_result.csv",
            mime="text/csv",
        )

    except Exception as error:
        st.error(f"حدث خطأ: {error}")

    finally:
        if temporary_path.exists():
            temporary_path.unlink()

شغّل الواجهة:

streamlit run app.py

20. مسار المعالجة كاملًا

صورة أو PDF
      ↓
استخراج النص
      ↓
OCR إذا كان الملف مصورًا
      ↓
نص خام
      ↓
Regex
      ↓
رقم + تاريخ + مورد + مبلغ + ضريبة + عملة
      ↓
فحص الحقول الأساسية
      ↓
Excel + CSV

21. حدود المشروع التي يجب أن تعرفها

  • دقة OCR تعتمد على جودة الصورة والإضاءة والميل وحجم الخط.
  • Regex لن يفهم كل تصميم فاتورة دون تخصيص.
  • الفواتير العربية المختلطة بالإنجليزية قد تحتاج إعدادات إضافية.
  • PDF المصور قد يحتاج Poppler في Windows.
  • المبالغ والتواريخ المستخرجة يجب مراجعتها بشريًا.
  • لا تستخدم فواتير حساسة مع خدمات خارجية دون مراجعة سياسة الخصوصية.

22. تطويرات لاحقة

  • حفظ النص الخام لكل فاتورة.
  • استخراج بنود الفاتورة والأسعار والكميات.
  • قواعد Regex مختلفة حسب المورد.
  • حفظ النتائج في SQLite.
  • معاينة وتصحيح الحقول قبل التصدير.
  • إضافة درجة ثقة ومراجعة بشرية منظمة.

أسئلة شائعة

هل pytesseract تكفي وحدها؟

لا. يجب تثبيت محرك Tesseract OCR على الجهاز، ثم تستدعيه pytesseract من بايثون.

هل أحتاج Poppler دائمًا؟

لا. نحتاجه فقط عند تحويل PDF المصور إلى صور بواسطة pdf2image. الصور العادية وPDF النصي لا يحتاجانه.

هل المشروع يدعم العربية؟

نعم عند تثبيت بيانات اللغة العربية في Tesseract واستخدام ara+eng، لكن جودة النتيجة تعتمد على جودة الفاتورة.

لماذا تنتقل بعض الملفات إلى failed؟

إما لحدوث خطأ تقني، أو لأن واحدًا أو أكثر من الحقول الأساسية لم يتم استخراجه ويحتاج إلى مراجعة.

هل يمكن استخراج بنود الفاتورة كاملة؟

يمكن، لكن استخراج الجداول والبنود أصعب من الحقول الرئيسية ويستحق مرحلة مستقلة.

هل Streamlit ضرورية؟

لا. هي واجهة اختيارية فقط، والمشروع الأساسي يعمل من الطرفية بواسطة python main.py.

إرسال تعليق

أحدث أقدم