SpringBoot+tabula+pdfbox解析pdf中的段落和表格数据

news2025/6/2 3:30:49

一、前言

在日常业务需求中,往往会遇到解析pdf文件中的段落或者表格数据的需求。
常见的做法是使用 pdfbox 来做,但是它只能提取文本数据,没有我们在文件页面上面的那种结构化组织,文本通常是散乱的包含各种换行回车空格等格式,因而它适合做一些段落文本提取。
而 tabula 在 pdfbox 的基础上做了表格的特殊处理,能够直接读取到单元格中的内容,但是它处理的前提是表格必须常规完整边框的表格,只有部分边框或者无边框的这种结构化数据还是束手无策。
针对上述情况,笔者实现了有边框和无边框表格的数据读取并结构化,也支持段落文本提取。

二、功能实现

2.1 引入依赖

<!-- PDF解析,内含pdfbox -->
<dependency>
    <groupId>technology.tabula</groupId>
    <artifactId>tabula</artifactId>
    <version>1.0.5</version>
</dependency>

2.2 完整边框表格

  • 支持多表格
  • 支持分页
  • 支持跳过标题行
  • 支持跳过标题前无关行
  • 支持生成字段
  • 返回完整集合数据
    在这里插入图片描述

2.2.1 代码实现

package com.qiangesoft.pdf.util;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import technology.tabula.*;
import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * pdf工具类
 * ps:适合解析纯文本、解析表格数据
 *
 * @author qiangesoft
 * @date 2025-05-28
 */
@Slf4j
public class PdfUtil {

    public static void main(String[] args) throws FileNotFoundException {
        String txt = readTxtFromPdf("C:\\Users\\admin\\Desktop\\微信流水.pdf", null);
        System.out.println(txt);

        List<List<Map<String, String>>> dataGroupList = readTableDataFromPdf("C:\\Users\\admin\\Desktop\\微信流水.pdf", null, true);
        for (List<Map<String, String>> list : dataGroupList) {
            for (Map<String, String> map : list) {
                System.out.println(JSON.toJSONString(map));
            }
        }
    }

    /**
     * 解析pdf的文本数据
     *
     * @param filePath 文件路径
     * @param password 文件密码
     * @return
     */
    public static String readTxtFromPdf(String filePath, String password) throws FileNotFoundException {
        return readTxtFromPdf(new FileInputStream(filePath), password);
    }

