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






4.87/5 (68投票s)
2004 年 5 月 16 日
4分钟阅读

865833

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.h 和 zlib.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;
}
}
}
}