夜雨聆风学习资料网

ARTICLE · 1011219

Java 读取 PDF 万字详解:从入门到踩坑

Java 读取 PDF 万字详解:从入门到踩坑

做后端开发,早晚有一天会碰上「读取 PDF」这个需求。

合同解析、发票识别、论文检索、报表抽取……需求五花八门,但落到代码上,第一个问题永远是:用哪个库?

搜一下,答案能有七八个:PDFBox、iText、OpenPDF、Spire.PDF、PDFjet、PDF Clown……每个都说自己好,新手直接懵。

这篇文章我帮你理清楚:PDF 为什么难读、主流库怎么选、实际代码怎么写、常见坑在哪。看完至少能省半天选型时间。

01

PDF 为什么这么难读?

先讲一个反直觉的事实:PDF 不是文档格式,是"打印指令"格式。

你用 Word 写文档,Word 知道「这是一段文字」「这是一个表格」「这是一张图片」——它有语义结构。你读 Word 文件,本质上是在读取这些结构化信息。

但 PDF 不一样。PDF 的底层是一串"画图指令"。    

举个例子,PDF 里没有"表格"这个概念。你看到的表格,其实是一堆线条 + 一堆文字摆放在合适的位置上,看起来像表格而已。

这就是为什么「从 PDF 里提取表格」永远是最难的——库要做的不是"读表格",而是"根据坐标推断哪堆文字属于哪个单元格"。

理解了这一点,后面很多事情就顺理成章了:

• 文本顺序不一定对:PDF 里的文字是按绘制顺序存的,不是按阅读顺序。双栏排版的 PDF,提取出来的文字可能左右穿插。

• 字体可能是自定义编码:有些 PDF 用的是内嵌字体 + 自定义编码,读出来全是乱码,根本无法直接转成 Unicode。

• 扫描件就是一张图:很多 PDF 其实是扫描仪直接扫进去的图片,里面根本没有文字层,OCR 是唯一出路。

一句话总结:读 PDF 不是"解析文档",是"从一堆画图指令里反向工程出文字和结构"。准确率从来不是 100%,只能尽量逼近。 

02

主流库横评:该用哪个?

Java 生态里读 PDF 的库不少,真正值得考虑的其实就四个。    

逐个说。

① Apache PDFBox —— 首选

Apache 基金会维护,完全开源免费(Apache 2.0 协议),功能全面,社区活跃,文档也齐全。读文本、读图片、拆分合并、加解密、表单填充……基本你能想到的功能它都有。

优点:免费、开源、功能全、社区大、持续更新。

缺点:表格提取弱,需要自己写逻辑;大文件内存占用偏高。

Maven 依赖:

<dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>2.0.32</version></dependency>

② iText 7 —— 功能最强,但要注意协议

iText 是 PDF 领域的老牌选手,功能非常强大,生成和读取都很强。但注意它的协议是 AGPL——如果你的项目是商用的且不打算开源,需要购买商业授权,否则有法律风险。

iText 5 是老版本(MPL/LGPL 协议,相对宽松),但已经停止维护了。新项目不建议用。

③ OpenPDF —— iText 4 的开源分支

OpenPDF 是从 iText 4.2 分支出来的,LGPL 协议,可以商用。功能比 iText 少一些,但基本的读写都够用。如果你对协议敏感又不想用 PDFBox,可以考虑它。

④ Spire.PDF for Java —— 商业库

商业软件,功能非常全面,表格提取、HTML 转 PDF 这些都做得很好。免费版有限制(最多 10 页)。如果预算充足且对表格提取要求高,可以评估一下。

选型结论:大多数场景用 PDFBox 就够了。协议友好、功能全面、社区活跃,遇到问题搜得到答案。如果表格提取是核心需求且预算充足,再考虑 Spire.PDF。 

03

PDFBox 实战:读文本

从最简单的开始:把 PDF 里所有文字提取出来。

