Генерация аналитических данных из PDF-файлов с помощью Apryse и GPT
Специалисты в области финансов, юриспруденции и здравоохранения знают, как тяжело работать с бесконечными банковскими выписками, юридическими контрактами, медицинскими картами и т. д., пытаясь извлечь что-то значимое из огромного количества неструктурированных данных.
К счастью, мы живем в эпоху, когда большие языковые модели (LLM) стали более доступными, чем когда-либо, и сегодня относительно просто создать приложение на базе ИИ, которое сможет выявлять закономерности, тенденции и связи. Таким образом эти технологии становятся помощниками (а не заменой) для команд специалистов в деле принятия решений, основанных на данных.
При каких условиях достигаются такие результаты? Это возможно, только когда мы научимся эффективно извлекать структурированные данные из неструктурированных PDF-файлов.
Узкое место: простого извлечения текста недостаточно
Хотя мы и можем попытаться использовать любую из библиотек на основе PDF.js для извлечения текста из PDF-файлов, у нас не получится сохранить данные в табличном формате, а это очень важно при обработке банковских выписок, финансовых отчетов, страховых документов и т. д.
Основная проблема заключается в том, что PDF-файлы не создавались как хранилища данных. Скорее, они предназначались для последовательного отображения контента, чтобы сохранить внешний вид документов — шрифт, текст, растровые/векторные изображения, таблицы, формы и все остальное — на разных устройствах и операционных системах. Эти файлы не привязаны к структурированной модели данных, с которой вы привыкли работать, например JSON или XML.
Отсутствие встроенной схемы превращает извлечение данных в сложную задачу, поскольку содержимое PDF-файлов организовано не логически, а визуально.
Шаблоны, основанные на правилах, могут частично решить эту проблему, но это ненадежное решение, которое не справляется с масштабированием. Приведем пример. У каждого банка свой формат выписки, и даже небольшие изменения в дизайне могут привести к сбоям в извлечении данных на основе шаблонов. Вручную создавать и поддерживать шаблоны для каждого банка непрактично. В конце концов, шаблон статичен по определению и требует постоянной поддержки, так что в долгосрочной перспективе такой выбор может сослужить плохую службу.
А без правильного выполнения этого важнейшего первого шага объем линейного, неформатированного текста, не сохраняющего контекст, никогда не даст вам надежных результатов при использовании недетерминированной LLM или в условиях масштабирования.
Есть ли варианты получше?
В течение некоторого времени я искал альтернативу, которая могла бы сочетать мощную обработку документов с гибким, масштабируемым API (речь не идет об отдельном приложении для десктопа), которая легко интегрировалась бы в наш конвейер разработки без головной боли, связанной с обязательной настройкой или постоянным ручным контролем. А поскольку безопасность имеет первостепенное значение, требовалось, чтобы это решение можно было развернуть в нашей инфраструктуре.
По всем параметрам мне подошел Apryse.

