Apryse文档提取JS开发包

金融、法律和医疗保健领域的专业人士都知道,他们要从无尽的银行对账单、法律合同、医疗记录等数据中,努力从大量非结构化数据中提取出有意义的信息,这是多么艰辛。

幸运的是,我们生活在一个大型语言模型 (LLM) 比以往任何时候都更容易获得的时代,如今,构建一个由人工智能驱动的应用程序相对简单,它可以识别模式、趋势和联系,帮助(而不是取代)人类团队做出数据驱动的决策。

问题是什么?这只有在你能够可靠地从非结构化 PDF 中提取结构化数据的情况下才有可能。

1、瓶颈:简单的文本提取是不够的

虽然我们当然可以尝试使用几个基于 PDF.js 的库中的任何一个从 PDF 中提取原始文本,但我们永远无法以表格格式保存数据——这在处理银行对账单、财务报告、保险单据等时是一个大问题。

核心问题是 PDF 从未被设计为数据存储库。相反,它们旨在实现一致的显示,旨在保留文档的外观(字体、文本、光栅/矢量图像、表格、表单等),适用于各种设备和操作系统,而不是被束缚在你可能习惯使用的结构化数据模型(如 JSON 或 XML)上。

由于缺乏固有模式,数据提取变成了一项棘手的任务,因为 PDF 中的内容不是按逻辑组织,而是按视觉组织。

基于规则的模板可以部分解决此问题,但这种解决方案很脆弱,一旦规模扩大就会失效。例如,每家银行都有自己的报表格式,即使是微小的设计调整也会导致基于模板的提取失败。手动为每家银行创建和维护模板是不切实际的,毕竟模板从定义上来说就是静态的,需要不断维护,这是一个糟糕的长期选择。

如果没有正确完成这一关键的第一步,那么一堵无法保留上下文的线性、无格式的文本墙就永远不会可靠地或大规模地为您提供非确定性 LLM 的结果。我永远不会将业务押注于此。

2、还有更好的吗?

一段时间以来,我一直在寻找一种替代方案,可以将强大的文档处理功能与灵活、可扩展的 API(因此不是独立的桌面应用程序)结合起来,无缝集成到我们的开发流程中,而无需强制定制或持续的手动监督。而且由于安全性至关重要,我需要它能够部署在我们自己的基础设施上。

Apryse 满足了所有要求。

Apryse 是一款用于文档管理的一体化原生工具包,它提供了用于 Web、移动、客户端和服务器使用的库,涵盖 PDF 查看、注释、编辑、创建、生成,以及与我的需求最相关的:通过其服务器 SDK 提取数据,以 JSON、XML 甚至 XLSX 格式提供数据。

借助 Apryse,我终于可以将注意力从繁琐的工作(数据提取、模板维护)转移到构建能够大规模推动价值的分析。它是高容量、数据驱动操作的可靠支柱。

3、智能数据处理

它与众不同之处在于:底层有一个复杂的神经网络,使用深度学习模型从 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 在我们洞察管道的下一阶段进行提取。

让我们看看它是如何工作的。

4、构建我们的管道

首先,确保你正在运行 Node.js 18+,并初始化一个新项目。

我们将安装核心 Apryse 库及其数据提取模块(其中包含我们讨论过的神经网络)。我们还将获取 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 密钥。

  • 如果需要,请获取你正在使用的 LLM 的密钥。对于 OpenAI,你可以在这里找到它。
  • 要获得免费的 Apryse API 密钥,请在此处登录以显示它。

将它们放在项目文件夹中的.env 文件中。这是我的密钥。

OPENAI_API_KEY = openai_api_key_here
APRYSE_API_KEY = apryse_trial_api_key_here

4.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 选项对象中设置密码,如下所示:

/* if password protected */
const options = new PDFNet.DataExtractionModule.DataExtractionOptions();
options.setPDFPassword("password")

const json = await PDFNet.DataExtractionModule.extractDataAsString('./bank-statement.pdf', 
PDFNet.DataExtractionModule.DataExtractionEngine.e_Tabular, options);

// rest of code

为了确保 Apryse SDK 在进程运行完成后清理所有内存对象,你应该运行使用 PDFNet.runWithCleanup() 初始化的 main(),这使得我们在提取阶段结束时的代码如下所示:

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();
    });

再次确保你的 Apryse API 密钥已在.env 文件中设置,并作为第二个参数传递给此处的 runWithCleanup 函数。

运行此脚本时,它应该会打印出我们之前讨论过的提取的、深度结构化的 JSON。

下一步是将提取的 JSON 传递给 LLM,并设计一个提示来从中提取见解。

4.2 让你的 LLM 摄取结构化输出以生成见解

首先集成 Vercel AI SDK 来处理对你的 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"),
            /* structured inputs to safeguard against prompt injection */
            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; // return null if there’s an error
    }
}

如你所见,这里的 Vercel AI SDK 可以非常轻松地替换所需的模型。除了模型属性的值之外,其他所有内容都将保持不变。

将所有内容放在一起并稍微清理一下代码,这就是我们所得到的:

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`; // remember to use structured inputs to safeguard against prompt injection

async function analyze(input) {
  try {
    const { text } = await generateText({
      model: openai("gpt-4o"),
      prompt: `${prompt}\n{"text_to_analyze": ${input}}`, // structured 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...");

  /* if password protected */
  // const options = new PDFNet.DataExtractionModule.DataExtractionOptions();
  // options.setPDFPassword("password")

  const json = await PDFNet.DataExtractionModule.extractDataAsString(
    "./bank-statement.pdf",
    PDFNet.DataExtractionModule.DataExtractionEngine.e_Tabular,
  ); // include options if you're using it
  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) {
      // Save insights to a text file
      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.

//...(more)

最后,这超出了本教程的范围,但如果我不提其他内容,那我就太失职了:银行对账单通常是一个足够小的 PDF,Apryse 提取的结构化数据不太可能超出您的 LLM 使用量或上下文限制。例如,OpenAI 的免费套餐具有 8K 令牌上下文限制,应该可以轻松处理大多数银行对账单。但如果你需要处理更大的 PDF,例如:

  • 年度财务报告
  • 多页法律合同
  • 或综合医疗记录

那么生成的 JSON 输出很容易超出许多 LLM 的令牌限制,特别是如果它包含大量布局元数据。对于这些,请考虑使用 RAG(检索增强生成)方法将内容分解为更小、更相关的块。这样,你可以索引这些块并仅检索与每个查询最相关的部分,从而降低令牌成本并保持在模型的上下文窗口内。

5、经验教训

PDF 数据提取是一种掷骰子游戏——通常涉及拼凑多个库并应对不一致的输出。基于深度学习的 Apryse 是一个令人耳目一新的简单解决方案。

SDK 在将 PDF 转换为 JSON 时保持结构保真度的能力——保留从表关系到空间布局的所有内容——为 LLM 驱动的分析提供了完美的基础。不再需要手动解析,不再需要正则表达式操作,也不再需要猜测文档层次结构。

开发人员的体验也非常出色。清晰的文档可以回答问题并让你深入了解 API 以找出你可能拥有的潜在用例,大量示例代码实际上反映了现实世界的用例,当然,它的速度很快。

我曾用它来处理银行对账单,但无论你是构建财务分析工具、文档处理管道,还是任何需要从 PDF 中提取有意义数据的应用程序,Apryse 都值得认真考虑。它将通常痛苦的开发过程变成了简单的实现,让你可以专注于构建功能,而不是与文档解析作斗争。


原文链接:How to Generate Insights from PDF Files with Apryse and GPT

汇智网翻译整理,转载请标明出处