SpringBoot+pdfbox實(shí)現(xiàn)解析pdf中的段落和表格數(shù)據(jù)
一、前言
在日常業(yè)務(wù)需求中,往往會(huì)遇到解析pdf文件中的段落或者表格數(shù)據(jù)的需求。
常見的做法是使用 pdfbox 來(lái)做,但是它只能提取文本數(shù)據(jù),沒(méi)有我們?cè)谖募?yè)面上面的那種結(jié)構(gòu)化組織,文本通常是散亂的包含各種換行回車空格等格式,因而它適合做一些段落文本提取。
而 tabula 在 pdfbox 的基礎(chǔ)上做了表格的特殊處理,能夠直接讀取到單元格中的內(nèi)容,但是它處理的前提是表格必須常規(guī)完整邊框的表格,只有部分邊框或者無(wú)邊框的這種結(jié)構(gòu)化數(shù)據(jù)還是束手無(wú)策。
針對(duì)上述情況,筆者實(shí)現(xiàn)了有邊框和無(wú)邊框表格的數(shù)據(jù)讀取并結(jié)構(gòu)化,也支持段落文本提取。
二、功能實(shí)現(xiàn)
2.1 引入依賴
<!-- PDF解析,內(nèi)含pdfbox -->
<dependency>
<groupId>technology.tabula</groupId>
<artifactId>tabula</artifactId>
<version>1.0.5</version>
</dependency>
2.2 完整邊框表格
- 支持多表格
- 支持分頁(yè)
- 支持跳過(guò)標(biāo)題行
- 支持跳過(guò)標(biāo)題前無(wú)關(guān)行
- 支持生成字段
- 返回完整集合數(shù)據(jù)

