Java之提取PDF表格数据

  一.业务需求


       最近奇奇怪怪的需求越来越多了,最近接到一个新需求,需要定时任务拿到其他服务器的一个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 方案中我们可以获取到单元格的高和宽,那么先做一次全遍历获取二维数组对于单元格定位后,根据高和宽进行虚拟表格的建设,最后根据二维数组对数据进行回填即可。这也是用回调将单元格操作分离的原因之一,为了后面做合并单元格解析做准备的。

     等有需求我在改代码,目前就这样把,我现在只想躺平了!!!!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值