Как извлечь таблицы из PDF с помощью LLM

Источник: OpenRouter Blog•

Как извлечь таблицы из PDF с помощью LLM

Отправьте PDF-файл модели, которая умеет читать файлы, со строгой JSON-схемой для одной строки, и вы получите типизированные строки в ответ — даже из сканированных документов и таблиц, занимающих несколько страниц. Мы протестировали пять вариантов на пяти PDF-файлах и обнаружили два сбоя, при…

Чтобы извлечь таблицы из PDF с помощью LLM, отправьте PDF модели, способной читать файлы, вместе со строгой JSON-схемой, описывающей одну строку таблицы. Модель возвращает типизированные строки. Числовые значения возвращаются как целые числа, убыток, записанный как (3,878), возвращается как -3878, а таблица, занимающая две страницы, возвращается как единый список.

Мы протестировали это на пяти PDF-файлах, два из которых были сканами. Каждая модель, прочитавшая PDF напрямую, распознала все 644 ячейки правильно. Сбои в наших тестах были связаны с тем, по какому маршруту PDF поступал к модели, причем два из них возвращали нормальный ответ без ошибки.

Коротко

  • Строгая JSON-схема для одной строки превращает таблицу из PDF в типизированные строки, а описания полей помогают очистить данные, например убрать отрицательные числа в круглых скобках и маркеры сносок.
  • В наших тестах модели, читавшие PDF напрямую, справились как с цифровыми файлами, так и со сканами. Модели, которые не умеют читать PDF, для сканов требуют парсер mistral-ocr.
  • Скан, отправленный через бесплатный парсер cloudflare-ai, возвращает ноль строк без ошибок, поэтому пустые результаты необходимо отсеивать.
  • Длинные таблицы могут остановиться из-за лимита токенов вывода, поэтому отклоняйте любые ответы со значением finish_reason, равным «length».

Прежде чем начать

Для запуска примеров вам понадобится следующее.

  • Аккаунт на OpenRouter с положительным балансом и ключ API, сохраненный в переменной окружения OPENROUTER_API_KEY. Модели в этом руководстве являются платными.
  • Python 3.10 или новее и пакет requests (pip install requests).
  • Node.js 18 или новее, но только для версии на TypeScript в конце.
  • PDF-файл, содержащий таблицу. Это может быть цифровой экспорт или скан.

Если вы раньше не вызывали API, в руководстве по быстрому старту показан первый запрос.

Код работает с цифровыми PDF и сканами, но результаты зависят от наличия текстового слоя в PDF, качества скана, выбранного маршрута парсера и модели. В примерах используются пять тестовых PDF-файлов, созданных нами на основе известных данных, чтобы каждую ячейку можно было сверить с исходником. Это счет-фактура, квартальный отчёт, журнал отгрузок на 58 строк, переходящий на вторую страницу, отсканированный заказ на поставку и намеренно некачественный скан со слитными ячейками.

Шаг 1: Как отправить PDF и получить строки в ответ?

Сохраните следующий код как extract_tables.py. Он отправляет PDF в виде содержимого файла, как описано на нашей странице вводов PDF, и запрашивает JSON, соответствующий вашей схеме, с помощью структурированных выводов. Если для параметра strict задано значение true, провайдеры, поддерживающие строгий режим, гарантируют соответствие вывода схеме, поэтому каждая строка содержит каждое поле правильного типа. Поддержка различается в зависимости от провайдера, а страница структурированных выводов показывает, как направлять запросы только тем провайдерам, которые ее поддерживают.

Две проверки перед возвратом результата перехватывают сбои, описанные на шагах 3 и 4, поэтому не удаляйте их. Затраты берутся из поля usage.cost, которое учет использования включает в каждый ответ.

Запустите функцию для счета-фактуры.

Вызов занял около шести секунд. В PDF указано 6,067.00, а строка содержит 6067.0 — это число, которое можно суммировать без предварительной очистки. Именно описание поля удалило разделитель тысяч.

Шаг 2: Как обрабатывать объединенные ячейки, круглые скобки и сноски?

Большую часть очистки, необходимую таблице из PDF, можно вынести в описания полей схемы. В нашем квартальном отчете есть заголовок «Fiscal 2026», охватывающий четыре столбца кварталов, убытки в круглых скобках и маркеры сносок у двух названий сегментов (Enterprise и Hardware resale). Описания полей справляются со скобками и маркерами сносок. Объединенный заголовок не требует описания, так как имена полей уже говорят о том, к какому кварталу относится каждое значение.

В строке Hardware resale значение (3,878) вернулось как -3878, а маркеры сносок исчезли из названий обоих сегментов.

