SpringBoot+tabula+pdfbox解析pdf中的段落和表格數據

一、前言

在日常業務需求中,往往會遇到解析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.pswp.cn/pingmian/82830.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/82830.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/82830.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【Elasticsearch】stored_fields

在 Elasticsearch 中&#xff0c;stored_fields 是一個非常重要的概念&#xff0c;主要用于控制文檔存儲和檢索時的行為。以下是對 stored_fields 的詳細解釋&#xff1a; 1\. stored_fields 的作用 stored_fields 用于指定在檢索文檔時需要返回的字段。默認情況下&#xff0c;…

計算機網絡 | 1.1 計算機網絡概述思維導圖

附大綱&#xff1a; 計算機網絡的概念 一個通過通信設備與線路把不同計算機系統連接起來&#xff0c;實現資源共享和信息傳遞的系統 計算機網絡的組成 從組成成分上 硬件&#xff1a;主機、通信鏈路、交換設備、通信處理機軟件&#xff1a;網絡操作系統、聊天軟件等協議&…

HOW - 簡歷和求職面試寶典(三)

文章目錄 1. 面試邀約2. 開始面試和自我介紹第一、面試前的準備工作第二、如何全面地介紹自己1. 面試邀約 第一、先認識日常HR 的工作流程 首先,電話溝通是 HR 核心工作內容的一部分。電話溝通分為兩種:一種是電話預約;另外一種是電話確認。 電話預約很清晰,就是確認面試…

Java基礎 Day24

一、進程和線程 1、進程 &#xff08;1&#xff09;概念 進程 (Process) 是計算機中的程序關于某數據集合上的一次運行活動 是系統進行資源分配的基本單位 簡單理解&#xff1a;程序的執行過程&#xff08;正在運行的應用程序&#xff09; &#xff08;2&#xff09;特性…

C#學習:基于LLM的簡歷評估程序

前言 在pocketflow的例子中看到了一個基于LLM的簡歷評估程序的例子&#xff0c;感覺還挺好玩的&#xff0c;為了練習一下C#&#xff0c;我最近使用C#重寫了一個。 準備不同的簡歷&#xff1a; 查看效果&#xff1a; 不足之處是現實的簡歷應該是pdf格式的&#xff0c;后面可以…

git怎么合并兩個分支

git怎么合并分支代碼 注意: 第一步你得把當前分支合到遠程分支去才能有下面的操作 另外我是將develop分支代碼合并到release分支去 git 命令 查看本地所有分支 git branch切換分支 例如切換到release分支 git checkout release拉取代碼 git pull up release 合并分支 …

Android-kotlin協程學習總結

Kotlin協程實戰對話? ?真題1&#xff1a;協程與線程的本質區別是什么&#xff1f;為什么說協程是輕量級的&#xff1f;?? ?面試官?&#xff1a; “我看你項目中用協程替代了線程池&#xff0c;能說說協程和線程的核心區別嗎&#xff1f;為什么協程更適合高并發&#xf…

uni-app學習筆記十四-vue3中emit的使用

在組件傳值中&#xff0c;無論是props還是slot都是單向數據流&#xff0c;父組件向子組件傳值&#xff0c;子組件不能直接對父組件傳過來的值進行重新賦值。 下面學習子組件向父組件傳值的工具--emit。 在子組件emit設置傳遞的函數名和值 <template><view>子組件…

Java設計模式從基礎到實際運用

第一部分&#xff1a;設計模式基礎 1. 設計模式概述 設計模式(Design Pattern)是一套被反復使用、多數人知曉的、經過分類編目的代碼設計經驗的總結&#xff0c;它描述了在軟件設計過程中一些不斷重復出現的問題以及該問題的解決方案。設計模式是在特定環境下解決軟件設計問題…

鴻蒙OSUniApp 制作自定義的進度條組件#三方框架 #Uniapp

使用 UniApp 制作自定義的進度條組件 在移動應用開發中&#xff0c;進度條是非常常見的 UI 組件&#xff0c;無論是文件上傳、下載、任務進度還是表單填寫反饋&#xff0c;進度條都能為用戶提供直觀的進度提示。雖然 UniApp 提供了一些基礎的進度條能力&#xff0c;但在實際項…

Python爬蟲實戰:研究Beautiful Soup框架相關技術

