当前位置: 首页 > news >正文

【Java】采用 Tabula 技术对 PDF 文件内表格进行数据提取

某天项目组来了个需求说需要提取 PDF 文件中数据作为数据沉淀使用,这是因为第三方系统不提供数据接口所以只能够出此下策。

就据我所知,PDF 文件内数据提取目前有 3 种解决方案:

第一种,资金足够的话可以直接通过人工智能对 PDF 内容进行解析,按照你需要的规格数据进行输出即可;

第二种,采用 OCR 识别技术对内容进行提取;

第三种,通过工具实现(也是我将为您呈现的)。在开源社区中 PDFbox 人气很高,文字的识别率也很不错,但是对于表格支持不太友好,涉及到表格数据提取的我选用了 Tabula 来实现;

Tabula 是什么?

Tabula是一个开源工具,用于从PDF文档中提取表格数据。它的主要技术包括:

  1. PDF 解析:Tabula 使用 Java 的 PDFBox 库来解析 PDF 文档的内容和布局。它可以定位到每个页的文本块和图像的坐标;
  2. 表格识别:Tabula 通过分析页面上的线条和文本块的布局来识别表格的结构。它会查找垂直和水平的线条作为列和行的分隔符;
  3. 单元格提取:在确定了表格的结构后,Tabula 会分析每个单元格对应的文本块,并提取出单元格中的文本内容;
  4. 数据整理:Tabula 会尝试自动整理从表格中提取的数据,例如:纵向和横向合并单元格,处理跨页的表格等。它也会执行一定的文本清理;
  5. 导出格式:Tabula 支持将提取出来的数据导出为 CSV 和 JSON 格式。用户可以导入到 Excel 等其他工具中进行后续分析。
  6. 优化算法: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 表格解析先。大致的思路就是:

  1. 创建一个空的 JSONArray 对象 reJsonArr ,用于存储解析后的表格数据;
  2. 使用 PDDocument.load 方法加载指定路径的 PDF 文件,并使用 try-with-resources 语句创建一个 PDDocument 对象 document ;
  3. 使用 ObjectExtractor 从 document 中提取页面迭代器 pi ;
  4. 使用 while 循环遍历每个页面,使用 pi.hasNext 方法判断是否还有下一个页面,如果有则进入循环;
  5. 使用 pi.next 方法获取当前页面对象 page ;
  6. 使用 SPREADSHEEET_EXTRACTION_ALGORITHM 解析 page 中的所有表格,并将结果存储在 tableList 中;
  7. 使用 for 循环遍历 tableList 中的每个表格,对于每个表格执行以下操作:
    a. 使用 table.getRows 方法获取表格中的每一行,并将结果存储在 rowList 中;
    b. 使用 for 循环遍历 rowList 中的每一行,从 customStart 位置开始,对于每一行执行以下操作:
    i. 使用 rowList.get 方法获取行中的每个单元格,并将结果存储在 cellList 中;
    ii. 将 cellList 、 rowIndex 和 reJsonArr 作为参数传递给回调函数 callback 的 handler 方法进行处理;
  8. 使用 try-catch 语句捕获可能发生的 IOException 异常,并记录错误信息;
  9. 使用 finally 语句移除 THREAD_LOCAL 中的数据;
  10. 返回解析后的 JSONArray 对象 reJsonArr ;

这里要加上一个 callback.handler 回调函数主要的目的是为了将“单元格操作”跟 pdf 解析两部分代码进行解耦,那么这个回调接口的接口定义如下:

@FunctionalInterface
public 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 方案中我们可以获取到单元格的高和宽,那么先做一次全遍历获取二维数组对于单元格定位后,根据高和宽进行虚拟表格的建设,最后根据二维数组对数据进行回填即可。这也是用回调将单元格操作分离的原因之一,为了后面做合并单元格解析做准备的。

但其实上面说这么多,合并单元格解析的代码我还没写呢(以上都是我吹的),等完成后再给大家分享。

相关文章:

【Java】采用 Tabula 技术对 PDF 文件内表格进行数据提取

某天项目组来了个需求说需要提取 PDF 文件中数据作为数据沉淀使用&#xff0c;这是因为第三方系统不提供数据接口所以只能够出此下策。 就据我所知&#xff0c;PDF 文件内数据提取目前有 3 种解决方案&#xff1a; 第一种&#xff0c;资金足够的话可以直接通过人工智能对 PDF…...

完全保密的以太坊交易:Aztec网络的隐私架构

