Сценарии использования и решения

Распознавание счетов и заказов OCR в Excel: данные из PDF, сканов и фото с помощью локальной LLM

6 шагов 18 мин чтения 45 просмотров 0
Содержание

Операционные и финансовые отделы тратят часы на перепечатку счетов, заказов на закупку, накладных и коммерческих предложений поставщиков в таблицы. С помощью OCR, который читает сканы и фотографии, и локальной языковой модели, возвращающей ровно те поля, которые вы задали, большую часть этого ручного ввода можно автоматизировать. В статье Tesseract и OCRmyPDF объединяются со структурированным выводом (structured output) Ollama: каждый документ превращается в JSON-файл с извлечёнными полями, а сводная таблица отмечает документы, у которых сумма позиций не сходится с итогом, — так людям остаётся проверить только исключения. Всё работает на вашем собственном сервере.

Относитесь к извлечённым данным как к черновику: проверяйте итоговые суммы, банковские реквизиты и номера документов, прежде чем использовать их для платежей, бухгалтерского учёта или таможенного оформления. Документы с персональными данными нужно обрабатывать в соответствии с вашими обязательствами по защите данных; обработка на собственном сервере помогает их выполнять, но не освобождает от них.

Шаг 1: Установка инструментов

# Debian / Ubuntu: PDF text, OCR engine with English and Simplified Chinese, OCR for PDFs
apt install -y python3-venv poppler-utils tesseract-ocr tesseract-ocr-eng tesseract-ocr-chi-sim ocrmypdf
python3 -m venv /opt/extract
/opt/extract/bin/pip install ollama
ollama pull qwen2.5:7b

Настройка Ollama описана в статье о запуске локальной LLM с Ollama. Для других языков установите соответствующий пакет Tesseract, например tesseract-ocr-jpn, tesseract-ocr-tha или tesseract-ocr-rus.

Шаг 2: Проверьте, как извлекается текст из ваших документов

# Digital PDF (created by software): the text can be read directly
pdftotext -layout invoice.pdf - | head -40

# Scanned PDF: add a text layer with OCR first, then read it
ocrmypdf -l eng+chi_sim --skip-text scan.pdf scan-ocr.pdf
pdftotext -layout scan-ocr.pdf - | head -40

# Photo or screenshot
tesseract photo.jpg - -l eng+chi_sim | head -40

Если текст здесь читается, модель сможет извлечь из него поля. Если получается бессмыслица, сначала улучшите исходные материалы: сканируйте с разрешением 300 dpi, фотографируйте документы ровно и при хорошем освещении.

Шаг 3: Создание скрипта извлечения

Схема SCHEMA определяет, какие поля вам нужны. Ollama принудительно приводит ответ модели ровно к этой структуре, поэтому на выходе всегда получается корректный JSON.

cat > /opt/extract/extract.py <<'EOF2'
import csv, json, os, subprocess, sys, tempfile
import ollama

MODEL = os.environ.get("MODEL", "qwen2.5:7b")
LANG = os.environ.get("OCR_LANG", "eng+chi_sim")
SCHEMA = {
    "type": "object",
    "properties": {
        "supplier": {"type": "string"},
        "document_number": {"type": "string"},
        "date": {"type": "string", "description": "YYYY-MM-DD"},
        "currency": {"type": "string"},
        "total": {"type": "number"},
        "tax": {"type": "number"},
        "items": {"type": "array", "items": {"type": "object", "properties": {
            "description": {"type": "string"}, "quantity": {"type": "number"},
            "unit_price": {"type": "number"}, "amount": {"type": "number"}},
            "required": ["description", "quantity", "amount"]}},
    },
    "required": ["supplier", "document_number", "date", "currency", "total", "tax", "items"],
}

def text_of(path):
    run = lambda cmd: subprocess.run(cmd, capture_output=True, text=True).stdout
    if not path.lower().endswith(".pdf"):
        return run(["tesseract", path, "-", "-l", LANG])
    text = run(["pdftotext", "-layout", path, "-"])
    if len(text.strip()) > 50:
        return text
    with tempfile.TemporaryDirectory() as tmp:           # scanned PDF: OCR it first
        ocr = os.path.join(tmp, "ocr.pdf")
        subprocess.run(["ocrmypdf", "-q", "-l", LANG, "--skip-text", path, ocr], check=True)
        return run(["pdftotext", "-layout", ocr, "-"])