1. 引言 1.1 研究背景與意義 隨著互聯網的快速發展,網絡上的數據量呈爆炸式增長。如何從海量的網頁數據中高效提取有價值的信息,成為信息科學領域的重要研究課題。網絡爬蟲作為一種自動獲取網頁內容的技術,能夠按照預設規則遍歷互聯網并采集數據,為信息檢索、輿情分析、商…

【Tips】關于PCI和PCIe的配置空間差異和io/memory io讀寫

最近在看同事2023年講的PCI基礎課&#xff0c;感覺確實是豁然開朗了&#xff0c;贊美同事。 PCIe實際上是PCI的擴展&#xff08;extended&#xff09;&#xff0c;PCIe設備相當于是迭代升級產品。 而PCIe的配置空間基于PCI原有的0xFF&#xff08;256字節&#xff09;配置空間…

桂花網體育運動監測方案:開啟幼兒園運動健康管理新篇章

在幼兒教育領域&#xff0c;運動能力的培養與健康監測始終是備受關注的核心環節。隨著科技的飛速發展&#xff0c;如何科學、有效地監測幼兒的運動狀態&#xff0c;成為了幼兒園教育者面臨的一大挑戰。桂花網體育運動監測方案憑借其高效、精準、智能化的特性&#xff0c;為幼兒…

Perforce P4產品簡介:無限擴展+全球協作+安全管控+工具集成(附下載)

本產品簡介由Perforce中國授權合作伙伴——龍智編輯整理&#xff0c;旨在帶您快速了解Perforce P4版本控制系統的強大之處。 世界級無限可擴展的版本控制系統 Perforce P4&#xff08;原Helix Core&#xff09;是業界領先的版本控制平臺&#xff0c;備受19家全球Top20 AAA級游…

pikachu靶場通關筆記08 XSS關卡04-DOM型XSS

目錄 一、XSS原理 二、DOM型XSS 三、源碼分析 1、進入靶場 2、XSS探測 3、源碼分析 四、滲透實戰 1、Payload1 2、Payload2 3、Payload3 本系列為通過《pikachu靶場通關筆記》的XSS關卡(共10關&#xff09;滲透集合&#xff0c;通過對XSS關卡源碼的代碼審計找到XSS風…

安全訪問 std::tuple 的容錯方法及氣象領域應用

安全訪問 std::tuple 的容錯方法及氣象領域應用 1. std::tuple 安全訪問的核心問題 1.1 元組結構性問題&#xff08;編譯時錯誤&#xff09; 當元組元素數量為空時&#xff08;std::tuple<>&#xff09;&#xff0c;任何訪問元素的嘗試都會導致編譯錯誤?&#xff1a;…

Webug4.0靶場通關筆記03- 第3關SQL注入之時間盲注(手注法+腳本法 兩種方法)

目錄 一、源碼分析 1.分析閉合 2.分析輸出 &#xff08;1&#xff09;查詢成功 &#xff08;2&#xff09;查詢失敗 &#xff08;3&#xff09;SQL語句執行報錯 二、第03關 延時注入 1.打開靶場 2.SQL手注 &#xff08;1&#xff09;盲注分析 &#xff08;2&#xf…

【大模型學習網絡互聯】Memory-Mapped I/O MMIO語義與MEM語義

MMIO語義和MEM語義的區別 嗯&#xff0c;用戶要求更詳細地區分MMIO和MEM語義&#xff0c;看來之前的中文對比雖然系統但還不夠深入。用戶可能是嵌入式開發者或OS內核學習者&#xff0c;需要從硬件行為層面理解差異。 用戶連續追問MMIO相關概念&#xff0c;說明ta在接觸底層開發…

如何在使用kickstart安裝物理機操作系統的過程中核對服務器的SN

原因 在使用kickstart對物理機進行重裝的時候&#xff0c;由于自動化配置&#xff0c;掛載鏡像重啟之后就會自動化開始安裝部署&#xff0c;不夠安全&#xff0c;萬一選錯服務器沒有辦法回退。因此可以在kickstart的ks配置文件中新增服務器SN的校驗&#xff0c;當校驗不通過的…

spring4第4課-ioc控制反轉-詳解如何注入參數

堅持住&#xff0c;第四天&#xff0c;繼續學習spring4.詳解如何注入參數 先總結&#xff0c;主要有如下6種&#xff1a; 1&#xff0c;基本類型值&#xff1b; 2&#xff0c;注入 bean&#xff1b; 3&#xff0c;內部 bean&#xff1b; 4&#xff0c;null 值&#xff1b; 5&…