65.9K
CodeProject 正在变化。 阅读更多。
Home

从 PDF 文件提取纯文本的代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (68投票s)

2004 年 5 月 16 日

4分钟阅读

viewsIcon

865833

downloadIcon

36772

显示如何解压缩和提取 PDF 文档中文本的源代码。

引言

PDF 文档被广泛使用,并且其内容通常是压缩的。本文介绍了一个简单的 C 代码,可用于从 PDF 文件中提取纯文本。

为什么?

Adobe 允许您提交 PDF 文件,然后会提取文本或 HTML 并通过电子邮件发送给您。但有时您需要自己提取文本,或者在应用程序内部提取。您可能还想应用特殊格式(例如,添加制表符),以便文本可以轻松导入 Excel(例如,当您的 PDF 文档主要包含需要移植到 Excel 的表格时,此代码就是这样开发的)。

“The Code Project”上有几个关于如何创建 PDF 文档的项目,但没有一个提供免费的代码来显示如何提取文本而不使用商业库。在读者评论中,有人表达了对此处提供的类似代码的需求。

市面上有许多用于读取或创建 PDF 文件的库,但您必须注册它们才能商用,或者签署各种协议。此处提供的代码非常简单基础,但完全免费。它仅使用 ZLIB 库,该库也是免费的。

基础

您可以从 这里 下载 PDFReference15_v5.pdf 等文档,其中解释了 PDF 文件的一些内部结构。简而言之,每个 PDF 文件包含一定数量的对象。每个对象可能需要一个或多个过滤器来解压缩它,并且可能还提供一个数据流。文本流通常使用 FlateDecode 过滤器进行压缩,可以使用 ZLIB (http://www.zlib.org/) 库中的代码进行解压缩。

每个对象的数据可以在“stream”和“endstream”部分之间找到。解压后,需要处理数据以提取文本。数据通常包含一个或多个文本对象(以 BT 开头,以 ET 结尾),其中包含格式指令。通过逐步调试此应用程序,您可以从 PDF 文件的结构中学到很多东西。

关于代码

这个单一的源代码文件包含非常简单、非常基础的 C 代码。它最初将整个 PDF 文件读入一个缓冲区,然后反复搜索“stream”和“endstream”部分。它不检查应该应用哪个过滤器,并且始终假定为 FlateDecode。(如果它弄错了,通常不会为该文件部分生成任何输出,所以问题不大)。一旦数据流被解压(未压缩),就会对其进行处理。在处理过程中,代码会搜索表示文本对象的 BT 和 ET 标记。每个对象的内容都会被处理以提取文本,并猜测是否需要制表符或换行符。

该代码远非完整或任何类型的通用实用程序类,但它确实演示了您如何自己提取文本。它足以向您展示方法并让您开始。

然而,代码是完全功能的,因此当应用于 PDF 文档时,它通常能很好地提取文本。它已经在几个 PDF 文件上进行了测试。

本代码按原样提供,不提供任何保证。请自行承担风险。

使用代码

下载内容包含一个 C 文件。要使用它,请创建一个简单的 Windows 32 控制台项目,并将 pdf.c 文件添加到项目中。您还需要 这里(感谢他们!)下载免费的“zlib compiled DLL”zip 文件。将 zdll.lib 提取到您的项目目录中,并将其添加为项目依赖项(链接它)。还将 zlib1.dll 放在您的项目目录中。还将 zconf.hzlib.h 放在您的项目目录中,并将它们添加到项目中。

现在,逐步调试应用程序,并注意输入 PDF 和输出文本文件的名称在 main 方法的开头被硬编码了。

未来的增强

如果有足够的兴趣,作者可能会考虑上传一个带有 Windows 界面的发布版本。该代码非常适合从表格中提取数据,并将其以可直接导入 Excel 的形式输出,同时保留列(因为添加了制表符)。

代码片段

流部分最初是这样定位的

size_t streamstart = FindStringInBuffer (buffer, "stream", filelen);
size_t streamend = FindStringInBuffer (buffer, "endstream", filelen);

然后,一旦确定了数据部分,它就会像这样进行解压

z_stream zstrm; ZeroMemory(&zstrm, sizeof(zstrm));
zstrm.avail_in = streamend - streamstart + 1;
zstrm.avail_out = outsize;
zstrm.next_in = (Bytef*)(buffer + streamstart);
zstrm.next_out = (Bytef*)output;
int rsti = inflateInit(&zstrm);
if (rsti == Z_OK)
{
  int rst2 = inflate (&zstrm, Z_FINISH);
  if (rst2 >= 0)
  {
    //Ok, got something, extract the text:
    size_t totout = zstrm.total_out;
    ProcessOutput(fileo, output, totout);
  }
}

主要工作在 ProcessOutput 方法中完成,该方法处理未压缩的流以提取任何文本对象的文本部分。它看起来像这样

void ProcessOutput(FILE* file, char* output, size_t len)
{
  //Are we currently inside a text object?
  bool intextobject = false;
  //Is the next character literal 
  //(e.g. \\ to get a \ character or \( to get ( ):
  bool nextliteral = false;

  //() Bracket nesting level. Text appears inside ()
  int rbdepth = 0;

  //Keep previous chars to extract numbers etc.:
  char oc[oldchar];
  int j=0;
  for (j=0; j<oldchar; j++) oc[j]=' ';

  for (size_t i=0; i<len; i++)
  {
    char c = output[i];
    if (intextobject)
    {
      if (rbdepth==0 && seen2("TD", oc))
      {
        //Positioning.
        //See if a new line has to start or just a tab:
        float num = ExtractNumber(oc,oldchar-5);
        if (num>1.0)
        {
          fputc(0x0d, file);
          fputc(0x0a, file);
        }
        if (num<1.0)
        {
          fputc('\t', file);
        }
      }
      if (rbdepth==0 && seen2("ET", oc))
      {
        //End of a text object, also go to a new line.
        intextobject = false;
        fputc(0x0d, file);
        fputc(0x0a, file);
      }
      else if (c=='(' && rbdepth==0 && !nextliteral) 
      {
        //Start outputting text!
        rbdepth=1;
        //See if a space or tab (>1000) is called for by looking
        //at the number in front of (
        int num = ExtractNumber(oc,oldchar-1);
        if (num>0)
        {
          if (num>1000.0)
          {
            fputc('\t', file);
          }
          else if (num>100.0)
          {
            fputc(' ', file);
          }
        }
      }
      else if (c==')' && rbdepth==1 && !nextliteral) 
      {
        //Stop outputting text
        rbdepth=0;
      }
      else if (rbdepth==1) 
      {
        //Just a normal text character:
        if (c=='\\' && !nextliteral)
        {
          //Only print out next character 
          //no matter what. Do not interpret.
          nextliteral = true;
        }
        else
        {
          nextliteral = false;
          if ( ((c>=' ') && (c<='~')) || ((c>=128) && (c<255)) )
          {
            fputc(c, file);
          }
        }
      }
    }
    //Store the recent characters for 
    //when we have to go back for a number:
    for (j=0; j<oldchar-1; j++) oc[j]=oc[j+1];
      oc[oldchar-1]=c;
    if (!intextobject)
    {
      if (seen2("BT", oc))
      {
        //Start of a text object:
        intextobject = true;
      }
    }
  }
}
© . All rights reserved.