PDF - это ад. Не буквально, конечно, но каждый, кто пытался вытащить оттуда данные, знает это чувство. Традиционные парсеры ломаются о сложные макеты, таблицы, сканы. Regex'ы превращаются в костыли. И вот появляются LLM с обещанием понимать контекст и структуру. Но какая модель подойдет? Локальная? Облачная? Бесплатная? Давайте разберемся без маркетинговой шелухи.
Проблема: почему традиционные методы парсинга PDF - это боль
Вы пытаетесь извлечь данные из инвойса. Там есть таблица с позициями, суммами, налогами. PyPDF2 или pdfplumber выдают вам текст, но он разбит непредсказуемо. Строки сливаются, колонки путаются. Если документ - скан, то OCR (типа Tesseract) добавляет свои ошибки. В итоге вы тратите 80% времени не на анализ данных, а на их очистку.
Главная проблема не в чтении текста, а в понимании его структуры. Человек видит таблицу и понимает, что это заголовки, а это данные. Традиционный парсер видит просто последовательность слов и координаты.
LLM меняют правила игры. Они могут понять семантику: "это заголовок таблицы", "это общая сумма", "это дата выставления счета". Но не все модели одинаково полезны. Выбор зависит от трех факторов: точность, стоимость и скорость.
Решение: какой молоток взять для этого гвоздя?
Есть два лагеря: облачные API (GPT-4, Gemini, Claude) и локальные модели (Mistral, Llama, Qwen). Первые - мощные, но платные и требуют интернет. Вторые - приватные, могут работать оффлайн, но требуют железа и настройки.
| Модель | Тип | Плюсы | Минусы | Когда использовать |
|---|---|---|---|---|
| GPT-4 Turbo | API (OpenAI) | Высочайшая точность, отличное понимание контекста | Дорого, данные уходят в облако | Критически важные документы, сложная структура |
| Gemini Flash | API (Google) | Быстро, дешево, хороший баланс | Может пропускать детали в больших PDF | Массовая обработка, инвойсы, формы |
| Claude 3 Haiku | API (Anthropic) | Хорошо работает с длинными контекстами | Цена, иногда излишне подробный вывод | Юридические документы, длинные контракты |
| Llama 3.1 8B | Локальная (Ollama) | Полная приватность, бесплатно после скачивания | Требует 8+ ГБ RAM, может ошибаться в деталях | Внутренние документы, когда данные не могут покидать сеть |
| Mistral 7B | Локальная (Ollama) | Легче Llama, хороша для простых структур | Меньший контекст, иногда галлюцинирует | Быстрый прототип, простые PDF на слабом железе |
Мой выбор для большинства задач? Gemini Flash через API. Почему? Цена - около $0.00015 за 1K токенов вывода. Скорость - несколько секунд на документ. Точность - для структурированных данных (инвойсы, заказы, формы) ее хватает с головой. Если приватность важнее денег - берите Llama 3.1 8B через Ollama. Но готовьтесь к танцам с бубном вокруг промптов.
1Шаг 1: Извлекаем текст из PDF (правильно)
Первая ошибка - скормить PDF напрямую в LLM. Большинство API принимают текст, а не файлы. Нужно сначала вытащить текст, сохранив структуру. Для этого используйте pdfplumber или pymupdf. Если есть сканы - pytesseract.
import pdfplumber
def extract_text_from_pdf(pdf_path):
full_text = []
with pdfplumber.open(pdf_path) as pdf:
for page in pdf.pages:
# Извлекаем текст с сохранением позиций (полезно для таблиц)
text = page.extract_text()
if text:
full_text.append(text)
return "\n\n".join(full_text)
# Если есть таблицы
with pdfplumber.open(pdf_path) as pdf:
page = pdf.pages[0]
table = page.extract_table() # Получаем таблицу как список списковПочему не PyPDF2? Он часто ломает пробелы и переносы. Pdfplumber лучше сохраняет layout.
Если документ очень длинный (более 100 страниц), не отправляйте его целиком. Разбейте на логические части (главы, разделы) или используйте RAG. Подробнее в статье про RAG для длинных PDF.
2Шаг 2: Пишем промпт, который заставит LLM молчать и парсить
Самая частая ошибка - промпт в духе "Извлеки данные из этого текста". LLM начнет болтать, объяснять, добавлять отсебятину. Нам нужен строгий, структурированный вывод. Вот шаблон, который работает:
prompt_template = """Ты - система извлечения структурированных данных. Извлеки информацию из предоставленного текста и верни ТОЛЬКО валидный JSON без каких-либо объяснений, комментариев или текста вне JSON.
Структура JSON, которую нужно вернуть:
{json_schema}
Текст для анализа:
{text}
Правила:
1. Если информация не найдена, используй null.
2. Строго следуй структуре выше.
3. Никакого дополнительного текста.
"""Ключевые моменты: "ТОЛЬКО валидный JSON", "никакого дополнительного текста". LLM нужно поставить в жесткие рамки. Еще лучше - использовать технику системного промпта, где роль задается жестко.
3Шаг 3: Определяем JSON-схему (это важнее, чем кажется)
Не говорите модели "верни данные о счете". Опишите точную структуру. Используйте JSON Schema или просто пример.
{
"invoice_number": "строка или null",
"date": "строка в формате YYYY-MM-DD",
"total_amount": число,
"currency": "код валюты (USD, EUR, RUB)",
"items": [
{
"description": "строка",
"quantity": число,
"unit_price": число
}
]
}Чем конкретнее схема, тем точнее результат. Укажите типы данных, форматы, обязательные поля. Это снижает вероятность галлюцинаций.
4Шаг 4: Отправляем в LLM и обрабатываем ответ
Теперь все собираем. Пример для Gemini Flash через Google AI Python SDK.
import google.generativeai as genai
import json
genai.configure(api_key="YOUR_API_KEY")
model = genai.GenerativeModel('gemini-1.5-flash')
def parse_pdf_to_json(pdf_text, json_schema):
prompt = prompt_template.format(json_schema=json.dumps(json_schema, indent=2), text=pdf_text)
response = model.generate_content(prompt)
# Извлекаем текст ответа
response_text = response.text.strip()
# Иногда модель оборачивает JSON в ```json ... ```
if response_text.startswith('```json'):
response_text = response_text[7:-3].strip()
elif response_text.startswith('```'):
response_text = response_text[3:-3].strip()
try:
parsed_json = json.loads(response_text)
return parsed_json
except json.JSONDecodeError as e:
print(f"Ошибка парсинга JSON: {e}")
print(f"Ответ модели: {response_text}")
return NoneОбратите внимание на обработку ответа. LLM любят оборачивать JSON в markdown-блоки. Нужно это чистить.
5Шаг 5: Валидация и постобработка
Никогда не доверяйте LLM на 100%. Всегда проверяйте выходные данные.
- Проверяйте типы данных: строка там, где должно быть число? Исправляйте.
- Ищите аномалии: отрицательное количество? Сумма не сходится? Возможно, модель ошиблась.
- Используйте эталоны: если обрабатываете однотипные документы, создайте набор правил (например, валюта всегда RUB).
Можно добавить второй проход с более простой моделью для проверки. Или использовать логический детектор ошибок.
Нюансы, которые сломают ваш пайплайн (если не знать)
1. Токены. Ограничение контекста. Gemini Flash - 1 млн токенов, но на практике для парсинга хватит 10-50к. Считайте токены заранее. Для длинных документов используйте карту документа или RAG.
2. Стоимость. API считают токены. 1 токен ~ 0.75 слова на английском. Русский текст может быть "токеннее". Прикиньте бюджет: 1000 документов по 10к токенов = 10 млн токенов. У Gemini Flash это около $0.15 за вывод. Плюс входные токены.
3. Галлюцинации. Модель может придумать данные, которых нет. Снижайте temperature (до 0.1), давайте четкие инструкции, используйте few-shot примеры (покажите образец правильного вывода).
4. Таблицы. Особенно сложные, с объединенными ячейками. Лучше извлекать таблицу отдельно (pdfplumber.extract_table()) и отправлять в LLM как массив, а не как текст.
5. Локальные модели. Они капризны. Нужно точно указать, какой JSON вы хотите. Иногда помогает ISON формат - более компактный, но не все модели его понимают.
Если вы работаете с медицинскими документами или другими чувствительными данными, локальная модель - must have. Посмотрите гайд по медицинским записям для вдохновения.
FAQ: короткие ответы на больные вопросы
| Вопрос | Ответ |
|---|---|
| Какой минимальный VPS для локальной модели? | Для Llama 3.1 8B - минимум 8 ГБ RAM, 4 ядра CPU. Лучше 16 ГБ и GPU (даже слабый). |
| Можно ли парсить сканы? | Да, но нужен OCR шаг. Используйте Tesseract с русским языком. Качество зависит от четкости скана. |
| Как ускорить обработку 1000 PDF? | Асинхронные запросы к API, батчинг (объединяйте несколько мелких документов в один запрос, если позволяет контекст). |
| Что делать, если модель возвращает не JSON? | Парсите ответ, ищите подстроку с '{' и '}', вырезайте. Добавьте в промпт угрозу "если не JSON - запрос не оплачивается". |
| Есть ли готовые инструменты? | Есть, но они или дорогие, или ограниченные. Свой пайплайн дает гибкость. Для начала можете попробовать LocalAI. |
Итог: что выбрать сегодня?
Для старта - Gemini Flash API. Быстро, дешево, работает из коробки. Когда наберете 1000 документов и поймете свои потребности, решите: масштабироваться на облаке или разворачивать локальное решение.
Локальные модели - это не про экономию (железо тоже стоит денег), а про контроль и приватность. Если вы парсите внутренние отчеты или медицинские карты - только локальные.
Самое главное - начните с маленького набора документов. Протестируйте 2-3 модели. Посчитайте точность не на глазок, а по метрикам (F1-score для извлеченных полей). Только так вы поймете, что работает именно для ваших данных.
И помните: LLM - это не волшебная палочка. Это еще один инструмент в арсенале инженера по обработке данных. Иногда умный, иногда капризный, но уже незаменимый.