1. 引言 Aztec为隐私优先的以太坊zkRollup&#xff1a;即其为具有完全隐私保护的L2。 为了理解私有交易的范式变化性质&#xff0c;以及为什么将隐私直接构建到网络架构中很重要&#xff0c;必须首先讨论为什么以太坊不是私有的。 2. 以太坊&#xff1a;公有链 以太坊为具有…...

初识Java 9-1 内部类

目录 创建内部类 到外部类的链接 使用.this和.new 内部类和向上转型 在方法和作用域中的内部类 匿名内部类 嵌套类 接口中的类 从多嵌套的内部类中访问外部人员 本笔记参考自&#xff1a; 《On Java 中文版》 定义在另一个类中的类称为内部类。利用内部类&#xff0c;…...

合宙Air724UG LuatOS-Air LVGL API控件-屏幕横屏竖屏切换(Rotation)

屏幕横屏竖屏切换(Rotation) lvgl.disp_set_rotation(nil, lvgl.DISP_ROT_angle) 屏幕横屏竖屏切换显示&#xff0c;core版本号要>3202参数 参数类型释义取值nil无意义nilangle显示角度0,90,270,360 返回值nil 例子 lvgl.init()- -初始化 lvgl.disp_set_rotation(nil,…...

在Unity中,Instantiate函数用于在场景中创建一个新的游戏对象实例

在Unity中&#xff0c;Instantiate函数用于在场景中创建一个新的游戏对象实例。它的语法如下所示&#xff1a; public static Object Instantiate(Object original, Vector3 position, Quaternion rotation); original&#xff1a;要实例化的原始游戏对象。position&#xff1…...

解决 tesserocr报错 Failed to init API, possibly an invalid tessdata path : ./

问题描述 我们在初次使用tesserocr库的时候&#xff0c;可能会报以下错误&#xff1a; RuntimeError: Failed to init API, possibly an invalid tessdata path: ./ 这是因为我们在使用 conda 创建的环境中找不到"tessdata"这个文件夹。 解决办法 这时候把Tessera…...

使用Python CV2融合人脸到新图片--优化版

优化说明 上一版本人脸跟奥特曼图片合并后边界感很严重&#xff0c;于是查找资料发现CV2还有一个泊松函数很适合融合图像。具体代码如下&#xff1a; import numpy as np import cv2usrFilePath "newpic22.jpg" atmFilePath "atm2.jpg" src cv2.imrea…...

Python分享之对象的属性

Python一切皆对象(object)&#xff0c;每个对象都可能有多个属性(attribute)。Python的属性有一套统一的管理方案。 属性的__dict__系统 对象的属性可能来自于其类定义&#xff0c;叫做类属性(class attribute)。类属性可能来自类定义自身&#xff0c;也可能根据类定义继承来的…...

编程参考 - std::exchange和std::swap的区别

这两个功能是C standard library中的Standard template library中的一部分。容易混淆&#xff0c;我们来看下它们的区别。 exchange&#xff1a; 这个函数是一个返回原先值的set函数。 std::exchange is a setter returning the old value. int z std::exchange(x, y); Af…...

Sentinel整合RestTemplate

resttemplate开启sentinel保护配置resttemplate.sentinel.enabledtrue配置sentinel-dashboard地址spring.cloud.sentinel.transport.dashboardlocalhost:8858\ spring.cloud.sentinel.transport.dashboard.port8739 实例化RestTemplate并加入SentinelRestTemplate注解Configura…...

微前端学习(下)

一、课程目标 qiankun 整体运行流程微前端实现方案二、课程大纲 qiankun 整体流程微前端方案实现DIY微前端核心能力1、微前端方案实现 基于 iframe 完全隔离的方案、使用纯的Web Components构建应用EMP基于webpack module federationqiankun、icestark 自己实现JS以及样式隔离2…...

Android Splash实现

1、创建Activity package com.wsy.knowledge.ui.splashimport android.animation.Animator import android.animation.AnimatorListenerAdapter import android.annotation.SuppressLint import android.os.Build import android.os.Looper import android.util.Log import an…...

FPGA projet : VGA

在vga屏幕上显示 &#xff1a; 野火科技 相比于上个工程&#xff0c;只需要修改 vga_pix 模块即可。 注意存储器类型变量的定义&#xff1a;reg 【宽度】<名称>【深度】 赋值 always &#xff08;poseedge vga_clk&#xff09;begin 为每一行赋值&#xff0c;不可位赋…...

JDK8 升级至JDK19

