SpringBoot+pdfbox實現(xiàn)解析pdf中的段落和表格數(shù)據(jù)
一、前言
在日常業(yè)務需求中,往往會遇到解析pdf文件中的段落或者表格數(shù)據(jù)的需求。
常見的做法是使用 pdfbox 來做,但是它只能提取文本數(shù)據(jù),沒有我們在文件頁面上面的那種結構化組織,文本通常是散亂的包含各種換行回車空格等格式,因而它適合做一些段落文本提取。
而 tabula 在 pdfbox 的基礎上做了表格的特殊處理,能夠直接讀取到單元格中的內(nèi)容,但是它處理的前提是表格必須常規(guī)完整邊框的表格,只有部分邊框或者無邊框的這種結構化數(shù)據(jù)還是束手無策。
針對上述情況,筆者實現(xiàn)了有邊框和無邊框表格的數(shù)據(jù)讀取并結構化,也支持段落文本提取。
二、功能實現(xiàn)
2.1 引入依賴
<!-- PDF解析,內(nèi)含pdfbox --> <dependency> <groupId>technology.tabula</groupId> <artifactId>tabula</artifactId> <version>1.0.5</version> </dependency>
2.2 完整邊框表格
- 支持多表格
- 支持分頁
- 支持跳過標題行
- 支持跳過標題前無關行
- 支持生成字段
- 返回完整集合數(shù)據(jù)
代碼實現(xiàn)
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:適合解析純文本、解析表格數(shù)據(jù) * * @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的文本數(shù)據(jù) * * @param filePath 文件路徑 * @param password 文件密碼 * @return */ public static String readTxtFromPdf(String filePath, String password) throws FileNotFoundException { return readTxtFromPdf(new FileInputStream(filePath), password); } /** * 解析pdf的文本數(shù)據(jù) * * @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的表格數(shù)據(jù) * * @param filePath 文件路徑 * @param password 文件密碼 * @param skipFirstRow 是否跳過表頭行 【連續(xù)分頁表格可能每頁有表頭】 * @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的表格數(shù)據(jù) * * @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軸且列數(shù)相同為同一個表格 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; } /** * 將表格數(shù)據(jù)轉為映射數(shù)據(jù) * * @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.3 無邊框表格
- 支持單表格
- 支持分頁
- 支持跳過標題行
- 支持生成字段
- 返回完整集合數(shù)據(jù)
代碼實現(xiàn)
package com.qiangesoft.pdf.util; import com.alibaba.fastjson.JSONObject; import org.springframework.util.CollectionUtils; import java.io.IOException; import java.util.*; /** * pdf規(guī)則數(shù)據(jù)分析工具類 * 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); } /** * 解析表格數(shù)據(jù) * * @param fileTxt 文本數(shù)據(jù) * @param startStr 開始字符串 【一般為標題行,字段根據(jù)標題行定,***很重要***】 * @param endStr 結束字符串 【結束標志,如果表格連續(xù)中間沒有重復的標題行則直接使用表格末尾的結束標志即可,如果表格不連續(xù)每頁都有標題行則使用每頁的結束標志】 * @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的文本數(shù)據(jù) * ps:通過換行符進行分割行,然后根據(jù)空格分割列【如果列中數(shù)據(jù)存在空格則無法解決】 * * @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) { // 不足列數(shù) // 拼接本行 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) { // 超過列數(shù) 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) { // 本行不足列數(shù) // 拼接本行 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 { // 本行大于等于列數(shù) 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 { // 等于列數(shù) 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.4 解析段落
代碼實現(xiàn)
/** * 讀取指定文字中間的文本 * * @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); }
解析結果
到此這篇關于SpringBoot+pdfbox實現(xiàn)解析pdf中的段落和表格數(shù)據(jù)的文章就介紹到這了,更多相關SpringBoot解析pdf數(shù)據(jù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
如何解決報錯:java.net.BindException:無法指定被請求的地址問題
在Linux虛擬機上安裝并啟動Tomcat時遇到啟動失敗的問題,通過檢查端口及配置文件未發(fā)現(xiàn)異常,后發(fā)現(xiàn)/etc/hosts文件中缺少localhost的映射,添加后重啟Tomcat成功,Tomcat啟動時會檢查localhost的IP映射,缺失或錯誤都可能導致啟動失敗2024-10-10Java Web項目創(chuàng)建并實現(xiàn)前后端交互
本文主要介紹了Java Web項目創(chuàng)建并實現(xiàn)前后端交互,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-07-07IntelliJ IDEA 設置數(shù)據(jù)庫連接全局共享的步驟
在日常的軟件開發(fā)工作中,我們經(jīng)常會遇到需要在多個項目之間共享同一個數(shù)據(jù)庫連接的情況,默認情況下,IntelliJ IDEA 中的數(shù)據(jù)庫連接配置是針對每個項目單獨存儲的,幸運的是,IntelliJ IDEA 提供了一種方法來將數(shù)據(jù)庫連接配置設置為全局共享,從而簡化這一過程2024-10-10Java練習題之實現(xiàn)平方根(sqrt)函數(shù)
這篇文章主要介紹了Java練習題之實現(xiàn)平方根(sqrt)函數(shù)的相關資料,平方根是一個數(shù)學概念,表示一個數(shù)的正平方根,文中通過代碼和圖文介紹的非常詳細,需要的朋友可以參考下2023-07-07SpringBoot+layuimini實現(xiàn)左側菜單動態(tài)展示的示例代碼
Layuimini是Layui的升級版,它是專業(yè)做后臺頁面的框架,而且是適合PC端和移動端,以下地址可以在PC端顯示,也可以在手機上顯示,只不過會做自適應,本文將給大家介紹了SpringBoot+layuimini實現(xiàn)左側菜單動態(tài)展示的方法,需要的朋友可以參考下2024-04-04java前后端使用ajax數(shù)據(jù)交互問題(簡單demo)
這篇文章主要介紹了java前后端使用ajax數(shù)據(jù)交互問題(簡單demo),具有很好的參考價值,希望對大家有所幫助。2023-06-06