Как распознать PDF нейросетью: страницы в картинки и запрос к модели

Сканы без текстового слоя обычным парсером не прочитать. Превращаем страницы в картинки, отправляем в модель и считаем, во что обойдётся документ.

6 мин · · разработка · автоматизация

Сканы договоров, счета от контрагентов, старые отчёты — PDF без текстового слоя обычный парсер не прочитает. Если нужно распознать PDF нейросетью, самый универсальный путь — превратить каждую страницу в картинку и отправить её модели, которая принимает изображения на вход. Ниже рабочий код на Python, выбор модели и расчёт стоимости по формуле.

Сначала проверьте, нужен ли vision вообще

PDF бывают двух видов: с текстовым слоем (выгружены из Word, 1С, браузера) и сканы или фото. В первом случае текст достаётся бесплатно, без модели:

bash
import pymupdf  # pip install pymupdf

with pymupdf.open("invoice.pdf") as doc:
    for page in doc:
        text = page.get_text()
        print(page.number, len(text.strip()))

Если get_text() возвращает осмысленный текст, отправляйте в модель текст, а не картинку: текстовый вход обычно дешевле и предсказуемее. Картинка нужна, когда текстового слоя нет, или когда важна вёрстка: таблицы с объединёнными ячейками, печати, рукописные пометки на полях.

Шаг 1. Страницы PDF в картинки

Вариант A: PyMuPDF

PyMuPDF ставится одной командой и не требует внешних программ. Метод Page.get_pixmap(dpi=...) рендерит страницу в растровое изображение, Pixmap.tobytes("png") возвращает байты PNG. Оба метода описаны в официальной документации PyMuPDF.

bash
import base64
import pymupdf


def pdf_pages_to_png_b64(path: str, dpi: int = 150) -> list[str]:
    pages = []
    with pymupdf.open(path) as doc:
        for page in doc:
            pix = page.get_pixmap(dpi=dpi)
            png_bytes = pix.tobytes("png")
            pages.append(base64.b64encode(png_bytes).decode("ascii"))
    return pages

В старых примерах встречается import fitz — это прежнее имя модуля. Документация PyMuPDF рекомендует писать import pymupdf.

Вариант B: pdf2image

pdf2image — обёртка над утилитами poppler. Сначала установите poppler: на macOS brew install poppler, в Linux пакет poppler-utils. Функция convert_from_path возвращает список изображений PIL.

bash
import base64
import io

from pdf2image import convert_from_path  # pip install pdf2image


def pdf_pages_to_png_b64(path: str, dpi: int = 150) -> list[str]:
    result = []
    for image in convert_from_path(path, dpi=dpi):
        buf = io.BytesIO()
        image.save(buf, format="PNG")
        result.append(base64.b64encode(buf.getvalue()).decode("ascii"))
    return result

Для длинных файлов пригодятся параметры first_page и last_page: можно обрабатывать документ частями и не держать сотни картинок в памяти.

Какое разрешение выбрать

150 dpi — разумная отправная точка для печатного текста: лист A4 получается примерно 1240×1754 пикселя. Мелкий шрифт и сноски могут потребовать 200 dpi. Чем больше картинка, тем больше токенов за неё насчитает поставщик модели, а слишком большие изображения вендоры обычно уменьшают сами — поэтому бесконечно поднимать dpi бессмысленно. Подберите значение на 3–5 типичных страницах. Про ограничения на размер и формат — в статье об ошибках при отправке изображений.

Шаг 2. Какую модель выбрать

Картинки на вход принимают не все модели. Вот модели из каталога Zukko с отметкой «картинки на вход: да» (цены на 6 октября 2026, $ за 1M токенов):

МодельВходВыход
gpt-6-sol0.130.63
gpt-5.6-terra0.130.63
deepseek-v4-pro0.0880.25
qwen3.8-max0.10.263
claude-sonnet-50.21.013
gpt-5.6-sol0.221.07
gpt-6.1-sol0.271.32
claude-opus-5-50.42
gpt-6-astra0.623.05

Также изображения принимают claude-sonnet-4-6, claude-opus-5 и claude-fable-5-1. А вот gemini-3.8-flash, deepseek-v4.1-flash, gpt-6-luna, glm-5.3 и другие модели с отметкой «нет» картинки не принимают — им можно отдавать только текст из текстового слоя.

Какая модель лучше читает именно ваши документы, заранее не скажет никто. Возьмите 10 страниц с заранее известным правильным текстом, прогоните через 2–3 модели и сравните результат построчно. Пример отправки картинки в самую недорогую из этих моделей — в статье DeepSeek V4 Pro через API. Актуальный список и цены — GET https://api.zukko.pro/v1/models или страница «Модели» в кабинете.

Шаг 3. Отправка страницы в модель

Используем OpenAI-совместимый формат: картинка передаётся как data URL в части сообщения с типом image_url. Шлюз конвертирует запрос, поэтому тот же код работает и с claude-sonnet-5, и с gpt-6.1-sol — меняется только имя модели. Подробнее о клиенте — в документации по Python.

