一.业务需求
最近奇奇怪怪的需求越来越多了,最近接到一个新需求,需要定时任务拿到其他服务器的一个PDF文件,然后读取PDF里面的文件,读里面的数据记录下来;
二.技术选型
尝试过的工具包有:pdfbox、tabula。最终选用tabula
因为tabula可以读取pdf表格,解析为json或者其他格式,最主要是免费的,其实最开始的是想将PDF转excel,然后用poi读取excel数据,因为这个表格最开始就是excel模板
简单介绍一下tabula:
Tabula是一个开源工具,用于从PDF文档中提取表格数据。它的主要技术包括:
1.PDF 解析:Tabula 使用 Java 的 PDFBox 库来解析 PDF 文档的内容和布局。它可以定位到每个页的文本块和图像的坐标;
2.表格识别:Tabula 通过分析页面上的线条和文本块的布局来识别表格的结构。它会查找垂直和水平的线条作为列和行的分隔符;
3.单元格提取:在确定了表格的结构后,Tabula 会分析每个单元格对应的文本块,并提取出单元格中的文本内容;
4.数据整理:Tabula 会尝试自动整理从表格中提取的数据,例如:纵向和横向合并单元格,处理跨页的表格等。它也会执行一定的文本清理;
5.导出格式:Tabula 支持将提取出来的数据导出为 CSV 和 JSON 格式。用户可以导入到 Excel 等其他工具中进行后续分析。
6.优化算法:Tabula 在表格分析和数据提取方面使用了一些优化的算法和启发式规则,以提高正确率。同时它也提供了交互式的编辑接口供用户校正结果
三.导入依赖
<dependency>
<groupId>technology.tabula</groupId>
<artifactId>tabula</artifactId>
<version>1.0.3</version>
<exclusions>
<exclusion>
<artifactId>slf4j-simple</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
四.封装的工具类
因为我只要读取表格内的数据,其他的不怎么在意,然后一切从简,下面的代码直接搬来用
@Slf4j
public class ReadPDFUtil {
private static final SpreadsheetExtractionAlgorithm SPREADSHEEET_EXTRACTION_ALGORITHM = new SpreadsheetExtractionAlgorithm();
private static final ThreadLocal<List<String>> THREAD_LOCAL = new ThreadLocal<>();
public static void main(String[] args){
// 方法一
String filePath = "E:\\Downloads\\预防检修任务计划表.pdf";
// 因为我是需要从第六行开始读取数据
List<String> strings = parsePdfTableForTasks(filePath, 6);
for (String string : strings) {
System.out.println(string);
}
// 方法二,三
//String json = readPDFcontentByFilePath(filePath);
}
/**
*
* @param pdfPath 文件路径
* @param customStart 读取表格开始的行数
* @return 读取的内容
*/
public static List<String> parsePdfTableForTasks(String pdfPath, Integer customStart) {
List<String> resultStr = new ArrayList<>(); // 存储解析后的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);
StringBuilder str = new StringBuilder();
for (RectangularTextContainer rectangularTextContainer : cellList) {
String text = rectangularTextContainer.getText();
if (StrUtil.isEmpty(text)){
text=" ";
}
str.append(text).append("__");
}
resultStr.add(str.toString());
}
}
}
} catch (IOException e) {
log.error("读取PDF错误:{}",e.getMessage());
} finally {
THREAD_LOCAL.remove();
}
return resultStr;
}
/**
* 读取PDF方法
* @param filePath PDF的全路径
* @return PDF内容
*/
public static String readPDFcontentByFilePath(String filePath){
try {
String[] argsa = new String[]{"-f=JSON", "-p=1", filePath, "-l"};
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(CommandLineApp.buildOptions(), argsa);
StringBuilder stringBuilder = new StringBuilder();
new CommandLineApp(stringBuilder, cmd).extractTables(cmd);
return stringBuilder.toString();
}catch (Exception e){
log.error("读取PDF错误:{}",e.getMessage());
}
return null;
}
/**
* 读取PDF方法
* @param conversionType -f导出格式,默认CSV (一定要大写)
* @param pageNo -p 指导出哪页,all是所有
* @param filePath 文件路径
* @param modeType 读取类型
* @return -l 强制使用点阵模式提取PDF (关键在于这儿)
*/
public static String readPDFcontentByFilePath(String conversionType,String pageNo,String filePath,String modeType){
if(Objects.isNull(conversionType)){
conversionType = "JSON";
}
if(Objects.isNull(pageNo)){
pageNo = "all";
}
if(Objects.isNull(modeType)){
modeType = "l";
}
try {
String[] argsa = new String[]{"-f="+conversionType, "-p="+pageNo, filePath, "-"+modeType};
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(CommandLineApp.buildOptions(), argsa);
StringBuilder stringBuilder = new StringBuilder();
new CommandLineApp(stringBuilder, cmd).extractTables(cmd);
return stringBuilder.toString();
}catch (Exception e){
log.error("读取PDF错误:{}",e.getMessage());
}
return null;
}
}
我目前用的是第一个方法;运行结果是
需要根据自己需求调整一下代码就可以了,感觉还有很多bug
以上的代码对于一般的 PDF 表格解析是基本没有问题的,但是对于带有合并单元格的解析就不能满足了。合并单元格需要考虑横向合并、纵向合并和混合合并三种合并模式,不是说 tabula-java 的 sdk 不能做只是比较麻烦,在 tabula-java 方案中我们可以获取到单元格的高和宽,那么先做一次全遍历获取二维数组对于单元格定位后,根据高和宽进行虚拟表格的建设,最后根据二维数组对数据进行回填即可。这也是用回调将单元格操作分离的原因之一,为了后面做合并单元格解析做准备的。
等有需求我在改代码,目前就这样把,我现在只想躺平了!!!!!