ARTICLE · 1011219
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 不是一个库的事,是一整套方案的事——选对工具、管理预期、做好兜底,这件事就没那么难。