Как распознать PDF нейросетью: страницы в картинки и запрос к модели
Сканы без текстового слоя обычным парсером не прочитать. Превращаем страницы в картинки, отправляем в модель и считаем, во что обойдётся документ.
6 мин · · разработка · автоматизация
Сканы договоров, счета от контрагентов, старые отчёты — PDF без текстового слоя обычный парсер не прочитает. Если нужно распознать PDF нейросетью, самый универсальный путь — превратить каждую страницу в картинку и отправить её модели, которая принимает изображения на вход. Ниже рабочий код на Python, выбор модели и расчёт стоимости по формуле.
Сначала проверьте, нужен ли vision вообще
PDF бывают двух видов: с текстовым слоем (выгружены из Word, 1С, браузера) и сканы или фото. В первом случае текст достаётся бесплатно, без модели:
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.
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.
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-sol | 0.13 | 0.63 |
gpt-5.6-terra | 0.13 | 0.63 |
deepseek-v4-pro | 0.088 | 0.25 |
qwen3.8-max | 0.1 | 0.263 |
claude-sonnet-5 | 0.2 | 1.013 |
gpt-5.6-sol | 0.22 | 1.07 |
gpt-6.1-sol | 0.27 | 1.32 |
claude-opus-5-5 | 0.4 | 2 |
gpt-6-astra | 0.62 | 3.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.
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 и проверьте его кодом:
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-sol | 150 000 × 0.13 / 1M = $0.0195 | 70 000 × 0.63 / 1M = $0.0441 | $0.0636 |
claude-sonnet-5 | 150 000 × 0.2 / 1M = $0.03 | 70 000 × 1.013 / 1M = $0.07091 | $0.10091 |
gpt-6.1-sol | 150 000 × 0.27 / 1M = $0.0405 | 70 000 × 1.32 / 1M = $0.0924 | $0.1329 |
claude-opus-5-5 | 150 000 × 0.4 / 1M = $0.06 | 70 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.
Похожие статьи
- Перевод субтитров нейросетью: SRT без сбитых таймкодовГлавный риск при переводе SRT нейросетью — сломанная структура файла. Показываем схему, где таймкоды не попадают к модели, и скрипт на Python с проверкой.
- Редактирование текстов нейросетью: корректура, стиль и сокращениеРазные виды редактуры требуют разных промптов. Показываем формулировки для корректуры, стиля и сокращения, как получать правки с объяснением и не терять авторский голос.
- Нейросеть для написания скриптов: Bash, PowerShell и PythonМодель быстро пишет скрипты для рутины, но запускать их вслепую нельзя. Показываем, как ставить задачу, требовать безопасный режим dry-run и проверять результат до запуска.
- Dify в Docker: установка, первичная настройка и подключение моделейСтавим Dify через Docker Compose, создаём администратора и подключаем модели Claude, GPT и DeepSeek по одному ключу. Отдельно — что делать, если контейнеры не стартуют.