import org.apache.pdfbox.pdmodel.PDDocument;import org.apache.pdfbox.text.PDFTextStripper;import java.io.File;public static String extractText(String filePath) throws Exception {try (PDDocument document = PDDocument.load(new File(filePath))) {        PDFTextStripper stripper = new PDFTextStripper();// 设置排序:尽量按阅读顺序输出        stripper.setSortByPosition(true);// 提取全部页面        String text = stripper.getText(document);return text;    }}

就这么简单?是的,基础用法就是这么简单。但有几个坑要注意。

坑 1:setSortByPosition 不是万能的。

这个参数会让 PDFBox 尝试按"从上到下、从左到右"的阅读顺序排列文字。对于单栏文档效果还可以,但对于复杂的多栏布局、图文混排,效果依然不理想。

坑 2:字体编码问题。

有些 PDF 的字体是自定义编码(比如某些设计院、印刷厂的 PDF),提取出来全是方框或乱码。这种情况 PDFBox 也没辙——它只能忠实读取 PDF 里存的内容,存的是乱码就只能读出乱码。

判断方法:用 PDF 阅读器打开,能不能正常复制文字?如果复制出来就是乱码,那任何库都读不出来,只能上 OCR。

坑 3:只提取指定页。

// 只提取第 3 到第 5 页(从 1 开始计数)stripper.setStartPage(3);stripper.setEndPage(5);String partialText = stripper.getText(document);

04

PDFBox 实战:读图片

提取 PDF 里的图片,思路是遍历每一页的资源,找出所有图像对象。

public static voidextractImages(String pdfPath, String outputDir)throws Exception {try (PDDocument document = PDDocument.load(new File(pdfPath))) {int imageIndex = 0;for (int pageIdx = 0; pageIdx < document.getNumberOfPages(); pageIdx++) {            PDPage page = document.getPage(pageIdx);            PDResources resources = page.getResources();for (COSName name : resources.getXObjectNames()) {                PDXObject xobj = resources.getXObject(name);if (xobj instanceof PDImageXObject) {                    PDImageXObject image = (PDImageXObject) xobj;                    String outPath = outputDir + "/image_"                            + (pageIdx + 1) + "_"                            + (++imageIndex) + ".png";                    ImageIO.write(image.getImage(), "png",new File(outPath));                }            }        }    }}

这段代码能把 PDF 里嵌入的图片全部提取出来。但要注意两点:

• 提取出来的图片没有上下文,你不知道它在原文的哪个位置、对应哪段文字。如果需要位置信息,得额外处理坐标。

• 有些 PDF 把图片切成了很多小图块(为了压缩或渲染优化),提取出来是一堆碎片,不是完整的大图。

05

最难的部分:提取表格

这是 PDF 提取里最难的一块,也是翻车最多的地方。

前面说了,PDF 里没有"表格"这个概念,只有线条和文字。要提取表格,本质上是一个逆向工程:先找到所有横线竖线,推断出单元格的位置,再把落在每个单元格里的文字归进去。    

PDFBox 本身不提供表格提取功能。要做表格提取,有三条路:

方案一:自己写(适合简单表格)

如果你的 PDF 格式固定、表格规整,可以自己写逻辑。大致思路:

1. 用 PDFTextStripperByArea 按区域提取文字

2. 或者用 TextPosition 获取每个字符的坐标

3. 根据 Y 坐标分行、X 坐标分列,自己拼装成表格

适合格式固定且简单的场景,比如发票、固定模板的报表。一旦格式变了,代码就得重写。

方案二:用 tabula-java(推荐)

tabula-java 是一个专门用来从 PDF 提取表格的开源库,基于 PDFBox 开发。它实现了两种提取算法:一种基于线条检测,一种基于文本间距推断,能处理大多数有框线和无框线的表格。

public static List<List<List<String>>> extractTables(        String pdfPath, int pageNum) throws Exception {    PDDocument document = PDDocument.load(new File(pdfPath));    SpreadsheetExtractionAlgorithm algorithm =new SpreadsheetExtractionAlgorithm();    Page page = new ObjectExtractor(document)            .extract(pageNum);    List<Table> tables = algorithm.extract(page);return tables.stream()        .map(t -> t.getRows().stream()            .map(r -> r.stream()                .map(Cell::getText)                .collect(toList()))            .collect(toList()))        .collect(toList());}

Maven 依赖:

<dependency><groupId>technology.tabula</groupId><artifactId>tabula</artifactId><version>1.0.5</version></dependency>

注意:tabula 的准确率取决于表格的规整程度。线条清晰、单元格对齐整齐的表格,准确率能到 95% 以上;但如果是合并单元格、嵌套表格、虚线边框,效果就会打折扣。

方案三:商业库(Spire / Aspose)

如果预算充足且表格提取是核心需求,商业库的表格识别能力通常比开源方案强一大截。Spire.PDF 和 Aspose.PDF 都有成熟的表格提取 API,几行代码就能搞定。

06

遇到扫描件怎么办?

前面说的所有方法,都有一个前提:PDF 里有文字层。

但很多 PDF 是扫描件——整页就是一张大图片,里面根本没有文字。这种情况,PDFBox 再强大也没用,因为确实没东西可读。

怎么办?答案是 OCR(光学字符识别):先把 PDF 每页转成图片,再用 OCR 引擎识别图片里的文字。    

Java 生态里最常用的 OCR 方案是 Tesseract,通过 tess4j 这个库来调用。

public static String ocrPdf(String pdfPath) throws Exception {    Tesseract tesseract = new Tesseract();// 设置语言包路径和语言    tesseract.setDatapath("/path/to/tessdata");    tesseract.setLanguage("chi_sim+eng"); // 中文简体+英文// 直接对 PDF 做 OCR(内部会转图片)    String result = tesseract.doOCR(new File(pdfPath));return result;}

几个关键注意点:

• 语言包要下载:Tesseract 需要对应语言的训练数据(.traineddata 文件),中文简体是 chi_sim.traineddata,去 GitHub 上下载放到 tessdata 目录。

• 速度慢:OCR 比直接读文字慢 10-100 倍,一页可能要 1-5 秒。大文件建议异步处理或分批处理。

• 准确率取决于清晰度:300 DPI 以上的扫描件效果不错,低分辨率、模糊、倾斜的效果会差很多。建议先做预处理(去噪、二值化、倾斜校正)再识别。

• 原生依赖:tess4j 底层调用的是 C++ 的 Tesseract,需要安装原生库。服务器部署时要注意环境问题。

07

性能与内存:别让大文件搞崩你的服务

PDF 文件说大不大,说小不小。几 MB 的文件没问题,但如果是几百页的扫描件,可能几十上百 MB,处理不好很容易 OOM。

① 用 try-with-resources 确保关闭

PDDocument 是需要关闭的资源,一定要用 try-with-resources,否则文件句柄和内存泄漏都是常事。

② 流式读取(PDFBox 2.0+)

对于特别大的文件,可以用流式加载,只把当前需要的部分加载到内存:

// 流式加载,减少内存占用PDDocument document = Loader.loadPDF(new RandomAccessReadBufferedFile(file));// 或者设置内存使用策略MemoryUsageSetting memSetting =    MemoryUsageSetting.setupMixed(256 * 1024 * 1024);PDDocument doc = PDDocument.load(file, memSetting);

setupMixed(256MB) 的意思是:内存最多用 256MB,超过了就把部分数据写到临时文件里。可以有效防止大文件把堆内存撑爆。

③ 逐页处理,不要一次性全读

如果你的业务是"逐页处理"(比如每页提取文字后存入数据库),那就不要一次性把整个文档的文字都读出来。读完一页处理一页、释放一页的相关对象,内存占用会小很多。

④ 并发控制

PDF 解析很吃 CPU 和内存。如果是 Web 服务,建议对 PDF 解析接口做并发限制,比如同一时间最多处理 2-3 个 PDF,请求多了就排队。不然很容易把服务器打挂。

08

最后说两句

回到开头那个问题:Java 读 PDF,到底用哪个库?

我的建议很简单:    

但比选型更重要的,是管理好预期。

很多人一上来就想"100% 准确提取 PDF 内容"——这是不现实的。PDF 格式的本质决定了它不是为"被解析"而设计的。再强的库,遇到排版复杂的 PDF,准确率也会下降。

正确的心态是:

• 如果 PDF 是自己生成的、格式可控的——那最简单,怎么提取都准。

• 如果 PDF 是外部来的、格式固定的(比如某类发票、某种报表)——针对格式做适配,准确率可以很高。

• 如果 PDF 是来源杂、格式五花八门的——降低预期,把 PDF 提取当作"初筛",关键数据还是要人工复核。

一句话收尾:读 PDF 不是一个库的事,是一整套方案的事——选对工具、管理预期、做好兜底,这件事就没那么难。 
— 完 —

相关学习资料