☰
PDFLIB+TET实战:破解加密PDF与乱码文本提取难题
2026/10/17 2:56:12 网站建设 项目流程

简介:面向在 Visual Studio 2010 中调用 PDFLIB/TET 库读取 PDF 文件的 C/C++ 开发者,这份资源以 PDFTry 工程为完整实例,直接给出了可运行的示例代码与配套说明文档。压缩包共 49 个文件,大小约 11.54MB,类别包括 C++ 源文件、头文件、解决方案与项目配置、编译生成的动态链接库与静态库、可执行文件,以及 PDF 格式的技术说明和 TXT 文本样例;覆盖了从源码编辑、编译链接到运行调试的完整流程,便于在 VS2010 中还原和对照。示例工程覆盖 TET 集成配置、打开 PDF、初始化 TET、读取元数据、逐页提取文本、抽取图像等关键环节,源码中涉及 p_open_file、tet_init、tet_textpage 等接口,可以直观了解调用顺序;配套文档部分还补充了库目录设置、附加依赖项、错误码处理与许可注意事项。截至目前,已有 2558 人学习下载,适合刚开始接触 PDF 解析库、希望以完整工程为参照搭建 PDF 读取流程的开发者参考。

1. 用 PDFLIB 读 PDF:先搞清它和“免费开源库”根本不在一条赛道上

打算用 PDFLIB 读 PDF 的人,多半是接到“把这几百份合同里的编号、金额、日期批量抽出来”这种活。用 PyPDF2 试过几份常规文件没问题,一碰到加密合同、扫描附件,哪怕是显示正常的中文,复制出来全是错位字,就开始怀疑工具选错了。PDFLIB 不是免费库,但它的读取组件配合 TET 文本分析引擎,专门处理这类“免费库读不动”的 PDF。下面直接讲它的组件定位、安装验证、逐页提取参数,以及五个实际项目里必踩的坑,适合做生产级批量文本抽取的开发者参考。

2. PDFLIB 的读取能力边界:文本提取得靠 TET,别装错主角

2.1 PDFLIB 本体负责“生成”,TET 负责“读取”

很多人看到“PDFLIB 读取 PDF”,下载库之后满世界找 read_pdf 函数,结果发现它主要是为生成 PDF 设计的接口。这不是文档藏得深,而是 PDFLIB 产品家族本来就分两条线:PDFlib 本身管生成与转换,文本提取的核心引擎叫 TET(Text Extraction Toolkit)。在 PDFLIB 安装包里,TET 通常作为一个独立组件或可选扩展存在。

我第一次接手时也吃过这个亏:装完 pdflib 扩展,翻了半天文档才发现读取入口是 tet_open_document。后来我把这理解成“插件关系”:TET 是 PDFLIB 里负责分析已有 PDF 的模块,能否按页取文本、带不带坐标,都取决于 TET 而不是 PDFLIB 本体。如果你拿到的包只有生成接口,那大概率叫“PDFlib”,没有 TET 部分;真正要做读取,找带 TET 字样的包。

这里不是要劝退免费方案,而是提醒你先搞清楚手上工具的角色。PDFLIB 的商业版和评估版的许可机制同样适用于 TET,评估包可以跑小规模测试,生产环境需要正式授权。后面每一段代码都基于 TET 接口,核心调用形式在不同语言下是一套逻辑,换语言时重点看函数名大小写和参数数组的写法差异即可。

2.2 选型对比:PyPDF2、PDFium 与 PDFLIB+TET 的分水岭

选型时要把“提取质量”放在“能不能读”之前。很多免费库能打开 PDF,也能读出一堆文本,但结果和你在 PDF 阅读器里看到的内容对不上:有的缺空格,有的整个字符被拆开,有的中文全变成乱码。真正的分水岭在于对字体映射和字符坐标的还原能力。

方案复杂版面还原自定义字体乱码加密PDF扫描件坐标信息许可证
PyPDF2 / pdfminer一般常见乱码部分支持需OCR有限MIT
PDFium一般偶尔乱码支持需OCR少BSD
PDFBox中等看字体支持需OCR有Apache
PDFLIB+TET高多数能还原支持仍需OCR完整商业