rows = []
for path in sys.argv[1:]:
    if not os.path.isfile(path):
        continue
    r = ollama.chat(model=MODEL, format=SCHEMA, options={"temperature": 0, "num_ctx": 8192}, messages=[
        {"role": "system", "content": "Extract the requested fields from the document text. "
         "Copy names, numbers and IDs exactly as written. Use an empty string or 0 "
         "for anything that is not in the text. Do not guess."},
        {"role": "user", "content": text_of(path)[:15000]}])
    d = json.loads(r.message.content)
    items = round(sum(i.get("amount", 0) for i in d["items"]), 2)
    ok = d["total"] > 0 and min(abs(items - d["total"]), abs(items + d["tax"] - d["total"])) < 0.02
    with open(os.path.splitext(path)[0] + ".json", "w", encoding="utf-8") as f:
        json.dump(d, f, ensure_ascii=False, indent=2)
    rows.append([os.path.basename(path), d["supplier"], d["document_number"], d["date"],
                 d["currency"], d["total"], d["tax"], len(d["items"]), "OK" if ok else "CHECK"])
    print(path, "OK" if ok else "CHECK", flush=True)

with open("summary.csv", "w", newline="", encoding="utf-8-sig") as f:
    w = csv.writer(f)
    w.writerow(["file", "supplier", "document_number", "date", "currency", "total", "tax", "lines", "check"])
    w.writerows(rows)
EOF2

Шаг 4: Обработка папки с документами

cd /data/invoices/2026-10
/opt/extract/bin/python /opt/extract/extract.py *.pdf *.jpg *.png
# For a large folder, run it in the background:
nohup /opt/extract/bin/python /opt/extract/extract.py *.pdf > extract.log 2>&1 &

Для каждого документа рядом с ним создаётся файл .json со всеми полями и позициями, а в summary.csv каждому документу соответствует одна строка. Откройте его в Excel и отфильтруйте столбец check: OK означает, что сумма позиций сходится с итогом (с налогом или без), CHECK — что этот документ должен посмотреть человек.

Шаг 5: Адаптация полей под ваши документы

  • Заказы на закупку: добавьте buyer, delivery_date и поля SKU в позициях.
  • Отгрузочные документы: используйте поля вроде tracking_number, consignee, weight_kg и packages.
  • Коммерческие предложения поставщиков: извлекайте valid_until, payment_terms и цены за единицу, чтобы сравнивать поставщиков в одной таблице.

Короткий description у поля, как у date выше, подсказывает модели ожидаемый формат. Если вы обрабатываете несколько типов документов, держите отдельную схему для каждого типа в отдельных скриптах или папках.

Шаг 6: Повышение точности

  • Лучшие результаты дают цифровые PDF; для сканов и фото всё зависит от качества изображения.
  • Оставляйте температуру равной 0, чтобы один и тот же документ всегда давал одинаковый результат.
  • Более крупные модели, например qwen2.5:14b, лучше справляются со сложной вёрсткой и длинными таблицами, но работают медленнее.
  • Храните исходный файл рядом с его JSON, чтобы при аудите любую цифру можно было отследить до первоисточника.

Частые вопросы

Сколько времени занимает обработка одного документа?

На CPU-сервере — обычно от нескольких десятков секунд для одностраничного счёта до нескольких минут для длинных многостраничных документов, в зависимости от модели. Большие пакеты запускайте на ночь.

Можно ли вместо этого отправлять документы в облачный ИИ?

Многие ИИ API тоже поддерживают структурированный вывод, но тогда документы покидают ваш сервер. Сначала проверьте свои обязательства по защите данных, условия провайдера и поддерживаемые им регионы. Для счетов, договоров и данных клиентов локальная схема обычно безопаснее.

Можно ли загружать результаты сразу в нашу учётную или ERP-систему?

Большинство систем умеют импортировать CSV-файлы: сопоставьте столбцы summary.csv или преобразуйте JSON-файлы в формат импорта. Автоматически импортируйте только строки с отметкой OK, а строки CHECK оставляйте для ручной проверки.

Если проблема не решена, создайте тикет — техническая поддержка IMIDC работает 24/7. Укажите IP сервера, версию ОС, используемую модель и вывод с ошибкой, а по возможности приложите пример документа без конфиденциальных данных, чтобы мы быстрее нашли причину.

Помог ли вам данный ответ?

Похожие руководства