Объединенная ячейка, которая тянется вниз по столбцу, работает аналогично. В нашем некачественном скане каждая ячейка Category охватывает от шести до девяти строк. Мы описали это поле как «Категория в первом столбце. Это объединенная ячейка, поэтому повторяйте ее для каждой строки, которую она покрывает». В нашем тесте этого оказалось достаточно, чтобы все три модели, читавшие PDF, заполнили категорию для всех 30 строк.

Шаг 3: Какой маршрут парсера следует использовать?

Если модель умеет читать файлы, мы передаем PDF ей напрямую. Если нет, мы сначала парсим PDF и отправляем модели результат. Настройка engine плагина file-parser выбирает парсер. Если оставить ее неустановленной для модели, которая не умеет читать файлы, мы используем mistral-ocr, как указано на странице вводов PDF. Выбирайте маршрут в зависимости от внешнего вида ваших PDF-файлов.

  • Модель, которая читает PDF. Не устанавливайте engine. Claude Haiku 5.5, GPT-6 Luna и Gemini 3.8 Flash умеют читать файлы, и все три справились с обоими нашими сканами. За PDF взимается плата в виде входных токенов.
  • Модель, которая не читает PDF, с OCR. Используйте engine="mistral-ocr" или оставьте параметр неустановленным. Сначала она выполняет оптическое распознавание символов (OCR) для каждой страницы по цене 2 доллара за 1000 страниц сверх токенов модели.
  • Модель, которая не читает PDF, с бесплатным парсером. Используйте engine="cloudflare-ai" только в том случае, если каждый PDF имеет текстовый слой. Экспортированные счета и отчеты имеют его. Сканы — нет.

Вот отсканированный заказ на поставку, отправленный на deepseek/deepseek-v4.1-flash с каждым парсером.

Эта ошибка ValueError исходит из проверки пустых строк на шаге 1. Без этой проверки второй вызов возвращает 200 OK с finish_reason: "stop" и {"rows": []}. Парсер не нашел текста на скане, и модель корректно сообщила о пустой таблице. Ничто не вызывает ошибку, поэтому конвейер без проверки сохраняет ноль строк и переходит к следующему файлу. Это первый из двух молчаливых сбоев. Наш некачественный скан повел себя точно так же и набрал 0 из 30 строк.

Две другие ошибки в маршрутах приводят к явным сбоям. Неправильно написанное имя движка возвращает следующую ошибку.

Родной движок предназначен только для моделей, которые читают файлы. Когда мы все же установили engine="native" для DeepSeek V4.1 Flash, запрос сработал один раз, а затем дважды завершился с ошибкой провайдера.

Если оставить engine неустановленным для той же модели, это сработало оба раза, когда мы пробовали, поэтому не устанавливайте native для модели, которая не умеет читать файлы.

Шаг 4: Почему моя длинная таблица вернулась обрезанной?

Каждая строка, которую пишет модель, расходует токены вывода, поэтому для длинной таблицы требуется высокое значение max_tokens. Наш журнал отгрузок на 58 строк использовал от 2500 до 7700 выходных токенов в зависимости от модели, что оставляло самой многословной модели мало пространства в рамках значения по умолчанию в 8000 в extract_table. Это составляет от 43 до 133 токенов на строку. Для длинных строк начинайте примерно со 150 токенов на ожидаемую строку. Затем измеряйте собственные строки и сохраняйте проверку finish_reason, так как количество зависит от модели и схемы. Вот что происходит, когда лимит установлен слишком низко.

Без проверки finish_reason этот запрос завершается сбоем одним из двух способов. Обычный парсинг JSON вызывает ошибку на обрезке массива.

Второй способ — это еще один молчаливый сбой. При включенном плагине Response Healing JSON парсится, и вы получаете 5 строк из 58 без исключений. Healing исправляет синтаксис вывода, однако в его документации предупреждается, что он не может восстановить вывод, обрезанный из-за max_tokens, поэтому он не может добавить строки, которые модель так и не написала. Проверка finish_reason перехватывает оба случая.

Шаг 5: Как записать строки в CSV?

Каждая строка содержит ключи, определенные в схеме, поэтому csv.DictWriter может взять имена столбцов из схемы и записать строки в исходном виде. Excel открывает полученный файл напрямую.

Таблица перенеслась на вторую страницу, и ее заголовок повторился там. В CSV-файле содержится один заголовок и все 58 строк.

Какую модель следует использовать?

Мы прогнали каждый маршрут по одному разу для всех пяти PDF-файлов, что составило шесть страниц и 644 ячейки, и сравнили каждую ячейку с исходными данными, из которых были сгенерированы PDF-файлы. Каждый запуск использовал функцию extract_table и приведенный выше промпт с маршрутизацией через провайдера по умолчанию. Стоимость за 1000 страниц представляет собой общее значение usage.cost для шести страниц в пересчете на больший объем. Модели и цены указаны по состоянию на 8 октября 2026 года в соответствии с нашим каталогом моделей. Эти результаты описывают данный тестовый набор, а не производительность моделей в целом.