这张表不是贬低开源库,它们在轻量场景下完全够用。但从生产稳定性看,PDFLIB+TET 的价值在于对非标准字体的处理:当 PDF 缺少 ToUnicode 映射时,TET 会尝试按字形宽度和编码规则反推字符,这一手是免费库普遍欠缺的。坐标信息在办公自动化里也很有用,比如按段落位置抽取合同条款,没有坐标就只能按文本顺序硬切,漏掉关键内容的概率很高。

2.3 安装后第一步:先验证 License,再碰文档

装包过程不同语言差异很大,但第一段验证代码保持一致。不管是 PHP 扩展、Python 封装还是 JNI,最终都会创建一个 TET 句柄,然后看版本和授权状态。PHP 环境下常见做法是下载编译好的扩展并放到扩展目录,再通过 php.ini 加载。

<?php // 1. 检查扩展是否加载 if (!extension_loaded('pdflib')) { echo "pdflib 扩展未加载\n"; exit(1); } // 2. 创建 TET 句柄 $tet = tet_new(); // 3. 设置 license 文件路径,评估版也需要这一步 $license_result = tet_set_option($tet, "license=/path/to/license.txt"); echo "license 设置结果: " . $license_result . "\n"; // 4. 输出版本号方便核对 echo "TET 版本: " . tet_get_major_version($tet) . "." . tet_get_minor_version($tet) . "\n"; // 5. 释放句柄 tet_delete($tet); ?>

这段代码里几个函数的作用:tet_new 创建文本提取会话,所有后续操作都挂在句柄上;tet_set_option 用来传许可证路径,同时也可以配置字体目录、编码等;tet_get_major_version 和 tet_get_minor_version 返回组件版本号,用来核对安装包是否真的带 TET 部分。如果输出结果里 TET 版本为空,基本可以断定扩展加载有误,或下载的包不包含 TET 组件。具体函数命名以你下载包的 examples 目录为准,不同语言前缀可能从 tet 变成 TET。

3. 用 TET 把 PDF 文本读出来:完整流程与参数拆解

3.1 打开文档、读页数、构造逐页循环

有了授权,下一步就是完成“打开文档-读取文本-关闭文档”的闭环。TET 打开文档时会返回一个文档句柄,如果文件不存在、无法解析、被加密或授权不匹配,返回的句柄值是无效的,所以要养成先判断句柄再继续的习惯。

<?php $tet = tet_new(); tet_set_option($tet, "license=/path/to/license.txt"); // 打开文档,第三个参数为空表示不传候选参数 $doc = tet_open_document($tet, "/path/to/contract.pdf", ""); if ($doc == -1) { // 读取错误码与描述,第5章会有更细的排查 $errnum = tet_get_errnum($tet); $errmsg = tet_get_errmsg($tet); echo "打开失败,错误码: $errnum,信息: $errmsg\n"; tet_delete($tet); exit(1); } // 用 PCOS 属性接口获取页数,属性查询比字符串解析稳定 $pages = tet_pcos_get_number($tet, $doc, "length:pages"); echo "文档共 $pages 页\n"; // 逐页读取,页码从 1 开始 for ($pno = 1; $pno <= $pages; $pno++) { $page = tet_open_page($tet, $doc, $pno, "granularity=word"); if ($page == -1) { continue; } // 先测试读取,下一节统一处理文本内容 $text = tet_get_text($tet, $page, "encoding=utf8"); echo "第 $pno 页文本长度: " . strlen($text) . "\n"; tet_close_page($tet, $page); } tet_close_document($tet, $doc); tet_delete($tet); ?>

最值得先理解的是 tet_open_page 的第四个参数 granularity=word,它把文本粒度按单词划分。TET 也可以按 character 或 line 划分,word 粒度最适合大部分提取需求,因为它保留语义边界,同时避免把分散字符拼成一长串。granularity=character 适合做排版还原或笔画级分析,行粒度则更贴近阅读顺序。选错粒度不会报错,但后续做正则匹配、信息抽取时,结果形态差别很大。