    /**
     * 解析pdf的文本数据
     *
     * @param inputStream 文件流
     * @param password 文件密码
     * @return
     */
    public static String readTxtFromPdf(InputStream inputStream, String password) {
        String textContent = "";
        try (PDDocument document = PDDocument.load(inputStream, password)) {
            PDFTextStripper stripper = new PDFTextStripper();
            textContent = stripper.getText(document);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return textContent;
    }

    /**
     * 解析pdf的表格数据
     *
     * @param filePath 文件路径
     * @param password 文件密码
     * @param skipFirstRow 是否跳过表头行 【连续分页表格可能每页有表头】
     * @return
     */
    public static List<List<Map<String, String>>> readTableDataFromPdf(String filePath, String password, boolean skipFirstRow) throws FileNotFoundException {
        return readTableDataFromPdf(new FileInputStream(filePath), password, skipFirstRow);
    }

    /**
     * 解析pdf的表格数据
     *
     * @param inputStream 文件流
     * @param password 文件密码
     * @param skipFirstRow 是否跳过表头行
     * @return
     */
    public static List<List<Map<String, String>>> readTableDataFromPdf(InputStream inputStream, String password, boolean skipFirstRow) {
        // 按照同一个表格分组
        List<List<Map<String, String>>> dataGroupList = new ArrayList<>();

        // 表格提取算法
        SpreadsheetExtractionAlgorithm algorithm = new SpreadsheetExtractionAlgorithm();

        try (PDDocument document = PDDocument.load(inputStream, password)) {
            ObjectExtractor extractor = new ObjectExtractor(document);
            PageIterator pi = extractor.extract();
            // 遍历页
            double x = 0;
            int tableIndex = 0;
            int tableHeadRowNum = 0;
            List<Table> tables = new ArrayList<>();
            List<String> fieldList = new ArrayList<>();
            while (pi.hasNext()) {
                Page page = pi.next();
                List<Table> tableList = algorithm.extract(page);

                // 遍历表格
                for (Table table : tableList) {
                    if (tableIndex == 0) {
                        tableHeadRowNum = getTableHeadRowNum(table, fieldList);
                        tables.add(table);
                        tableIndex++;
                    } else {
                        // 第一个 or x轴且列数相同为同一个表格
                        if (new BigDecimal(table.getX()).subtract(new BigDecimal(x)).abs().compareTo(new BigDecimal("0.001")) <= 0
                                && fieldList.size() == table.getRows().get(0).size()) {
                            tables.add(table);
                        } else {
                            List<Map<String, String>> dataList = convertTableToMap(tables, fieldList, tableHeadRowNum, skipFirstRow);
                            dataGroupList.add(dataList);

                            tables = new ArrayList<>();
                            tables.add(table);
                            tableIndex = 0;
                        }
                    }
                    x = table.getX();
                }
            }

            // 最后一个特殊处理
            if (!tables.isEmpty()) {
                List<Map<String, String>> dataList = convertTableToMap(tables, fieldList, tableHeadRowNum, skipFirstRow);
                dataGroupList.add(dataList);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return dataGroupList;
    }

    /**
     * 获取字段并返回表格头的行
     *
     * @param table 表格
     * @param fieldList 字段列表
     * @return
     */
    private static int getTableHeadRowNum(Table table, List<String> fieldList) {
        // 获取表格头
        int headRowNum = 0;
        List<List<RectangularTextContainer>> rowList = table.getRows();
        for (int i = 0; i < rowList.size(); i++) {
            fieldList.clear();
            List<RectangularTextContainer> cellList = rowList.get(i);
            int k = 0;
            for (int j = 0; j < cellList.size(); j++) {
                RectangularTextContainer cell = cellList.get(j);
                if (cell instanceof Cell) {
                    k++;
                    fieldList.add("k" + k);
                }
            }

            if (fieldList.size() == cellList.size()) {
                headRowNum = i;
                break;
            }
        }

        return headRowNum;
    }

    /**
     * 将表格数据转为映射数据
     *
     * @param tableList 表格列表
     * @param fieldList 字段列表
     * @param tableHeadRowNum 表格头行
     * @param skipFirstRow 是否跳过表头行
     * @return
     */
    private static List<Map<String, String>> convertTableToMap(List<Table> tableList, List<String> fieldList, int tableHeadRowNum, boolean skipFirstRow) {
        List<Map<String, String>> dataList = new ArrayList<>();

        for (int i = 0; i < tableList.size(); i++) {
            // 表格所有行
            Table table = tableList.get(i);
            List<List<RectangularTextContainer>> rowList = table.getRows();

            // 遍历行
            for (int j = (i == 0 ? tableHeadRowNum + 1 : skipFirstRow ? 1 : 0); j < rowList.size(); j++) {
                List<RectangularTextContainer> cellList = rowList.get(j);
                Map<String, String> data = new HashMap<>();
                // 遍历列
                for (int m = 0; m < cellList.size(); m++) {
                    RectangularTextContainer cell = cellList.get(m);
                    // 去除换行符后设置值
                    String text = cell.getText().replace("\r", "");
                    data.put(fieldList.get(m), text);
                }
                dataList.add(data);
            }
        }
        return dataList;
    }

    /**
     * 读取指定文字中间的文本
     *
     * @param txt 文本
     * @param startStr 开始字符串
     * @param endStr 结束字符串
     * @return
     */
    public static String readTxtFormTxt(String txt, String startStr, String endStr) {
        int index1 = txt.indexOf(startStr);
        if (index1 == -1) {
            return null;
        }
        int index2 = txt.length();
        if (endStr != null) {
            index2 = txt.indexOf(endStr);
            if (index2 == -1) {
                index2 = txt.length();
            }
        }

        return txt.substring(index1 + startStr.length(), index2);
    }

}

2.2.2 解析结果

在这里插入图片描述

2.3 无边框表格

  • 支持单表格
  • 支持分页
  • 支持跳过标题行
  • 支持生成字段
  • 返回完整集合数据
    在这里插入图片描述

2.3.1 代码实现

package com.qiangesoft.pdf.util;

import com.alibaba.fastjson.JSONObject;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.*;

/**
 * pdf规则数据分析工具类
 * ps:分析处理PdfUtil解决不了的表格,没有格子
 *
 * @author qiangesoft
 * @date 2025-05-28
 */
public class PdfRuleDataUtil {

    public static void main(String[] args) throws IOException {
        String fileTxt = PdfUtil.readTxtFromPdf("C:\\Users\\admin\\Desktop\\流水文件\\中国建设银行.pdf", null);
        System.out.println(readTxt(fileTxt, "卡号/账号:", "客户名称:").trim());
        System.out.println(readTxt(fileTxt, "客户名称:", "起始日期:").trim());
        System.out.println(readTxt(fileTxt, "起始日期:", "结束日期:").trim());
        System.out.println(readTxt(fileTxt, "结束日期:", "序号").trim());
        List<Map<String, String>> dataList = readTableData(fileTxt, "序号 摘要 币别 钞汇 交易日期 交易金额 账户余额 交易地点/附言 对方账号与户名", "生成时间:");
        for (Map<String, String> map : dataList) {
            System.out.println(JSONObject.toJSONString(map));
        }
    }

    /**
     * 解析文本
     *
     * @param fileTxt
     * @param startStr
     * @param endStr
     * @return
     */
    public static String readTxt(String fileTxt, String startStr, String endStr) {
        return PdfUtil.readTxtFormTxt(fileTxt, startStr, endStr);
    }

    /**
     * 解析表格数据
     *
     * @param fileTxt 文本数据
     * @param startStr 开始字符串 【一般为标题行,字段根据标题行定,***很重要***】
     * @param endStr 结束字符串 【结束标志,如果表格连续中间没有重复的标题行则直接使用表格末尾的结束标志即可,如果表格不连续每页都有标题行则使用每页的结束标志】
     * @return
     */
    public static List<Map<String, String>> readTableData(String fileTxt, String startStr, String endStr) {
        int length = startStr.trim().split(" ").length;
        List<String> fieldList = new ArrayList<>();
        for (int i = 1; i <= length; i++) {
            fieldList.add("k" + i);
        }

        List<Map<String, String>> lists = new ArrayList<>();
        while (true) {
            String dataStr = readTxt(fileTxt, startStr, endStr);
            if (dataStr == null) {
                break;
            }
            List<Map<String, String>> pageLists = readDataFromTxt(dataStr, startStr, fieldList);
            fileTxt = fileTxt.substring(fileTxt.indexOf(endStr) + endStr.length());
            if (CollectionUtils.isEmpty(pageLists)) {
                break;
            } else {
                lists.addAll(pageLists);
            }
        }

        return lists;
    }

    /**
     * 解析pdf的文本数据
     * ps:通过换行符进行分割行,然后根据空格分割列【如果列中数据存在空格则无法解决】
     *
     * @param dataStr 待解析的文本
     * @param tableHeadTxt 标题行文本
     * @param fieldList 字段列表
     * @return
     */
    private static List<Map<String, String>> readDataFromTxt(String dataStr, String tableHeadTxt, List<String> fieldList) {
        List<Map<String, String>> dataList = new ArrayList<>();

        int cellNum = fieldList.size();
        // "\r\n" or "\n"
        String[] split = dataStr.split(System.lineSeparator());

        StringBuilder chargeStr = new StringBuilder();
        for (int a = 0; a < split.length; a++) {
            String itemStr = split[a];
            // 标题行跳过
            if (itemStr.contains(tableHeadTxt)) {
                continue;
            }

            String[] split1;
            if (!chargeStr.toString().isEmpty()) {
                // 上一行未处理【加上本行一起处理】
                chargeStr.append(itemStr);
                split1 = chargeStr.toString().split(" ");
            } else {
                split1 = itemStr.split(" ");
            }

            if (split1.length < cellNum) { // 不足列数
                // 拼接本行
                if (chargeStr.toString().isEmpty()) {
                    chargeStr.append(itemStr);
                }
                // 最后一行特殊处理
                if (a == split.length - 1) {
                    Map<String, String> dataMap = new HashMap<>();
                    for (int i = 0; i < cellNum; i++) {
                        if (i > split1.length - 1) {
                            dataMap.put(fieldList.get(i), null);
                        } else {
                            dataMap.put(fieldList.get(i), split1[i]);
                        }
                    }
                    dataList.add(dataMap);
                }
            } else if (split1.length > cellNum) { // 超过列数
                if (!chargeStr.toString().isEmpty()) {
                    // 处理上一行
                    String[] split2 = chargeStr.toString().replace(itemStr, "").split(" ");
                    Map<String, String> dataMap = new HashMap<>();
                    for (int i = 0; i < cellNum; i++) {
                        if (i > split2.length - 1) {
                            dataMap.put(fieldList.get(i), null);
                        } else {
                            dataMap.put(fieldList.get(i), split2[i]);
                        }
                    }
                    dataList.add(dataMap);
                }

                // 处理本行
                chargeStr = new StringBuilder();
                String[] split3 = itemStr.split(" ");
                if (split3.length < cellNum) { // 本行不足列数
                    // 拼接本行
                    if (chargeStr.toString().isEmpty()) {
                        chargeStr.append(itemStr);
                    }
                    // 最后一行特殊处理
                    if (a == split.length - 1) {
                        Map<String, String> dataMap = new HashMap<>();
                        for (int i = 0; i < cellNum; i++) {
                            if (i > split3.length - 1) {
                                dataMap.put(fieldList.get(i), null);
                            } else {
                                dataMap.put(fieldList.get(i), split3[i]);
                            }
                        }
                        dataList.add(dataMap);
                    }
                } else { // 本行大于等于列数
                    Map<String, String> dataMap = new HashMap<>();
                    for (int i = 0; i < cellNum; i++) {
                        if (i > split3.length - 1) {
                            dataMap.put(fieldList.get(i), null);
                        } else {
                            dataMap.put(fieldList.get(i), split3[i]);
                        }
                    }
                    dataList.add(dataMap);
                }
            } else { // 等于列数
                Map<String, String> dataMap = new HashMap<>();
                for (int i = 0; i < cellNum; i++) {
                    dataMap.put(fieldList.get(i), split1[i]);
                }
                dataList.add(dataMap);
                chargeStr = new StringBuilder();
            }
        }

        return dataList;
    }


}

2.3.2 解析结果

在这里插入图片描述

2.4 解析段落

在这里插入图片描述

2.4.1 代码实现

/**
     * 读取指定文字中间的文本
     *
     * @param txt 文本
     * @param startStr 开始字符串
     * @param endStr 结束字符串
     * @return
     */
    public static String readTxtFormTxt(String txt, String startStr, String endStr) {
        int index1 = txt.indexOf(startStr);
        if (index1 == -1) {
            return null;
        }
        int index2 = txt.length();
        if (endStr != null) {
            index2 = txt.indexOf(endStr);
            if (index2 == -1) {
                index2 = txt.length();
            }
        }

        return txt.substring(index1 + startStr.length(), index2);
    }

2.4.2 解析结果

在这里插入图片描述

三、源码仓库

码云:https://gitee.com/qiangesoft/boot-business/tree/master/boot-business-pdf

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2393226.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GitHub push失败解决办法-fatal: unable to access ‘https://github.com/xxx

问题描述&#xff1a; 问题解决&#xff1a; 1、首先查找自己电脑的代理地址和端口 windows教程如下&#xff1a; 1、搜索控制面板-打开Internet选项 2、点击局域网设置&#xff1a; 3、如图为地址和端口号 即可获得本机地址和端口号 2、根据上一步获得的本机地址和端口号为…

电商平台 API、数据抓取与爬虫技术的区别及优势分析

一、技术定义与核心原理 电商平台 API&#xff08;应用程序编程接口&#xff09; 作为平台官方提供的标准化数据交互通道&#xff0c;API 通过 HTTP 协议实现不同系统间的结构化数据传输。开发者需申请授权&#xff08;如 API 密钥&#xff09;&#xff0c;按照文档规范调用接口…

单卡4090部署Qwen3-32B-AWQ(4bit量化)-vllm

单卡4090部署Qwen3-32B-AWQ(4bit量化) 模型&#xff1a;Qwen3-32B-AWQ(4bit量化) 显卡&#xff1a;4090 1 张 python版本 python 3.12 推理框架“vllm 重要包的版本 vllm0.9.0创建GPU云主机 这里我使用的是优云智算平台的GPU&#xff0c;使用链接可以看下面的 https://blog.…

漫画Android:Handler机制是怎么实现的?

线程之间通信会用到Handler&#xff0c;比如&#xff0c;在子线程中进行耗时的网络请求任务&#xff0c;子线程在获取到数据后&#xff0c;更新界面的时候就需要用到Handler&#xff1b; 子线程在获取到数据后&#xff0c;不直接去更新 界面&#xff0c;而是把数据通过一个消息…

多部手机连接同一wifi的ip一样吗?如何更改ip

通常情况下&#xff0c;多部手机连接同一个WiFi时&#xff0c;它们的IP地址是各不相同的&#xff08;在局域网内&#xff09;。但是&#xff0c;从互联网&#xff08;外网&#xff09;的角度看&#xff0c;它们共享同一个公网IP地址。让我详细解释一下&#xff0c;并说明如何更…

飞牛fnNAS的Docker应用之迅雷篇

目录 一、“迅雷”应用安装 二、启动迅雷 三、迅雷账号登录 四、修改“迅雷”下载保存路径 1、下载路径准备 2、停止“迅雷”Docker容器 3、修改存储位置 4、重新启动Docker容器 5、再次“启用”迅雷 五、测试 1、在PC上添加下载任务 2、手机上管理 3、手机添加下…

SQLMesh 用户定义变量详解:从全局到局部的全方位配置指南

SQLMesh 提供了灵活的多层级变量系统&#xff0c;支持从全局配置到模型局部作用域的变量定义。本文将详细介绍 SQLMesh 的四类用户定义变量&#xff08;global、gateway、blueprint 和 local&#xff09;以及宏函数的使用方法。 一、变量类型概述 SQLMesh 支持四种用户定义变量…

inviteflood:基于 UDP 的 SIP/SDP 洪水攻击工具!全参数详细教程!Kali Linux教程!

简介 一种通过 UDP/IP 执行 SIP/SDP INVITE 消息泛洪的工具。该工具已在 Linux Red Hat Fedora Core 4 平台&#xff08;奔腾 IV&#xff0c;2.5 GHz&#xff09;上测试&#xff0c;但预计该工具可在各种 Linux 发行版上成功构建和执行。 inviteflood 是一款专注于 SIP 协议攻…

Visual Studio 2022 设置自动换行

Visual Studio 2022 设置自动换行 一、在 Visual Studio 菜单栏上&#xff0c;选择 工具>选项二、选择“文本编辑器”>“所有语言”>“常规” 全局设置此选项。 一、在 Visual Studio 菜单栏上&#xff0c;选择 工具>选项 二、选择“文本编辑器”>“所有语言”&…

【Linux网络篇】:简单的TCP网络程序编写以及相关内容的扩展

✨感谢您阅读本篇文章&#xff0c;文章内容是个人学习笔记的整理&#xff0c;如果哪里有误的话还请您指正噢✨ ✨ 个人主页&#xff1a;余辉zmh–CSDN博客 ✨ 文章所属专栏&#xff1a;Linux篇–CSDN博客 文章目录 一.简单的TCP网络程序相关接口代码实现服务器单进程版服务器多…

Scratch节日 | 粽子收集

端午节怎么过&#xff1f;当然是收粽子啦&#xff01;这款 粽子收集 小游戏&#xff0c;让你一秒沉浸节日氛围&#xff0c;轻松收集粽子&#xff0c;收获满满快乐&#xff01; &#x1f3ae; 玩法介绍f 开始游戏&#xff1a;点击开始按钮&#xff0c;游戏正式开始&#xff01;…

stl三角面元文件转颗粒VTK文件

效果展示&#xff1a; import os import sys import json import argparse import numpy as np import pandas as pd import open3d as o3d from glob import globPARTICLE_RADIUS 0.025def stl_to_particles(objpath, radiusNone):if radius is None:radius PARTICLE_RADIU…

Java String的使用续 -- StringBuilder类和StringBuffer

文章目录 字符串的不可变性StringBuilder和StringBuffer函数使用 字符串的不可变性 字符串不可变是因为有private修饰&#xff0c;只能在类的内部使用不可以在类外使用&#xff0c;因此使用时是不可以修改字符串的 public class test {public static void main(String[] args…

基于python+Django+Mysql的校园二手交易市场

文章目录 基于pythonDjangoMysql的校园二手交易市场运行步骤系统设计功能设计任务目标用户特点参与者列表基本要求功能模块图 数据库设计会员用户信息表&#xff08;user_userinfo&#xff09;商品信息表&#xff08;goods_goodsinfo&#xff09;管理员用户信息表&#xff08;a…

从零打造算法题刷题助手:Agent搭建保姆级攻略

我用Trae 做了一个有意思的Agent 「大厂机试助手」。 点击 https://s.trae.com.cn/a/d2a596 立即复刻&#xff0c;一起来玩吧&#xff01; Agent 简介 Agent名称为大厂机试助手&#xff0c;主要功能有以下三点。 解题&#xff1a; 根据用户给出的题目给出具体的解题思路引导做…

懒人云电脑方案:飞牛NAS远程唤醒 + 节点小宝一键唤醒、远程控制Windows!

后台高频问题解答&#xff1a; “博主&#xff0c;飞牛NAS能定时开关机了&#xff0c;能不能让它顺便把家里Windows电脑也远程唤醒控制&#xff1f;最好点一下就能连&#xff0c;不用记IP端口那种&#xff01;” 安排&#xff01;今天这套方案完美实现&#xff1a; ✅ 飞牛NAS…

【Python】第一弹:对 Python 的认知

目录 一、Python 的背景 1.1. Python 的由来 1.2 Python 的作用 1.3 Python 的优缺点 1.4 Python 的开发工具 一、Python 的背景 1.1. Python 的由来 Python 由荷兰数学和计算机科学研究学会的吉多・范罗苏姆 &#xff08;Guido van Rossum&#xff09;在 20 世纪 80 年代…

直播预告 | 聚焦芯必达|打造可靠高效的国产 MCU 与智能 SBC 汽车解决方案

随着汽车电子国产化快速推进&#xff0c;车规级 MCU 与 CAN/LIN SBC 作为车身控制的核心组件&#xff0c;正面临更高的安全与可靠性挑战。品佳集团将携手芯必达微电子&#xff0c;深入剖析国产 MCU/SBC/智能 SBC 的最新技术与应用&#xff0c;助力企业打造高性能、可量产的国产…

Java源码中有哪些细节可以参考?(持续更新)

欢迎来到啾啾的博客&#x1f431;。 记录学习点滴。分享工作思考和实用技巧&#xff0c;偶尔也分享一些杂谈&#x1f4ac;。 有很多很多不足的地方&#xff0c;欢迎评论交流&#xff0c;感谢您的阅读和评论&#x1f604;。 目录 String的比较final的使用transient避免序列化 St…

GelSight Mini触觉传感器:7μm精度+3D 映射,赋能具身智能精密操作

GelSight Mini 高分辨率视触觉传感器采用先进的光学成像与触觉感知技术&#xff0c;赋予机器人接近人类的触觉能力。该设备可捕捉物体表面微观细节&#xff0c;并生成高精度的2D/3D数字映射&#xff0c;帮助机器人识别形状、纹理及接触力&#xff0c;从而执行更复杂、精准的操作…