На этом тестовом наборе GPT-6 Luna и Claude Haiku 5.5 оказались самыми дешевыми маршрутами, которые правильно распознали каждую ячейку, в том числе на скане низкого качества, и обе модели стоят 0,10 долл. США за миллион входных токенов. Это делает их разумным вариантом для первого тестирования на ваших собственных PDF-файлах. Gemini 3.8 Flash продемонстрировала такую же точность, но обошлась в восемь-десять раз дороже за страницу на тех же файлах. Ее прейскурантная цена составляет 1,50 долл. США за миллион входных токенов со скидкой до 0,75 долл. США 21 октября 2026 года.

Маршрут с использованием OCR обходится дороже, чем предполагают низкие цены DeepSeek на токены, поскольку к каждому запросу добавляется 2 долл. США за 1000 страниц за OCR, и это отражено в параметре usage.cost. Две ошибки представляли собой маркеры сносок в разделах Enterprise и Hardware resale, которые модель сохранила, несмотря на требование схемы удалить их. Бесплатный парсер правильно обработал все три цифровых PDF-файла и полностью ошибочно — оба скана.

Стоимость в столбце затрат получена путем экстраполяции на основе шести страниц для каждого маршрута. Используйте ее для сравнения маршрутов, а затем измерьте точность, время и стоимость на выборке ваших собственных PDF-файлов, прежде чем сделать выбор, используя золотой набор для оценки, поскольку более плотные страницы стоят дороже, а другие макеты могут вести себя иначе.

Когда достаточно библиотеки для работы с PDF?

Если все ваши PDF-файлы являются цифровыми, а каждая таблица имеет один заголовок, бесплатная библиотека, такая как pdfplumber, найдет таблицы. Мы запустили ее на тех же файлах. Она нашла каждую таблицу в трех цифровых PDF-файлах и вернула каждую ячейку в виде строки, например «6 067,00» или «(3 878)». Двухуровневый заголовок квартального отчета вернулся в виде двух строк со значением None в объединенных ячейках, а журнал отгрузок вернулся в виде двух отдельных таблиц — по одной на страницу. Библиотека ничего не нашла ни в одном из сканов, поскольку в скане отсутствует текстовый слой для чтения.

Таким образом, библиотеки достаточно, если ваши таблицы просты, а PDF-файлы имеют цифровой формат. Как только вам приходится писать код очистки для объединенных заголовков, разрывов страниц и форматов чисел, схема из шага 2 требует меньше усилий по поддержке.

Тот же запрос на TypeScript

Тело запроса одинаково на любом языке. Сохраните это как extract.mjs и запустите с помощью node extract.mjs invoice.pdf в Node.js 18 или новее.

Скрипт проверяет статус HTTP, а затем выполняет те же две проверки, что и версия на Python. Он останавливает выполнение, если finish_reason не равен «stop», а массив rows не содержит хотя бы одну строку.

Заключение

Извлечение таблицы из PDF с помощью LLM требует одного запроса, в котором PDF передается как часть файла, а также строгой схемы, описывающей одну строку. В наших тестах точность обеспечивалась чтением PDF моделью напрямую, а риск зависел от маршрута, который прошел PDF. Текстовый парсер на скане и ограничение токенов для длинной таблицы выдали ответы, выглядящие нормальными. Две проверки в функции extract_table отслеживают оба этих случая, поэтому сохраняйте их в любой используемой версии кода. Прежде чем остановиться на определенном маршруте, пропустите через него выборку ваших собственных PDF-файлов и сравните строки, время и параметр usage.cost.

Часто задаваемые вопросы

Может ли LLM читать сканированный PDF?

Да, если модель читает PDF напрямую или вы отправляете его через парсер mistral-ocr. Все три модели, читающие PDF напрямую, правильно обработали оба наших скана, как и DeepSeek V4.1 Flash с mistral-ocr. Текстовый парсер, такой как cloudflare-ai, возвращает пустой результат для скана без ошибок, поскольку в скане отсутствует текстовый слой.

На наших шести тестовых страницах по ценам на октябрь 2026 года стоимость составила около 0,77 долл. США за 1000 страниц с GPT-6 Luna и 1,00 долл. США с Claude Haiku 5.5. Модель, работающая через mistral-ocr, добавляет 2 долл. США за 1000 страниц за OCR. Проверяйте текущие цены и считывайте значение usage.cost из ваших первых запросов, так как плотные таблицы стоят дороже.