bash
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://api.zukko.pro/v1",
    api_key=os.environ["ZUKKO_API_KEY"],  # ключ вида fetch_...
)

OCR_PROMPT = (
    "Это страница отсканированного документа. Перепиши весь текст дословно, "
    "сохраняя порядок абзацев. Таблицы оформи в Markdown. "
    "Если фрагмент не читается, напиши [неразборчиво]. Ничего не додумывай."
)

Одна страница — один запрос. Так проще повторить упавшую страницу, легче уложиться в лимиты на размер запроса, а результат сразу привязан к номеру страницы.

Таблицы и поля в JSON

Для счетов и актов обычно нужен не сплошной текст, а конкретные поля. Попросите модель вернуть JSON и проверьте его кодом:

bash
import json

FIELDS_PROMPT = (
    "Извлеки из счёта поля и верни только JSON без пояснений: "
    '{"number": str, "date": "YYYY-MM-DD", "seller_inn": str, '
    '"total": number, "items": [{"name": str, "qty": number, "price": number}]}. '
    "Если поля нет на странице, поставь null."
)


FENCE = "`" * 3


def parse_json(text: str) -> dict:

Программная проверка — контрольные суммы, формат даты, длина ИНН (10 или 12 цифр) — ловит ошибки распознавания лучше, чем повторный запрос к той же модели.

Шаг 4. Сколько стоит распознать PDF

Формула для одного документа:

стоимость = вход_токены × цена_входа / 1 000 000 + выход_токены × цена_выхода / 1 000 000

Важная оговорка: сколько токенов «весит» картинка, считает поставщик модели по своим правилам, и одна и та же страница у разных вендоров даёт разное число. Поэтому не прикидывайте на глаз: прогоните 5–10 страниц и возьмите prompt_tokens и completion_tokens из usage — код выше их суммирует.

Пример с условными цифрами: допустим, на ваших сканах вышло в среднем 1 500 входных токенов (картинка плюс промпт) и 700 выходных на страницу. Документ на 100 страниц — это 150 000 входных и 70 000 выходных токенов.

МодельВходВыходИтого
gpt-6-sol150 000 × 0.13 / 1M = $0.019570 000 × 0.63 / 1M = $0.0441$0.0636
claude-sonnet-5150 000 × 0.2 / 1M = $0.0370 000 × 1.013 / 1M = $0.07091$0.10091
gpt-6.1-sol150 000 × 0.27 / 1M = $0.040570 000 × 1.32 / 1M = $0.0924$0.1329
claude-opus-5-5150 000 × 0.4 / 1M = $0.0670 000 × 2 / 1M = $0.14$0.20

Для сравнения: по официальной цене вендора ($2 / $10) тот же объём на GPT-6.1 Sol стоил бы 150 000 × 2 / 1M + 70 000 × 10 / 1M = $0.30 + $0.70 = $1.00. Подставьте в формулу свои средние значения из usage — и получите реальную цену своего документа.

Практические советы

  • Таблица на две страницы. Отправьте обе картинки в одном запросе (две части image_url в одном сообщении) и попросите собрать одну таблицу.
  • Отдельный ключ под пайплайн. В кабинете у ключа можно задать дневной и месячный лимит в долларах и список разрешённых моделей — если скрипт зациклится, он не израсходует весь баланс. Подробнее — в разделе о лимитах ключа.
  • Ошибки. 413 — запрос слишком большой: уменьшите dpi или отправляйте страницы по одной. 429 — исчерпан дневной или месячный лимит ключа (key_daily_limit_reached / key_monthly_limit_reached) — поднимите лимит или дождитесь сброса — либо ограничение у провайдера модели: добавьте паузу между запросами. Общего ограничения частоты у Zukko нет.

Ключ создаётся в кабинете на странице API-ключи; баланс пополняется от $1, в том числе через СБП и картой РФ.

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

Можно ли отправить PDF целиком, без конвертации?

У некоторых вендоров в собственных API есть отдельный приём PDF-файлов, но в документации Zukko такой способ не описан. Путь через картинки работает с любой моделью, у которой в каталоге стоит «картинки на вход: да», и код одинаков для Claude и GPT.

Какой dpi ставить?

Начните со 150 dpi для печатного текста и 200 dpi для мелкого шрифта. Проверьте на нескольких страницах: если качество не растёт, а входных токенов становится больше, вернитесь к меньшему значению.

Почему у Claude и GPT разное число токенов за одну страницу?

Каждый поставщик считает токены изображения по своим правилам. Сравнивайте модели по итоговой стоимости страницы из usage, а не по цене за миллион токенов.

Справится ли модель с рукописным текстом?

Зависит от модели и качества скана. Соберите 10–20 образцов своих документов, прогоните через несколько моделей и сравните с правильным текстом — это единственный надёжный способ.

zukko

Платите в 25×
меньше

за Claude Code & ChatGPT

Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.