Парсинг PDF в JSON с LLM: сравнение моделей для точного извлечения данных | AiManual
AiManual Logo Ai / Manual.
11 Янв 2026 Гайд

Как парсить PDF в JSON с помощью LLM: сравнение моделей для точного извлечения структурированных данных

Глубокое руководство по извлечению структурированных данных из PDF с помощью LLM. Сравнение локальных моделей и API, оптимизация промптов, настройка пайплайна о

PDF - это ад. Не буквально, конечно, но каждый, кто пытался вытащить оттуда данные, знает это чувство. Традиционные парсеры ломаются о сложные макеты, таблицы, сканы. Regex'ы превращаются в костыли. И вот появляются LLM с обещанием понимать контекст и структуру. Но какая модель подойдет? Локальная? Облачная? Бесплатная? Давайте разберемся без маркетинговой шелухи.

Проблема: почему традиционные методы парсинга PDF - это боль

Вы пытаетесь извлечь данные из инвойса. Там есть таблица с позициями, суммами, налогами. PyPDF2 или pdfplumber выдают вам текст, но он разбит непредсказуемо. Строки сливаются, колонки путаются. Если документ - скан, то OCR (типа Tesseract) добавляет свои ошибки. В итоге вы тратите 80% времени не на анализ данных, а на их очистку.

Главная проблема не в чтении текста, а в понимании его структуры. Человек видит таблицу и понимает, что это заголовки, а это данные. Традиционный парсер видит просто последовательность слов и координаты.

LLM меняют правила игры. Они могут понять семантику: "это заголовок таблицы", "это общая сумма", "это дата выставления счета". Но не все модели одинаково полезны. Выбор зависит от трех факторов: точность, стоимость и скорость.

Решение: какой молоток взять для этого гвоздя?

Есть два лагеря: облачные API (GPT-4, Gemini, Claude) и локальные модели (Mistral, Llama, Qwen). Первые - мощные, но платные и требуют интернет. Вторые - приватные, могут работать оффлайн, но требуют железа и настройки.

МодельТипПлюсыМинусыКогда использовать
GPT-4 TurboAPI (OpenAI)Высочайшая точность, отличное понимание контекстаДорого, данные уходят в облакоКритически важные документы, сложная структура
Gemini FlashAPI (Google)Быстро, дешево, хороший балансМожет пропускать детали в больших PDFМассовая обработка, инвойсы, формы
Claude 3 HaikuAPI (Anthropic)Хорошо работает с длинными контекстамиЦена, иногда излишне подробный выводЮридические документы, длинные контракты
Llama 3.1 8BЛокальная (Ollama)Полная приватность, бесплатно после скачиванияТребует 8+ ГБ RAM, может ошибаться в деталяхВнутренние документы, когда данные не могут покидать сеть
Mistral 7BЛокальная (Ollama)Легче Llama, хороша для простых структурМеньший контекст, иногда галлюцинируетБыстрый прототип, простые PDF на слабом железе

Мой выбор для большинства задач? Gemini Flash через API. Почему? Цена - около $0.00015 за 1K токенов вывода. Скорость - несколько секунд на документ. Точность - для структурированных данных (инвойсы, заказы, формы) ее хватает с головой. Если приватность важнее денег - берите Llama 3.1 8B через Ollama. Но готовьтесь к танцам с бубном вокруг промптов.

💡
Забудьте про GPT-4 для массового парсинга. Это все равно что использовать Ferrari для доставки пиццы. Быстро, дорого, и вы все равно упретесь в лимиты токенов. Gemini Flash - это надежный фургон: дешево и сердито.

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 нужно поставить в жесткие рамки. Еще лучше - использовать технику системного промпта, где роль задается жестко.

💡
Боитесь, что модель начнет болтать? Укажите в промпте: "Любой текст вне JSON будет считаться ошибкой и приведет к штрафу". Это психологический трюк, но он работает даже с локальными моделями. Подробнее о борьбе с болтливостью в отдельной статье.

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 - это не волшебная палочка. Это еще один инструмент в арсенале инженера по обработке данных. Иногда умный, иногда капризный, но уже незаменимый.