Выдумает ли модель строки, которых нет в PDF?

Мы не обнаружили вымышленных строк при 25 извлечениях из пяти наших тестовых PDF-файлов, но это не исключает их появления в других документах. Пропущенные строки представляют собой более серьезный риск, вызванный обрезкой вывода по лимиту токенов или парсером, не обнаружившим текст. Сравните количество строк или промежуточный итог с документом.

Для цифровых PDF с простыми заголовками библиотека pdfplumber бесплатна и нашла каждую таблицу в наших тестовых цифровых файлах. Она возвращает каждую ячейку в виде текста, разбивает многоуровневые заголовки на отдельные строки и ничего не нашла в наших сканах. Camelot и Tabula — другие бесплатные варианты, но мы не тестировали их в рамках этого руководства.

Следующие шаги

  • В разделе «Входные данные PDF» рассматривается отправка PDF-файлов по URL-адресу вместо base64, движки парсеров и их цены, а также аннотации файлов, которые позволяют отправлять результаты парсинга обратно, чтобы не платить за повторный парсинг одного и того же PDF-файла.

В разделе «Входные данные PDF» рассматривается отправка PDF-файлов по URL-адресу вместо base64, движки парсеров и их цены, а также аннотации файлов, которые позволяют отправлять результаты парсинга обратно, чтобы не платить за повторный парсинг одного и того же PDF-файла.

  • В разделе «Структурированные выводы» объясняются строгие схемы JSON и способы маршрутизации запросов только к тем провайдерам, которые их поддерживают.

В разделе «Структурированные выводы» объясняются строгие схемы JSON и способы маршрутизации запросов только к тем провайдерам, которые их поддерживают.

  • Пресеты сохраняют модель, системный промпт и настройки провайдера под одним именем. Поместите инструкцию по извлечению в системный промпт пресета и вызывайте @preset/your-name из каждого сервиса, чтобы все они использовали одинаковую конфигурацию. Не забывайте отправлять схему в поле response_format с каждым запросом, поскольку в документации по пресетам она не указана среди параметров, сохраняемых в пресете. В нашем руководстве по пресетам описан процесс настройки.

Пресеты сохраняют модель, системный промпт и настройки провайдера под одним именем. Поместите инструкцию по извлечению в системный промпт пресета и вызывайте @preset/your-name из каждого сервиса, чтобы все они использовали одинаковую конфигурацию. Не забывайте отправлять схему в поле response_format с каждым запросом, поскольку в документации по пресетам она не указана среди параметров, сохраняемых в пресете. В нашем руководстве по пресетам описан процесс настройки.

  • Для обработки накопившихся PDF-файлов пакетный API (Batch API) выполняет те же запросы асинхронно, обычно в течение 24 часов. Цены на anthropic/claude-haiku-5.5:batch и openai/gpt-6-luna:batch составляют половину от стандартной цены за токен. Пакетные запросы принимают PDF-файлы в виде публичных URL-адресов, а не в кодировке base64, поэтому сначала разместите файлы на хостинге. URL-адреса файлов работают только с моделями, в которых указан ввод файлов, поэтому маршрут DeepSeek с mistral-ocr из шага 3 недоступен в пакетном режиме, а в моделях OpenAI они работают только через Responses API. Мы не измеряли показатели пакетных запусков для этого руководства.

Для накопившихся PDF-файлов Batch API выполняет те же запросы асинхронно, как правило, в течение 24 часов. Модели anthropic/claude-haiku-5.5:batch и openai/gpt-6-luna:batch стоят вдвое дешевле стандартной цены за токен. Пакетные запросы принимают PDF-файлы в виде публичных URL-адресов, а не в формате base64, поэтому сначала разместите файлы на хостинге. URL-адреса файлов работают только с моделями, поддерживающими ввод файлов, поэтому маршрут DeepSeek с mistral-ocr из шага 3 недоступен в пакетном режиме, а в моделях OpenAI они работают только через Responses API. Мы не измеряли время выполнения пакетных запросов для этого руководства.

  • Если вы создаете это в рамках LangChain, ChatOpenRouter передает PDF-входы и поддерживает строгие схемы JSON с помощью with_structured_output.

Если вы создаете это в рамках LangChain, ChatOpenRouter передает PDF-входы и поддерживает строгие схемы JSON с помощью with_structured_output.

  • Если ваши исходные документы представляют собой изображения, а не PDF, в статье «Как отправить изображение в LLM» рассматривается работа с содержимым изображений.

Если ваши исходные документы представляют собой изображения, а не PDF, в статье «Как отправить изображение в LLM» рассматривается работа с содержимым изображений.

О чём эта статья

Ещё в разделе «AI и машинное обучение»

Все →

Ещё от OpenRouter