代碼實(shí)現(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 是否跳過(guò)表頭行 【連續(xù)分頁(yè)表格可能每頁(yè)有表頭】
* @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 是否跳過(guò)表頭行
* @return
*/
public static List<List<Map<String, String>>> readTableDataFromPdf(InputStream inputStream, String password, boolean skipFirstRow) {
// 按照同一個(gè)表格分組
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();
// 遍歷頁(yè)
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 {
// 第一個(gè) or x軸且列數(shù)相同為同一個(gè)表格
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();
}
}
// 最后一個(gè)特殊處理
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ù)轉(zhuǎn)為映射數(shù)據(jù)
*
* @param tableList 表格列表
* @param fieldList 字段列表
* @param tableHeadRowNum 表格頭行
* @param skipFirstRow 是否跳過(guò)表頭行
* @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);
// 去除換行符后設(shè)置值
String text = cell.getText().replace("\r", "");
data.put(fieldList.get(m), text);
}
dataList.add(data);
}
}
return dataList;
}
/**
* 讀取指定文字中間的文本
*
* @param txt 文本
* @param startStr 開始字符串
* @param endStr 結(jié)束字符串
* @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);
}
}解析結(jié)果

2.3 無(wú)邊框表格
- 支持單表格
- 支持分頁(yè)
- 支持跳過(guò)標(biāo)題行
- 支持生成字段
- 返回完整集合數(shù)據(jù)

代碼實(shí)現(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解決不了的表格,沒(méi)有格子
*
* @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\\流水文件\\中國(guó)建設(shè)銀行.pdf", null);
System.out.println(readTxt(fileTxt, "卡號(hào)/賬號(hào):", "客戶名稱:").trim());
System.out.println(readTxt(fileTxt, "客戶名稱:", "起始日期:").trim());
System.out.println(readTxt(fileTxt, "起始日期:", "結(jié)束日期:").trim());
System.out.println(readTxt(fileTxt, "結(jié)束日期:", "序號(hào)").trim());
List<Map<String, String>> dataList = readTableData(fileTxt, "序號(hào) 摘要 幣別 鈔匯 交易日期 交易金額 賬戶余額 交易地點(diǎn)/附言 對(duì)方賬號(hào)與戶名", "生成時(shí)間:");
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 開始字符串 【一般為標(biāo)題行,字段根據(jù)標(biāo)題行定,***很重要***】
* @param endStr 結(jié)束字符串 【結(jié)束標(biāo)志,如果表格連續(xù)中間沒(méi)有重復(fù)的標(biāo)題行則直接使用表格末尾的結(jié)束標(biāo)志即可,如果表格不連續(xù)每頁(yè)都有標(biāo)題行則使用每頁(yè)的結(jié)束標(biāo)志】
* @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:通過(guò)換行符進(jìn)行分割行,然后根據(jù)空格分割列【如果列中數(shù)據(jù)存在空格則無(wú)法解決】
*
* @param dataStr 待解析的文本
* @param tableHeadTxt 標(biāo)題行文本
* @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];
// 標(biāo)題行跳過(guò)
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) { // 超過(guò)列數(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;
}
}解析結(jié)果

2.4 解析段落

代碼實(shí)現(xiàn)
/**
* 讀取指定文字中間的文本
*
* @param txt 文本
* @param startStr 開始字符串
* @param endStr 結(jié)束字符串
* @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);
}
解析結(jié)果

到此這篇關(guān)于SpringBoot+pdfbox實(shí)現(xiàn)解析pdf中的段落和表格數(shù)據(jù)的文章就介紹到這了,更多相關(guān)SpringBoot解析pdf數(shù)據(jù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何解決報(bào)錯(cuò):java.net.BindException:無(wú)法指定被請(qǐng)求的地址問(wèn)題
在Linux虛擬機(jī)上安裝并啟動(dòng)Tomcat時(shí)遇到啟動(dòng)失敗的問(wèn)題,通過(guò)檢查端口及配置文件未發(fā)現(xiàn)異常,后發(fā)現(xiàn)/etc/hosts文件中缺少localhost的映射,添加后重啟Tomcat成功,Tomcat啟動(dòng)時(shí)會(huì)檢查localhost的IP映射,缺失或錯(cuò)誤都可能導(dǎo)致啟動(dòng)失敗2024-10-10
java 替換docx文件中的字符串方法實(shí)現(xiàn)
這篇文章主要介紹了java 替換docx文件中的字符串方法實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02
servlet簡(jiǎn)單實(shí)現(xiàn)文件下載的方法
這篇文章主要介紹了servlet簡(jiǎn)單實(shí)現(xiàn)文件下載的方法,涉及基于servlet技術(shù)實(shí)現(xiàn)流形式文件傳輸?shù)南嚓P(guān)操作技巧,需要的朋友可以參考下2016-12-12
Java Web項(xiàng)目創(chuàng)建并實(shí)現(xiàn)前后端交互
本文主要介紹了Java Web項(xiàng)目創(chuàng)建并實(shí)現(xiàn)前后端交互,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07
Java?EE實(shí)現(xiàn)用戶后臺(tái)管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java?EE實(shí)現(xiàn)用戶后臺(tái)管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05
IntelliJ IDEA 設(shè)置數(shù)據(jù)庫(kù)連接全局共享的步驟
在日常的軟件開發(fā)工作中,我們經(jīng)常會(huì)遇到需要在多個(gè)項(xiàng)目之間共享同一個(gè)數(shù)據(jù)庫(kù)連接的情況,默認(rèn)情況下,IntelliJ IDEA 中的數(shù)據(jù)庫(kù)連接配置是針對(duì)每個(gè)項(xiàng)目單獨(dú)存儲(chǔ)的,幸運(yùn)的是,IntelliJ IDEA 提供了一種方法來(lái)將數(shù)據(jù)庫(kù)連接配置設(shè)置為全局共享,從而簡(jiǎn)化這一過(guò)程2024-10-10
Web容器啟動(dòng)過(guò)程中如何執(zhí)行Java類
這篇文章主要介紹了Web容器啟動(dòng)過(guò)程中如何執(zhí)行Java類,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10
Java練習(xí)題之實(shí)現(xiàn)平方根(sqrt)函數(shù)
這篇文章主要介紹了Java練習(xí)題之實(shí)現(xiàn)平方根(sqrt)函數(shù)的相關(guān)資料,平方根是一個(gè)數(shù)學(xué)概念,表示一個(gè)數(shù)的正平方根,文中通過(guò)代碼和圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-07-07
SpringBoot+layuimini實(shí)現(xiàn)左側(cè)菜單動(dòng)態(tài)展示的示例代碼
Layuimini是Layui的升級(jí)版,它是專業(yè)做后臺(tái)頁(yè)面的框架,而且是適合PC端和移動(dòng)端,以下地址可以在PC端顯示,也可以在手機(jī)上顯示,只不過(guò)會(huì)做自適應(yīng),本文將給大家介紹了SpringBoot+layuimini實(shí)現(xiàn)左側(cè)菜單動(dòng)態(tài)展示的方法,需要的朋友可以參考下2024-04-04
java前后端使用ajax數(shù)據(jù)交互問(wèn)題(簡(jiǎn)單demo)
這篇文章主要介紹了java前后端使用ajax數(shù)據(jù)交互問(wèn)題(簡(jiǎn)單demo),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。2023-06-06