Apryse — это универсальный нативный набор инструментов для управления документами с коммерческой лицензией. Он предоставляет библиотеки для веб-, мобильных, клиентских и серверных приложений, которые позволяют просматривать, аннотировать, редактировать, создавать, генерировать и, что наиболее важно для моих нужд, извлекать данные с помощью серверного SDK, предоставляя их в форматах JSON, XML или даже XLSX.
С Apryse я наконец-то могу переключить внимание с монотонной работы (извлечение данных, поддержка шаблонов) на создание аналитики, которая является источником ценности при масштабировании. Это надежная основа для работы с большими объемами данных.
Интеллектуальная обработка данных
Особенностью этой библиотеки является сложная нейронная сеть «под капотом», использующая модели глубокого обучения для интеллектуального извлечения структурированных данных из PDF-файлов. По сути, библиотека Apryse использует конвейер этих моделей, которые «научились» распознавать, как выглядят табличные данные в PDF (сетки, столбцы, строки), как они расположены по отношению друг к другу и чем отличаются от абзацев текста, растровых/векторных изображений и так далее.
Инициализируйте библиотеку, подайте ей на вход PDF-файл, и она выдаст разобранные и структурированные данные так, как они расположены на странице. Она определит таблицы, верхние и нижние колонтитулы, строки и столбцы, извлечет абзацы/текстовое содержимое вместе с порядком чтения и позиционными данными (координаты ограничительных рамок и базовые линии).
// output.json { "pages": [ { "elements": [ { "columnWidths": [138, 154], "groupId": 0, "rect": [70, 697, 362, 524], "trs": [ { "rect": [70, 697, 362, 675], "tds": [ { "colStart": 0, "contents": [ { "baseline": 674.4262771606445, "rect": [ 71.999997, 698.7012722546347, 190.82616323890954, 665.7694694275744 ], "text": "ABC Bank", "type": "textLine" } ], "rect": [70, 697, 208, 675], "rowStart": 0, "type": "td" } ], "type": "tr" } // more rows here ], "type": "table" } // more elements here ] } // more pages here ] }
На выходе вы получаете высокоструктурированный вывод, позволяющий вам в дальнейшем анализировать или переформатировать данные для получения дополнительных сведений. Это идеальный вариант для передачи LLM на следующем этапе работы нашего конвейера получения информации.
Посмотрим, как это работает.
Создание конвейера
Предварительные условия
Прежде всего убедитесь, что используете Node.js 18+, и инициируйте новый проект.
Установим основную библиотеку Apryse и ее модуль Data Extraction (который содержит нейронную сеть, о которой мы говорили). Также нам понадобится dotenv для переменных среды.
Для установки зависимостей можете использовать выбранный вами менеджер пакетов. Здесь мы остановим выбор на NPM, поскольку большинство пользователей Node.js используют его по умолчанию.
npm install @pdftron/pdfnet-node npm install @pdftron/data-extraction npm install dotenv
Я воспользуюсь LLM от OpenAI, так как у меня есть подписка. Но чтобы этот гайд был как можно более доступным и все читающие его могли следовать за моими действиями, будем использовать Vercel AI SDK, который представляет собой унифицированный интерфейс, позволяющий применять (и легко менять местами) OpenAI, Anthropic, Gemini и все остальные модели, к которым у вас есть доступ, включая пользовательские.
npm install ai @ai-sdk/openai
И наконец, ключи API.
- Получите ключ API для используемой вами LLM, если в этом есть необходимость. Для OpenAI вы найдете ключ здесь.
- Для получения бесплатного API-ключа Apryse войдите сюда.
Поместите их в файл .env
в папке вашего проекта. Вот как выглядит мой файл .env
.
OPENAI_API_KEY = openai_api_key_here
APRYSE_API_KEY = apryse_trial_api_key_here
Пробная версия подойдет для неограниченного непроизводственного использования, если вас устраивает водяной знак на всех созданных PDF-файлах.
Шаг 1. Извлечение
Точка входа для скрипта очень проста. Вы импортируете библиотеку, используете addResourceSearchPath
для указания на дополнение для извлечения данных (которое технически является внешним ресурсом) и ожидаете извлечения табличных данных из входного PDF (bank-statement.pdf
в том же каталоге, здесь) в виде строки JSON.
require("dotenv").config();
const { PDFNet } = require('@pdftron/pdfnet-node')
async function main() {
await PDFNet.addResourceSearchPath("./node_modules/@pdftron/data-extraction/lib")
try {
const json = await PDFNet.DataExtractionModule.extractDataAsString('./bank-statement.pdf',
PDFNet.DataExtractionModule.DataExtractionEngine.e_Tabular);
console.log('-----Extracted Text------');
console.log(json);
} catch (error) {
console.error("Error :", error);
}
}
Если PDF защищен паролем, просто задайте пароль в объекте опций DataExtractionOptions
, как показано ниже:
/* если файл защищен паролем */
const options = new PDFNet.DataExtractionModule.DataExtractionOptions();
options.setPDFPassword("password")
const json = await PDFNet.DataExtractionModule.extractDataAsString('./bank-statement.pdf',
PDFNet.DataExtractionModule.DataExtractionEngine.e_Tabular, options);
// остальная часть кода
Чтобы убедиться в том, что SDK Apryse очищает все объекты в памяти после завершения процесса, необходимо запустить main()
(инициализируется с помощью PDFNet.runWithCleanup()
), в результате чего код в конце этапа извлечения будет выглядеть следующим образом:
require("dotenv").config();
const { PDFNet } = require('@pdftron/pdfnet-node')
async function main() {
await PDFNet.addResourceSearchPath("./node_modules/@pdftron/data-extraction/lib")
try {
const json = await PDFNet.DataExtractionModule.extractDataAsString('./bank-statement.pdf',
PDFNet.DataExtractionModule.DataExtractionEngine.e_Tabular);
console.log('-----Extracted Text------');
console.log(json);
} catch (error) {
console.error("Error :", error);
}
}
PDFNet.runWithCleanup(main, process.env.APRYSE_API_KEY)
.catch(error => console.error("Apryse library failed to initialize:", error))
.then(function () {
PDFNet.shutdown();
});
Опять же, удостоверьтесь, что ваш ключ API Apryse установлен в файле .env
и передан в качестве второго аргумента функции runWithCleanup
.
Когда вы запустите этот скрипт, он должен вывести извлеченный высокоструктурированный JSON, о котором мы говорили ранее.
Следующим шагом будет передача этого извлеченного JSON в LLM с промптом, предназначенным для извлечения информации.
Шаг 2. Поглощение LLM структурированного вывода для получения аналитических данных
Начнем с интеграции SDK Vercel AI для обработки запросов к провайдеру LLM. Напишем простую функцию, которая принимает данные в формате JSON из предыдущего шага и отправляет их в LLM с определенным промптом, который мы разработаем для получения полезной информации.
В нашем сценарии мы анализируем банковскую выписку для внутреннего использования (то есть нам нужна стратегическая и финансовая информация для заинтересованных сторон). Ниже видим пример промпта, который кажется верным.
const prompt = `Analyze the following text, and generate a bullet-point list
of actionable financial and strategic insights for internal business use.`
Почти готово. Но это еще не все. LLM интерпретируют промпты как текст, без четкого разделения между инструкциями пользователя и командами, скрытыми в основном тексте. Представьте себе вредоносный текст в PDF-файле, который ловко искажает ваш промпт, делая акцент на определенных данных. Это может привести к тому, что ваша компания примет необъективные или даже пагубные решения. Не самый верный путь.
Если бы речь шла о традиционном приложении, мы бы просто обрабатывали пользовательские данные с помощью строгой очистки/валидации. Однако промпты для LLM сложнее защитить из-за отсутствия формальной структуры составления кода.
Тем не менее мы можем предпринять некоторые меры предосторожности. Поскольку у нас уже есть готовый структурированный JSON в качестве входных данных, мы просто поместим его в дополнительное поле JSON и сообщим LLM, чтобы она не обрабатывала ничего, что не входит в эту конкретную пару «ключ-значение».
const prompt = `Analyze the transaction data provided in JSON format
under the 'text_to_analyze' field, and nothing else. Each transaction is structured with details
like transaction description, amount, date, and other metadata.
Generate a bullet-point list of actionable financial and strategic insights
for internal business use. Focus on identifying cash flow patterns,
high-spend categories, recurring payments, large or unusual transactions,
and any debt obligations. Provide insights into areas for cost savings,
credit risk, operational efficiency, and potential financial risks.
Each insight should suggest actions or strategic considerations to
improve cash flow stability, optimize resource allocation, or flag
potential financial risks. Expand technical terms as needed to clarify
for business stakeholders.`
Недетерминированность LLM означает, что вы можете бесконечно долго настраивать этот промпт под свои нужды, но приведенный нами способ должен подойти в качестве базового решения.
После этого можем импортировать необходимые библиотеки и просто передавать LLM нужные данные, как показано ниже.
const { openai } = require('@ai-sdk/openai');
const { generateText } = require('ai')
async function analyze(input) {
try {
const { text } = await generateText({
model: openai("gpt-4o"),
/* структурированные входы для защиты от внедрения промптов */
prompt: `${prompt}\n{"text_to_analyze": ${input}}`
});
if (!text) {
throw new Error("No response text received from the generateText function.");
}
return text;
} catch (error) {
console.error("Error in analyze function:", error);
return null; // возвратить null, если произошла ошибка
}
}
Как видите, в SDK Vercel AI очень легко поменять модель на ту, которая нужна вам. Все остальное остается неизменным, кроме значения свойства model.
Соберем все вместе и немного почистим код. Вот что у нас получилось:
const { PDFNet } = require("@pdftron/pdfnet-node");
const { openai } = require("@ai-sdk/openai");
const { generateText } = require("ai");
const fs = require("fs");
require("dotenv").config();
const prompt = `YOUR_PROMPT_HERE`; // не забывайте использовать структурированные входы, чтобы защититься от внедрения промптов
async function analyze(input) {
try {
const { text } = await generateText({
model: openai("gpt-4o"),
prompt: `${prompt}\n{"text_to_analyze": ${input}}`, // структурированный вход
});
if (!text) {
throw new Error(
"No response text received from the generateText function.",
);
}
return text;
} catch (error) {
console.error("Error in analyze function:", error);
return null;
}
}
async function extractDataFromPDF() {
console.log("Extracting tabular data as a JSON string...");
/* если файл защищен паролем */
// const options = new PDFNet.DataExtractionModule.DataExtractionOptions();
// options.setPDFPassword("password")
const json = await PDFNet.DataExtractionModule.extractDataAsString(
"./bank-statement.pdf",
PDFNet.DataExtractionModule.DataExtractionEngine.e_Tabular,
); // включите опции при условии использования
fs.writeFileSync("./output.json", json);
console.log("Result saved ");
return json;
}
async function main() {
await PDFNet.addResourceSearchPath(
"./node_modules/@pdftron/data-extraction/lib",
);
try {
const extractedData = await extractDataFromPDF();
const insights = await analyze(extractedData);
if (insights) {
// Сохраните полученную информацию в текстовый файл
fs.writeFileSync("insights.md", insights, "utf8");
console.log("Insights saved to insights.md");
} else {
console.error("No insights generated. Skipping file write.");
}
} catch (error) {
console.error("Error :", error);
}
}
PDFNet.runWithCleanup(main, process.env.APRYSE_API_KEY)
.catch((error) =>
console.error("Apryse library failed to initialize:", error),
)
.then(function () {
PDFNet.shutdown();
});
Для наглядности я решил записывать выходные данные на обоих этапах (извлечение из PDF и информация, генерируемая LLM) в файлы на диске. Если что-то пойдет не так, надеюсь, это поможет определить причину и при необходимости внести необходимые коррективы.
Ваши выводы будут похожи на те, что показаны ниже. Выглядит довольно аккуратно!
// excerpt-of-insights.md
//...
### Significant or Unusual Transactions
1. **Equipment Purchase**: For the **$9,000.00** expense for computers on April 21, 2023,
I suggest verifying that the hardware specifications meet both current
operational demands and potential future needs to prevent frequent upgrades.
I also suggest considering bulk purchasing discounts or bundled warranties to
lower long-term maintenance costs. Additionally, consider leasing instead
of purchasing outright if you want to bring down upfront costs.
2. **Business Travel Expenses**: You spent **$36,500** on travel for the month
of April. This is a significant expense on travel, and I suggest double
checking to make sure the numbers are accurate, and shows clear alignment
between travel goals and expected returns. I also suggest a travel policy
that defines allowable expenses, reimbursement guidelines, and cost-saving
practices (e.g., advance bookings, lodging caps, per diem).
Regularly review this policy, and promote virtual meetings whenever possible.
//...(и так далее)
Наконец, хоть это и выходит за рамки данного туториала, не могу не упомянуть еще один момент. Банковская выписка обычно представляет собой достаточно небольшой PDF-файл, поэтому структурированные данные, которые извлечет Apryse, вряд ли превысят лимиты использования LLM или контекстных токенов. К примеру, бесплатный уровень OpenAI имеет ограничение по токенам контекста в 8K, что позволяет удобно обрабатывать большинство банковских выписок. Другое дело, когда вам нужна обработка более крупных PDF-файлов, таких как:
- годовые финансовые отчеты;
- многостраничные юридические контракты;
- комплексные медицинские карты.
В этом случае генерируемый JSON-вывод может превысить лимиты токенов для многих LLM, особенно если он включает большое количество метаданных, связанных с расположением содержимого. Для таких случаев следует использовать подход RAG (Retrieval-Augmented Generation), чтобы разбить содержимое на более мелкие и релевантные фрагменты. Таким образом, вы сможете индексировать фрагменты и извлекать только те части, которые наиболее релевантны каждому запросу, сокращая траты токенов и оставаясь в пределах контекстного окна вашей модели.
Подведем итоги
Извлечение данных из PDF-файлов — сложная задача, часто связанная с объединением нескольких библиотек и борьбой с несогласованными результатами. Apryse, прошедший глубокое обучение, оказался на удивление простым решением.
Способность SDK сохранять структурную точность при преобразовании PDF-файлов в JSON — с сохранением всего, от связей между таблицами до пространственного расположения, — обеспечивает идеальную основу для анализа с использованием LLM. Больше не нужны ни ручной парсинг, ни работа с регулярными выражениями, ни догадки относительно иерархии документов.
Опыт разработчика также становится исключительно приятным. Здесь стоит упомянуть кристально чистую документацию, которая отвечает на вопросы и позволяет глубоко погрузиться в API, чтобы определить возможные варианты использования, множество примеров кода, отражающих реальные случаи использования, и, конечно же, скорость.
Я применил Apryse для работы с банковскими выписками. Но независимо от того, создаете ли вы инструменты финансового анализа, конвейеры обработки документов или любое другое приложение, которому необходимо извлекать значимые данные из PDF-файлов, Apryse заслуживает серьезного изучения. Он превращает обычно болезненный процесс разработки в простую реализацию, которая позволяет сосредоточиться на создании функций, а не на парсинге документов.
Читайте также:
- 20 уникальных сценариев использования GPT-4o
- Зачем нужен AutoGPT
- Как сделать интеллектуальное приложение вопросов и ответов базы знаний с GPT-3 и Ruby
Читайте нас в Telegram, VK и Дзен
Перевод статьи Prithwish Nath: How to Generate Insights from PDF Files with Apryse and GPT