В современном мире данных PDF-файлы остаются одним из самых распространенных, но и самых неудобных форматов для автоматизации. В отличие от Excel или CSV, PDF не хранит данные в виде ячеек и строк; это «визуальный» формат, созданный для печати, а не для обработки. Если вам нужно перенести финансовые отчеты, инвойсы или логистические журналы из PDF в базу данных, традиционные парсеры часто терпят неудачу, особенно когда речь идет о сложных макетах, объединенных ячейках или отсканированных документах. Здесь на сцену выходят большие языковые модели (LLM), способные «понимать» контекст документа, а не просто искать текст по координатам.
В этой статье мы подробно разберем, как построить надежный пайплайн для извлечения таблиц из PDF с использованием API OpenRouter. Мы не просто покажем код, но и углубимся в технические нюансы: как заставить модель возвращать строго типизированные данные, как обрабатывать сканы, как избежать «молчаливых» ошибок и как оптимизировать затраты. Вы узнаете, почему простой вызов API без проверок может привести к потере данных, и как использовать схемы JSON для превращения неструктурированного текста в готовые к анализу строки.
01Введение: Почему LLM лучше традиционных парсеров?
Традиционные библиотеки для работы с PDF (например, PyPDF2 или pdfplumber) полагаются на геометрическое расположение текста. Если таблица сдвинута, если ячейки объединены, или если документ является отсканированным изображением, такие библиотеки часто выдают мусор. LLM, напротив, обладают семантическим пониманием. Они могут понять, что текст в скобках означает отрицательное число, или что заголовок, растянутый на несколько колонок, относится ко всем значениям под ним. Наше тестирование показало, что модели, способные напрямую обрабатывать файлы (file-reading models), справляются с 644 ячейками в пяти различных PDF-файлах (включая два скана) со 100% точностью. Однако ключ к успеху кроется не только в выборе модели, но и в том, как вы формируете запрос, какую схему используете и какой маршрут (route) выбираете для доставки документа до модели.02Шаг 1: Отправка PDF и получение структурированных строк
Первый и самый важный шаг — это научить модель возвращать данные в формате, который легко использовать в коде. Для этого мы используем функциюstructured outputs (структурированный вывод) с жесткой JSON-схемой. Это гарантирует, что каждый ответ будет соответствовать ожидаемой структуре, а типы данных (целые числа, строки, числа с плавающей точкой) будут корректными.
Вот базовая функция на Python, которая отправляет PDF-файл в модель и получает обратно список словарей, где каждый словарь — это строка таблицы:
import base64
import json
import os
import requests
API = "https://openrouter.ai/api/v1/chat/completions"
HEADERS = {
"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"
}
def rows_schema(fields):
"""Wrap the fields of one row in a strict schema for {"rows": [...]}."""
return {
"type": "object",
"properties": {
"rows": {
"type": "array",
"items": {
"type": "object",
"properties": fields,
"required": list(fields),
"additionalProperties": False
}
}
},
"required": ["rows"],
"additionalProperties": False
}
def extract_table(pdf_path, schema, model="anthropic/claude-haiku-5.5", engine=None, max_tokens=8000):
with open(pdf_path, "rb") as f:
data_url = "data:application/pdf;base64," + base64.b64encode(f.read()).decode()
body = {
"model": model,
"max_tokens": max_tokens,
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "Extract every row of the table in this PDF. Include rows from every page. Return only rows that are in the document."},
{"type": "file", "file": {"filename": os.path.basename(pdf_path), "file_data": data_url}}
]
}],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "table",
"strict": True,
"schema": schema
}
}
}
if engine:
body["plugins"] = [{"id": "file-parser", "pdf": {"engine": engine}}]
response = requests.post(API, headers=HEADERS, json=body, timeout=300)
response.raise_for_status()
data = response.json()
choice = data["choices"][0]
if choice["finish_reason"] == "length":
raise ValueError(f"Output stopped at max_tokens={max_tokens}. Raise it or split the PDF.")
rows = json.loads(choice["message"]["content"])["rows"]
if not rows:
raise ValueError("No rows came back. If this PDF is a scan, use a model that reads PDFs or engine='mistral-ocr'.")
return rows, data["usage"]["cost"]
LINE_ITEMS = rows_schema({
"sku": {"type": "string"},
"description": {"type": "string"},
"qty": {"type": "integer"},
"unit_price": {"type": "number", "description": "USD, no currency symbol or thousands separator"},
"amount": {"type": "number", "description": "USD, no currency symbol or thousands separator"}
})
rows, cost = extract_table("invoice.pdf", LINE_ITEMS)
for row in rows[:]:
print(row)
print(f"{len(rows)} rows, ${cost:.5f}")Обратите внимание на поле description в схеме. Именно там мы указываем модели, что число должно быть без разделителей тысяч и символов валюты. В примере выше, сумма 6,067.00 из PDF была преобразована в число 6067.0, которое можно сразу использовать в математических операциях. Это избавляет от необходимости писать дополнительные функции очистки данных.
strict: True в схеме JSON. Это заставляет провайдеров (таких как Anthropic или OpenAI) ограничивать вывод модели только полями, указанными в схеме. Это предотвращает появление лишнего текста или «галлюцинаций» модели, которые могут сломать парсинг вашего JSON.03Шаг 2: Обработка сложных элементов: объединенные ячейки, скобки и сноски
Реальные документы редко бывают идеальными. В финансовых отчетах часто встречаются объединенные заголовки, отрицательные числа, записанные в скобках, и сноски, которые мешают чистоте данных. LLM отлично справляются с этими задачами, если правильно описать их в схеме.Рассмотрим пример квартального отчета. У нас есть заголовок «Fiscal 2026», который охватывает четыре колонки кварталов. Нам не нужно объяснять модели, что это заголовок, если мы уже назвали поля q1, q2, q3, q4. Однако нам нужно объяснить, как обрабатывать отрицательные числа. В бухгалтерии убытки часто записываются как (1,204). Мы можем добавить описание в схему:
THOUSANDS = {
"type": "integer",
"description": "Thousands of USD. A value in parentheses, like (1,204), is negative: -1204."
}
QUARTERLY = rows_schema({
"segment": {"type": "string", "description": "Segment name without footnote markers"},
"q1": THOUSANDS,
"q2": THOUSANDS,
"q3": THOUSANDS,
"q4": THOUSANDS,
"fy_total": THOUSANDS
})В результате модель вернет значение -3878 для строки, где в PDF было (3,878). Также, если в названии сегмента есть маркеры сносок (например, Enterprise*), описание поля "Segment name without footnote markers" заставит модель убрать звездочку. Это экономит часы ручной очистки данных.
Что касается объединенных ячеек (merged cells), которые часто встречаются в сканированных документах, где одна категория растягивается на несколько строк, достаточно добавить в описание поля инструкцию: "The category in the first column. It is a merged cell, so repeat it on every row it covers.". В наших тестах этого оказалось достаточно, чтобы модель заполнила категорию для всех 30 строк, даже на «грязном» скане.
04Шаг 3: Выбор маршрута парсинга (Parser Route)
Не все модели могут читать PDF-файлы напрямую. Некоторые модели (например, Claude Haiku 5.5, GPT-6 Luna, Gemini 3.8 Flash) имеют встроенную способность обрабатывать файлы. Для них нужно просто передать PDF. Другие модели (например, некоторые версии DeepSeek) принимают только текст или изображения. Для них требуется промежуточный этап — OCR (оптическое распознавание символов) или текстовый парсинг.В OpenRouter это настраивается через параметр engine в плагине file-parser:
- Модель читает PDF напрямую: Не указывайте
engine. PDF будет отправлен как есть. Это самый дешевый и быстрый способ. - Модель не читает PDF, нужен OCR: Установите
engine="mistral-ocr". Это запустит OCR на каждой странице. Стоимость: $2 за 1000 страниц поверх стоимости токенов модели. Это необходимо для сканов. - Бесплатный парсер: Установите
engine="cloudflare-ai". Этот метод работает только если у PDF есть текстовый слой (не скан). Для сканов он вернет пустой результат.
cloudflare-ai для отсканированного документа, API вернет успешный ответ (200 OK) с пустым списком строк {"rows": []}. Ошибки не будет. Если вы не проверите длину списка строк, ваша система сохранит пустую таблицу и продолжит работу, считая, что все прошло успешно. Всегда добавляйте проверку: if not rows: raise ValueError(...).Также будьте осторожны с параметром engine="native". Он предназначен только для моделей, которые нативно поддерживают файлы. Если вы попытаетесь использовать его с моделью, которая этого не делает, вы получите ошибку провайдера или, в худшем случае, молчаливый сбой.
05Шаг 4: Проблема длинных таблиц и лимит токенов
Каждая сгенерированная строка таблицы стоит токенов. Если таблица большая, модель может остановиться на середине вывода, достигнув лимитаmax_tokens. В нашем тесте журнал отгрузок на 58 строк потребовал от 2500 до 7700 выходных токенов в зависимости от модели. Если оставить стандартный лимит в 8000 токенов,verbose-модели могут не уложиться.
Рекомендуемое правило: устанавливайте max_tokens примерно из расчета 150 токенов на ожидаемую строку. Для 58 строк это около 8700 токенов. Но главное — проверять поле finish_reason в ответе API.
if choice["finish_reason"] == "length":
raise ValueError(f"Output stopped at max_tokens={max_tokens}.")Если вы не сделаете эту проверку, вы столкнетесь с двумя типами ошибок. Первая — это ошибка парсинга JSON (например, JSONDecodeError: Unterminated string), так как JSON-массив будет обрезан. Вторая, более коварная, — это использование плагина Response Healing. Этот плагин пытается «починить» сломанный JSON, и он может успешно вернуть 5 строк из 58, не выдав ошибки. Но вы потеряете 53 строки данных. Проверка finish_reason ловит оба случая.
06Шаг 5: Запись результатов в CSV
Поскольку мы получили данные в виде списка словарей (list of dicts), запись в CSV тривиальна. Python-библиотекаcsv позволяет использовать ключи словарей в качестве заголовков столбцов.
import csv
from extract_tables import extract_table
# Определение SHIPMENTS см. в шаге 4
rows, cost = extract_table("shipments.pdf", SHIPMENTS, model="openai/gpt-6-luna")
with open("shipments.csv", "w", newline="") as f:
fieldnames = list(SHIPMENTS["properties"]["rows"]["items"]["properties"].keys())
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
print(f"Wrote {len(rows)} rows to shipments.csv (${cost:.5f})")Результатом будет файл shipments.csv, который можно сразу открыть в Excel. Обратите внимание, что даже если таблица в PDF разбивалась на две страницы с повторяющимся заголовком, наша функция вернет один заголовок и все 58 строк данных. Это происходит потому, что мы просим модель извлечь «каждую строку», а не копировать макет документа.
07Какая модель лучше? Сравнение затрат и точности
Мы протестировали несколько моделей на наборе из пяти PDF-файлов (6 страниц, 644 ячейки). Вот ключевые выводы:- GPT-6 Luna и Claude Haiku 5.5: Самые дешевые маршруты, которые показали 100% точность, включая работу с «грязными» сканами. Стоимость около $0.10 за миллион входных токенов. Это отличный выбор для начала.
- Gemini 3.8 Flash: Также показал 100% точность, но стоит в 8-10 раз дороже ($0.75-$1.50 за миллион токенов). Если бюджет не ограничен, это надежный вариант, но переплата не всегда оправдана для простых таблиц.
- DeepSeek с OCR (mistral-ocr): Сам по себе DeepSeek дешев, но добавление OCR ($2 за 1000 страниц) делает его дороже. Кроме того, в тестах он допустил ошибки: оставил маркеры сносок в названиях сегментов, несмотря на инструкции в схеме.
- Cloudflare AI (бесплатный парсер): Идеально работает с цифровыми PDF (экспортированными из Excel/Word), но полностью провалил сканы, вернув 0 строк.
08Когда библиотеки PDF достаточно?
LLM — это мощный инструмент, но он не всегда необходим. Если ваши PDF-файлы являются цифровыми экспортами (не сканами) и имеют стабильную структуру, простые библиотеки вродеpdfplumber или camelot могут быть быстрее и дешевле. Они не требуют API-ключей и работают локально. Однако, если у вас есть сканы, сложные макеты, рукописные пометки или переменная структура документов, LLM-подход с OpenRouter будет значительно надежнее.
09Что это значит на практике
Извлечение таблиц из PDF с помощью LLM — это не просто «вызов API». Это проектирование системы, которая учитывает природу входных данных. Вот чек-лист для внедрения этого решения в ваш рабочий процесс:- Определите тип документов: Если это сканы, вам нужна модель с поддержкой файлов или маршрут с OCR. Если это цифровые PDF, можно использовать более дешевые модели и парсеры.
- Создайте строгую схему: Не полагайтесь на то, что модель «понимает» формат. Опишите каждый столбец, укажите типы данных и правила очистки (например, «убрать скобки для отрицательных чисел»).
- Защититесь от сбоев: Всегда проверяйте
finish_reasonнаlengthи длину списка строк на0. Без этих проверок вы рискуете получить пустые или обрезанные данные, которые ваша система примет за успешные. - Тестируйте на реальных данных: Используйте «золотой набор» (golden eval set) — небольшой набор документов с известным правильным ответом, чтобы оценить точность и стоимость перед запуском в продакшен.
- Масштабируйте правильно: Рассчитывайте
max_tokensисходя из количества строк. Для длинных таблиц увеличивайте лимит или разбивайте PDF на части.
Используя эти принципы, вы можете автоматизировать обработку тысяч документов, превращая неструктурированные PDF в чистые, готовые к анализу данные с минимальными затратами и максимальным уровнем надежности.
10Часто задаваемые вопросы
Можно ли использовать этот метод для изображений?
Да, если вы используете модель, которая принимает изображения, или если вы применяете OCR-парсер. Однако для изображений лучше использовать модели, специально обученные на визуальных данных (VLM), такие как GPT-4o или Claude 3.5 Sonnet, хотя в данном руководстве мы фокусировались на PDF.
Что делать, если модель игнорирует часть таблицы?
Проверьте, не превышает ли таблица лимит токенов. Также убедитесь, что в промпте четко указано: Include rows from every page. Иногда модели «забывают» о второй странице, если она визуально отделена от первой.
Как снизить стоимость?
Используйте самые дешевые модели, которые показывают приемлемую точность (например, Claude Haiku или GPT-4o-mini, если они поддерживают файлы). Избегайте OCR, если это возможно, используя модели, читающие PDF напрямую. Оптимизируйте схему, убирая лишние поля, которые не нужны для анализа.
11Заключение
Интеграция LLM для извлечения данных из PDF открывает новые возможности для автоматизации бизнес-процессов. Ключ к успеху лежит в сочетании правильной модели, строгой схемы вывода и надежной обработки ошибок. Следуя приведенным в этой статье рекомендациям, вы сможете создать robust-систему, которая будет работать стабильно даже с самыми сложными документами.12Следующие шаги
Попробуйте запустить код с вашим собственным PDF-файлом. Начните с простой схемы, затем усложняйте её, добавляя описания для очистки данных. Экспериментируйте с разными моделями и маршрутами, чтобы найти оптимальное соотношение цены и качества для вашего конкретного случая использования.Источник: OpenRouter ↗