优质博文IT-BLOG-CN 目前部分项目使用JDK8&#xff0c;部分项目使用JDK19因此&#xff0c;环境变量中还是保持JDK8&#xff0c;只需要下载JDK19免安装版本&#xff0c;通过配置IDEA就可以完成本地开发。 一、IDEA 环境设置 【1】通过快捷键CTRL SHIFT ALT S或者File->P…...

Python3.10 IDLE更换主题

前言 自定义主题网上有很多&#xff0c;3.10IDLE的UI有一些新的东西&#xff0c;直接扣过来会有些地方覆盖不到&#xff0c;需要自己测试着添几行配置&#xff0c;以下做个记录。 配置文件路径 Python安装目录下的Lib\idlelib\config-highlight.def。如果是默认安装&#xf…...

C# OpenVino Yolov8 Pose 姿态识别

效果 项目 代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using OpenCvSharp;namespace OpenVino_Yolov8_Demo {public…...

北邮22级信通院数电:Verilog-FPGA(1)实验一“跑通第一个例程” 过程中遇到的常见问题与解决方案汇总(持续更新中)

北邮22信通一枚~ 跟随课程进度更新北邮信通院数字系统设计的笔记、代码和文章 持续关注作者 迎接数电实验学习~ 获取更多文章&#xff0c;请访问专栏&#xff1a; 北邮22级信通院数电实验_青山如墨雨如画的博客-CSDN博客 目录 问题一&#xff1a;Verilog代码没有跑通 报…...

CSS - 鼠标移入整行高亮显示,适用于会员套餐各参数对比页面(display: table,div 转表格形式)

效果图 可根据基础示例和进阶示例&#xff0c;复制进行改造样式。 如下图所示&#xff0c;本文提供 2 个示例。 基础示例 找个 HTML 页面&#xff0c;一键复制运行。 <body><h1 style"text-align: center;">基础示例</h1><section class"…...

无涯教程-JavaScript - ATAN2函数

描述 The ATAN2 function returns the arctangent, or inverse tangent, of the specified x- and ycoordinates, in radians, between -π/2 and π/2. 语法 ATAN2 (x_num, y_num)争论 Argument描述Required/OptionalX_numThe x-coordinate of the point.RequiredY_numThe…...

Tomcat 下部署 jFinal

1、检查web.xml 配置&#xff0c;在 tomcat 下部署需要检查 web.xml 是否存在&#xff0c;并且要确保配置正确&#xff0c;配置格式如下。 <?xml version"1.0" encoding"UTF-8"?> <web-app xmlns:xsi"http://www.w3.org/2001/XMLSchema-i…...

树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法

树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源&#xff1a; http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作&#xff0c;无需更改相机配置。但是&#xff0c;一…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容

基于 ​UniApp + WebSocket​实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配​微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...

Python爬虫实战:研究feedparser库相关技术

1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...

CentOS下的分布式内存计算Spark环境部署

一、Spark 核心架构与应用场景 1.1 分布式计算引擎的核心优势 Spark 是基于内存的分布式计算框架&#xff0c;相比 MapReduce 具有以下核心优势&#xff1a; 内存计算&#xff1a;数据可常驻内存&#xff0c;迭代计算性能提升 10-100 倍&#xff08;文档段落&#xff1a;3-79…...

多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验

一、多模态商品数据接口的技术架构 &#xff08;一&#xff09;多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如&#xff0c;当用户上传一张“蓝色连衣裙”的图片时&#xff0c;接口可自动提取图像中的颜色&#xff08;RGB值&…...

Java多线程实现之Callable接口深度解析

Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...

AI,如何重构理解、匹配与决策?

AI 时代&#xff0c;我们如何理解消费&#xff1f; 作者&#xff5c;王彬 封面&#xff5c;Unplash 人们通过信息理解世界。 曾几何时&#xff0c;PC 与移动互联网重塑了人们的购物路径&#xff1a;信息变得唾手可得&#xff0c;商品决策变得高度依赖内容。 但 AI 时代的来…...

代码随想录刷题day30

1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币&#xff0c;另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额&#xff0c;返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...

R语言速释制剂QBD解决方案之三

本文是《Quality by Design for ANDAs: An Example for Immediate-Release Dosage Forms》第一个处方的R语言解决方案。 第一个处方研究评估原料药粒径分布、MCC/Lactose比例、崩解剂用量对制剂CQAs的影响。 第二处方研究用于理解颗粒外加硬脂酸镁和滑石粉对片剂质量和可生产…...

并发编程 - go版

1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程&#xff0c;系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...