就据我所知,PDF 文件内数据提取目前有 3 种办理方案:
第一种,资金足够的话可以直接通过人工智能对 PDF 内容进行解析,按照你须要的规格数据进行输出即可;
第二种,采取 OCR 识别技能对内容进行提取;

第三种,通过工具实现(也是我将为您呈现的)。在开源社区中 PDFbox 人气很高,笔墨的识别率也很不错,但是对付表格支持不太友好,涉及到表格数据提取的我选用了 Tabula 来实现;
Tabula 是什么?Tabula是一个开源工具,用于从PDF文档中提取表格数据。它的紧张技能包括:
PDF 解析:Tabula 利用 Java 的 PDFBox 库来解析 PDF 文档的内容和布局。它可以定位到每个页的文本块和图像的坐标;表格识别:Tabula 通过剖析页面上的线条和文本块的布局来识别表格的构造。它会查找垂直和水平的线条作为列和行的分隔符;单元格提取:在确定了表格的构造后,Tabula 会剖析每个单元格对应的文本块,并提取出单元格中的文本内容;数据整理:Tabula 会考试测验自动整理从表格中提取的数据,例如:纵向和横向合并单元格,处理跨页的表格等。它也会实行一定的文本清理;导出格式:Tabula 支持将提取出来的数据导出为 CSV 和 JSON 格式。用户可以导入到 Excel 等其他工具中进行后续剖析。优化算法:Tabula 在表格剖析和数据提取方面利用了一些优化的算法和启示式规则,以提高精确率。同时它也供应了交互式的编辑接口供用户校正结果。怎么用 Tabula?首先肯定是引入 pom 文件依赖,如下图:
<dependency> <groupId>technology.tabula</groupId> <artifactId>tabula</artifactId> <version>1.0.5</version></dependency>
接着就可以创建 PDF 工具类了(PdfUtil)
public class PdfUtil { ... private static final SpreadsheetExtractionAlgorithm SPREADSHEEET_EXTRACTION_ALGORITHM = new SpreadsheetExtractionAlgorithm(); private static final ThreadLocal<List<String>> THREAD_LOCAL = new ThreadLocal<>(); ... / @description: 解析pdf表格(私有方法) 利用 tabula-java 的 sdk 基本上都是这样来解析 pdf 中的表格的,以是可以将程序提取出来,直到 cell 单元格为止 @param {} String pdf 路径 @param {} int 自定义起始行 @param {} PdfCellCallback 分外回调处理 @return {} / private static JSONArray parsePdfTable(String pdfPath, int customStart, PdfCellCustomProcess callback) { JSONArray reJsonArr = new JSONArray(); // 存储解析后的JSON数组 try (PDDocument document = PDDocument.load(new File(pdfPath))) { PageIterator pi = new ObjectExtractor(document).extract(); // 获取页面迭代器 // 遍历所有页面 while (pi.hasNext()) { Page page = pi.next(); // 获取当前页 List<Table> tableList = SPREADSHEEET_EXTRACTION_ALGORITHM.extract(page); // 解析页面上的所有表格 // 遍历所有表格 for (Table table : tableList) { List<List<RectangularTextContainer>> rowList = table.getRows(); // 获取表格中的每一行 // 遍历所有行并获取每个单元格信息 for (int rowIndex = customStart; rowIndex < rowList.size(); rowIndex++) { List<RectangularTextContainer> cellList = rowList.get(rowIndex); // 获取行中的每个单元格 callback.handler(cellList, rowIndex, reJsonArr); } } } } catch (IOException e) { LOGGER.error(MARKER, "function[PdfUtil.parsePdfTable] Exception [{} - {}] stackTrace[{}]", e.getCause(), e.getMessage(), e.getStackTrace()); } finally { THREAD_LOCAL.remove(); } return reJsonArr; // 返回解析后的JSON数组 } ...}
这里我们先按照官网样例代码来实现 pdf 表格解析先。大致的思路便是:
创建一个空的 JSONArray 工具 reJsonArr ,用于存储解析后的表格数据;利用 PDDocument.load 方法加载指定路径的 PDF 文件,并利用 try-with-resources 语句创建一个 PDDocument 工具 document ;利用 ObjectExtractor 从 document 中提取页面迭代器 pi ;利用 while 循环遍历每个页面,利用 pi.hasNext 方法判断是否还有下一个页面,如果有则进入循环;利用 pi.next 方法获取当前页面对象 page ;利用 SPREADSHEEET_EXTRACTION_ALGORITHM 解析 page 中的所有表格,并将结果存储在 tableList 中;利用 for 循环遍历 tableList 中的每个表格,对付每个表格实行以下操作:a. 利用 table.getRows 方法获取表格中的每一行,并将结果存储在 rowList 中;b. 利用 for 循环遍历 rowList 中的每一行,从 customStart 位置开始,对付每一行实行以下操作:i. 利用 rowList.get 方法获取行中的每个单元格,并将结果存储在 cellList 中;ii. 将 cellList 、 rowIndex 和 reJsonArr 作为参数通报给回调函数 callback 的 handler 方法进行处理;利用 try-catch 语句捕获可能发生的 IOException 非常,并记录缺点信息;利用 finally 语句移除 THREAD_LOCAL 中的数据;返回解析后的 JSONArray 工具 reJsonArr ;这里要加上一个 callback.handler 回调函数紧张的目的是为了将“单元格操作”跟 pdf 解析两部分代码进行解耦,那么这个回调接口的接口定义如下:
@FunctionalInterfacepublic interface PdfCellCustomProcess { / @description: 自定义单元格回调处理 @return {} / void handler(List<RectangularTextContainer> cellList, int rowIndex, JSONArray reJsonArr);}
个中 cellList 传入的是这一行的所有单元格的凑集,rowIndex 传入的是当前行码,reJsonArr 是返回值。详细的实当代码如下:
public class PdfUtil { ... / @description: 解析 pdf 中大略的表格并返回 json 数组 @param {} String PDF文件路径 @param {} int 自定义起始行 @return {} / public static JSONArray parsePdfSimpleTable(String pdfPath, int customStart) { return parsePdfTable(pdfPath, customStart, (cellList, rowIndex, reArr) -> { JSONObject jsonObj = new JSONObject(); // 遍历单元格获取每个单元格内字段内容 List<String> headList = ObjectUtil.isNullObj(THREAD_LOCAL.get()) ? new ArrayList<>() : THREAD_LOCAL.get(); for (int colIndex = 0; colIndex < cellList.size(); colIndex++) { String text = cellList.get(colIndex).getText().replace("\r", " "); if (rowIndex == customStart) { headList.add(text); } else { jsonObj.put(headList.get(colIndex), text); } } if (rowIndex == customStart) { THREAD_LOCAL.set(headList); } if (!jsonObj.isEmpty()) { reArr.add(jsonObj); } }); } ...}
代码的紧张部分是一个 Lambda 表达式,它作为参数通报给 parsePdfTable 方法。Lambda 表达式做了PdfCellCustomProcess 接口的实现。Lambda 表达式的代码块首先创建一个 JSONObject 工具,然后遍历单元格列表,获取每个单元格的文本内容。
如果当前行索引即是自定义起始行索引,将文本内容添加到 headList 列表中;否则,将文本内容作为键值对添加到jsonObj 工具中。末了,如果 jsonObj 工具不为空,则将其添加到 reArr 数组中。 代码还包含了一些其他操作。如果当前行索引即是自定义起始行索引,将 headList 列表设置为 THREAD_LOCAL 线程局部变量。末了,返回 reArr数组作为方法的结果。
末了只须要补上 main 方法调用即可获取到解析后的 JsonArray 凑集。但是直接输出 JsonArray 数据并不直不雅观,于是我又写了一个解析 JsonArray 数据的方法,并将里面的数据转换为 Markdown 格式,如下图:
private static String outputMdFormatForVerify(JSONArray jsonArr) { StringBuilder mdStrBld = new StringBuilder(); StringBuilder headerStrBld = new StringBuilder("|"); StringBuilder segmentStrBld = new StringBuilder("|"); for (int row = 0; row < jsonArr.size(); row++) { StringBuilder bodyStrBld = new StringBuilder("|"); JSONObject rowObj = (JSONObject) jsonArr.get(row); if (row == 0) { rowObj.forEach((k, v) -> { headerStrBld.append(" ").append(k).append(" |"); segmentStrBld.append(" ").append("---").append(" |"); }); headerStrBld.append("\n"); segmentStrBld.append("\n"); mdStrBld.append(headerStrBld).append(segmentStrBld); } rowObj.forEach((k, v) -> bodyStrBld.append("").append(v).append("|")); bodyStrBld.append("\n"); mdStrBld.append(bodyStrBld); } return mdStrBld.toString();}
这个该当比较好理解吧,这里就不再详述了。
以上的代码对付一样平常的 PDF 表格解析是基本没有问题的,但是对付带有合并单元格的解析就不能知足了。合并单元格须要考虑横向合并、纵向合并和稠浊合并三种合并模式,不是说 tabula-java 的 sdk 不能做只是比较麻烦,在 tabula-java 方案中我们可以获取到单元格的高和宽,那么先做一次全遍历获取二维数组对付单元格定位后,根据高和宽进行虚拟表格的培植,末了根据二维数组对数据进行回填即可。这也是用回调将单元格操作分离的缘故原由之一,为了后面做合并单元格解析做准备的。
但实在上面说这么多,合并单元格解析的代码我还没写呢(以上都是我吹的),等完成后再给大家分享。