tet_pcos_get_number 返回文档属性数值时,键名写成 length:pages 是有原因的:TET 把文档的各项物理属性通过 PCOS 协议暴露出来,length:pages 表示页数量。类似的还有 width:pages、length:outlines 等。这个命名方式和普通数组不同,不能在 PHP 里直接拿 pages 当键名,写错了会得到 -1。

3.2 提取文本块并保留页面坐标

上面只演示了打印文本长度,实际业务里更常见的是按页提取完整内容,同时拿到每个文本块的位置。TET 在打开页面后,除了 tet_get_text,还提供 tet_get_text_origin_x 和 tet_get_text_origin_y 这类接口,用来返回当前文本块的坐标。

<?php // 在已打开的 $page 上提取文本,并跟随坐标信息 $page = tet_open_page($tet, $doc, $pno, "granularity=line"); $text = tet_get_text($tet, $page, "encoding=utf8"); if (strlen($text) > 0) { $x = tet_get_text_origin_x($tet, $page); $y = tet_get_text_origin_y($tet, $page); echo "行文本: $text, 坐标: ($x, $y)\n"; } tet_close_page($tet, $page); ?>

这里把 granularity 改成了 line,因为行粒度下 tet_get_text_origin_x 和 tet_get_text_origin_y 返回的是整行首个字符左下角的 PDF 坐标。PDF 坐标系的单位是点,原点在页面左下角,Y 轴向上。如果要换算成像素,需要知道页面尺寸并自行换算,TET 本身不做这个转换。

坐标信息在合同抽取里非常实用。条款标题往往比正文缩进更靠左,字体更大,结合行坐标和字宽,可以不用解析样式就把标题和正文分开。我把这个思路用在模拟项目里,成功把几百份合同里的“合同编号”那一行自动定位,准确率比纯文本正则高不少。

3.3 提取结果落盘:写 CSV 前的内存注意点

批量抽取时,如果把每一页的处理结果都挂在变量里攒到最后一次性写文件,几十页还好,上千页 PDF 会直接把内存吃满。更合理的做法是每页处理完立即写入,同时按文件名和页码组织输出。

<?php $output = fopen("/path/to/output.csv", "w"); for ($pno = 1; $pno <= $pages; $pno++) { $page = tet_open_page($tet, $doc, $pno, "granularity=text"); $text = tet_get_text($tet, $page, "encoding=utf8"); if ($text === false) { continue; } // 用 fputcsv 一行一行落盘,释放页面句柄 $row = [basename("/path/to/contract.pdf"), $pno, $text]; fputcsv($output, $row); fflush($output); tet_close_page($tet, $page); } fclose($output); ?>

fputcsv 自动处理换行和引号转义,比手动拼字符串安全。fflush 把缓冲区内容立刻刷到磁盘,防止进程异常中断时丢失已处理数据。页面句柄在关闭后才释放内存,所以 tet_close_page 一定要在循环里执行,不关闭的后果在第5章单独讨论。

这里额外注释一下 granularity=text,它会让 TET 返回整个页面的连续文本流,适合做全文检索和关键词抽取。如果你需要保留段落或表格结构,用 line 或 word 再自己拼接更靠谱。选择哪一种粒度,取决于你要的是“一段话”还是“一个块”。

4. 复杂 PDF 读取路径:加密、扫描件与乱码的常规解法

4.1 加密 PDF:密码可以传,但要分清权限类型

加密 PDF 在合同和票据里非常常见。TET 在 tet_open_document 的第三个参数里支持 password 选项,用它传入用户密码或所有者密码:

<?php $options = "password=user123"; $doc = tet_open_document($tet, "/path/to/encrypted.pdf", $options); if ($doc == -1) { $errmsg = tet_get_errmsg($tet); echo "打开失败: $errmsg\n"; } ?>

注意这里有一个很隐蔽的差异:PDF 加密有两个密码,用户密码(user password)决定能否打开文档,所有者密码(owner password)决定能否做打印、复制、修改等操作。TET 提取文本属于内容访问,优先尝试用户密码。如果你手里只有 owner 密码但不知道 user 密码,有些加密 PDF 会允许用 owner 绕过限制,有些则不会。报错信息里如果出现 password 关键字,优先怀疑传错了密码类型。

另外,TET 也支持在错误策略上做控制。把 errorpolicy 设为 return,可以避免单个加密文件把整个批处理进程终止。我一般会在批量脚本里对每个文件都单独 try 一次,失败就把文件名记下来,最后统一处理,而不是在循环里直接退出。

4.2 扫描件与纯图像 PDF:TET 只能拿到隐藏文本层

扫描件是文本提取最容易被卡住的场景。TET 在遇到扫描 PDF 时,如果该 PDF 本身没有文字层,tet_get_text 返回空字符串,这不是程序 bug,而是页面上真的没有可提取的文本数据。扫描件本质是一张图片,文字只是图片里的像素。

解决路径一般是两步:先用 TET 判断页面是否存在文本层,对空文本页面用 OCR 引擎做识别。选 OCR 引擎时,中文识别用现有的开源模型质量已不错,关键是输出坐标要和 TET 的坐标对齐。我通常的做法是:如果某一页的文本长度小于阈值,比如小于 10 个字符且页面尺寸接近 A4,就把它标记为候选扫描页,交给 OCR 流程。这样做的好处是让 TET 处理绝大部分正常页面,OCR 只处理真正需要识别的页面,整体吞吐量能高很多。

4.3 自定义字体与乱码:为什么内容看得见却读不出

乱码问题多数发生在 PDF 嵌入了一些非标准字体,字体文件内部没有 ToUnicode 映射表。这个映射表负责把字形编号翻译成 Unicode 字符。没有它,TET 就没法准确知道某个编码对应的到底是什么字符,最后只能输出占位符或空串。

TET 处理这种情况的策略是通过内部算法反推字形路径,同时提供 font-name 之类的选项让你报告字体情况。实际项目里,我把出现乱码的页面单独拉出来,打印页面上使用的字体名和编码方式,再决定是用 TET 内置映射,还是导出字体后做二次映射。如果乱码集中在某几个字体上,优先怀疑字体子集问题,而不是整体策略问题。

5. PDFLIB/TET 读取避坑:五个最容易被拖进去的现场

5.1 现象:打开文档总是返回 -1,错误信息为空

第一次部署时,不管传什么文件,tet_open_document 都返回 -1,但错误码和错误信息都是空的。查了半天才发现问题出在 license 文件路径上。TET 的授权检查发生在创建句柄后的第一次操作,路径写错或权限不足时,接口不会报具体位置,只会让后续所有操作失败。

原因在于 license 文件在初始化阶段就要被 TET 内部加载,加载失败后句柄内部状态已经标记为不可用,但接口层面没有明显提示。解决方法是单独调用一次 tet_set_option,并在其后显式检查返回值,确认配置是否被接受。从那以后,我每次写新脚本都会把 license 检查放在第一段,跑通了再处理文件。

5.2 现象:文本能提取一部分,到某一段全是空字符

遇到过一份 PDF,前几页提取正常,中间几页拿到很多连续空字符串。起初以为是扫描件,做了一次 OCR,页面确实有文本层,但 OCR 结果把编号数字拆得稀碎。后来核对才发现,这几个页面的文本块使用了嵌入式 Type3 字体,字体内容本身是通过程序化指令绘制出来的,而非普通字符编码。

TET 对这类字体的支持取决于字体内部的字符映射数据,Type3 字体的字符可以绑定到任意绘制操作,没有可靠方法反推原文。解决方法是设置 tet_set_option 里的嵌入式字体相关选项,允许 TET 加载字体内部结构的专业映射数据。如果仍然失败,只能放弃这页的文本提取,用 OCR 兜底。遇到这种情况不要反复改参数,先确认字体类型再动手。

5.3 现象:中文整页提取正常,关键字部分乱码

这是最隐蔽的一类问题。整页文本看着没问题,但金额、日期这类关键字经常乱码,因为在页面设计时这些字段使用了不同的字体子集,子集字体保留了原字符集编码,TET 按主流字体映射方式去解码,就会在局部错位。排查时要把每个文本块关联的字体名分组输出对比。

做法是在循环里记录每个文本块关联的字体名,再单独对乱码字体做映射。通常乱码字体是 PDF 的 CID 字体变体,TET 提供了针对 CID 字体的专用选项,能按 CID 值做二次映射。我还会把提取结果和 PDF 阅读器里复制出来的文字做对比,两者一致才认为这一页处理干净。不一致时,检查字体子集和 ToUnicode 映射是最直接的路。

5.4 现象:几千页大文件读到一半,内存暴涨进程被杀

大文件批量读取时,曾遇过 9000 多页的 PDF,循环跑到 4000 多页后进程被系统杀掉。起初以为是机器内存不足,后来发现是 tet_open_page 打开的页面一直没有正确关闭,TET 的页面句柄会持续持有内部缓冲,不关闭就累积。解决办法就是第 3 章的循环写法,每一页必须 tet_close_page。

另外,循环里加载到页面的图片和图形资源也会占内存。如果确定只需要文本,可以在 tet_open_page 的参数里加上 noimages,让 TET 不解析图片对象。这个参数对纯文本提取没有副作用,还能显著降低内存占用。实测在类似场景下,单文件内存峰值从 900MB 降到不到 200MB,效果非常明显。

5.5 现象:本地提取正常,换到服务器后结果对不上

同一份 PDF 本地跑一遍正常,部署到服务器后同样的命令提取出来是空串或字符缺失。最初以为是权限问题,文件夹可读可写,文件也能打开,但结果就是不对。

原因多半是服务器上缺少 TET 的字体资源目录,特别是非标准字体的缓存。TET 在解析字体时要访问操作系统字体目录,服务器精简安装时没有这些路径,提取结果自然残缺。解决方法是把本地能跑通环境里的字体目录拷贝到服务器,并在 tet_set_option 里显式指定字体路径,然后在服务器上跑一遍第 2 章的版本验证脚本,确认 TET 版本一致后再处理业务文件。

6. 进阶:给批量读取加一道“读前体检”,再谈效率

6.1 建立体检脚本清单

与其等数据出问题再分析,不如在提取前先花几秒掌握每个 PDF 的基本属性。体检脚本四步:返回文档页数、获取加密状态、统计每页文本量、标记异常页面。这几项通过 TET 自带接口就能完成,不必额外引入依赖。

<?php // 体检脚本:输出每个 PDF 的页数、是否存在文本层、可疑页面列表 $summary = []; foreach ($pdfFiles as $file) { $doc = tet_open_document($tet, $file, "errorpolicy=return"); if ($doc == -1) { $summary[$file] = "无法打开"; continue; } $pages = tet_pcos_get_number($tet, $doc, "length:pages"); $emptyPages = 0; for ($p = 1; $p <= $pages; $p++) { $page = tet_open_page($tet, $doc, $p, "granularity=text"); $text = tet_get_text($tet, $page, "encoding=utf8"); if (strlen(trim($text)) < 10) { $emptyPages++; } tet_close_page($tet, $page); } tet_close_document($tet, $doc); $summary[$file] = "页数: $pages, 低文本页: $emptyPages"; } print_r($summary); ?>

如果你的数据里有大量低文本页,说明这批文件扫描件占比高,应该提前准备 OCR 流程,而不是等主流程跑完再补救。体检结果建议存成 JSON 或 CSV 留档,方便后续和抽取结果对齐。

6.2 抽查式验证:拿人工阅读结果对抽提结果

批量读取的验证方法是随机抽 10 个 PDF,每个抽 3 页,人工阅读这些页,再对比代码提取出的文本。对比时重点看三处:字符是否完整、数字是否走错、关键字边界是否被截断。如果有问题,按第 5 章的排查顺序定位,修完再扩大范围。

如果项目需要把提取结果放进数据库,建议同时记录提取的可信度信号:是否有过乱码、文本块是否连续、页面是否为空。TET 不直接给置信度数值,但这些信号能帮你自动筛出需要复审的记录。从那以后,我每次接到 PDF 读取需求,都先跑一遍这套体检脚本,把乱码页、空页、异常页在文本初始化阶段就拦下来,而不是等整个批处理结束后再返工。读 PDF 真正耗时间的从来不是提取本身,而是你信任那份提取结果之前的校验过程。希望这些经验能帮到你写第一版不返工的读取程序。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询