java利用Tabula實現(xiàn)對PDF內(nèi)表格數(shù)據(jù)提取
某天項目組來了個需求說需要提取 PDF 文件中數(shù)據(jù)作為數(shù)據(jù)沉淀使用,這是因為第三方系統(tǒng)不提供數(shù)據(jù)接口所以只能夠出此下策。
就據(jù)我所知,PDF 文件內(nèi)數(shù)據(jù)提取目前有 3 種解決方案:
第一種,資金足夠的話可以直接通過人工智能對 PDF 內(nèi)容進行解析,按照你需要的規(guī)格數(shù)據(jù)進行輸出即可;
第二種,采用 OCR 識別技術(shù)對內(nèi)容進行提??;
第三種,通過工具實現(xiàn)(也是我將為您呈現(xiàn)的)。在開源社區(qū)中 PDFbox 人氣很高,文字的識別率也很不錯,但是對于表格支持不太友好,涉及到表格數(shù)據(jù)提取的我選用了 Tabula 來實現(xiàn);
Tabula 是什么
Tabula是一個開源工具,用于從PDF文檔中提取表格數(shù)據(jù)。它的主要技術(shù)包括:
- PDF 解析:Tabula 使用 Java 的 PDFBox 庫來解析 PDF 文檔的內(nèi)容和布局。它可以定位到每個頁的文本塊和圖像的坐標(biāo);
- 表格識別:Tabula 通過分析頁面上的線條和文本塊的布局來識別表格的結(jié)構(gòu)。它會查找垂直和水平的線條作為列和行的分隔符;
- 單元格提取:在確定了表格的結(jié)構(gòu)后,Tabula 會分析每個單元格對應(yīng)的文本塊,并提取出單元格中的文本內(nèi)容;
- 數(shù)據(jù)整理:Tabula 會嘗試自動整理從表格中提取的數(shù)據(jù),例如:縱向和橫向合并單元格,處理跨頁的表格等。它也會執(zhí)行一定的文本清理;
- 導(dǎo)出格式:Tabula 支持將提取出來的數(shù)據(jù)導(dǎo)出為 CSV 和 JSON 格式。用戶可以導(dǎo)入到 Excel 等其他工具中進行后續(xù)分析。
- 優(yōu)化算法:Tabula 在表格分析和數(shù)據(jù)提取方面使用了一些優(yōu)化的算法和啟發(fā)式規(guī)則,以提高正確率。同時它也提供了交互式的編輯接口供用戶校正結(jié)果。
怎么用 Tabula
首先肯定是引入 pom 文件依賴,如下圖:
<dependency> <groupId>technology.tabula</groupId> <artifactId>tabula</artifactId> <version>1.0.5</version> </dependency>
接著就可以創(chuàng)建 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 特殊回調(diào)處理 * @return {*} */ private static JSONArray parsePdfTable(String pdfPath, int customStart, PdfCellCustomProcess callback) { JSONArray reJsonArr = new JSONArray(); // 存儲解析后的JSON數(shù)組 try (PDDocument document = PDDocument.load(new File(pdfPath))) { PageIterator pi = new ObjectExtractor(document).extract(); // 獲取頁面迭代器 // 遍歷所有頁面 while (pi.hasNext()) { Page page = pi.next(); // 獲取當(dāng)前頁 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數(shù)組 } ... }
這里我們先按照官網(wǎng)樣例代碼來實現(xiàn) pdf 表格解析先。大致的思路就是:
- 創(chuàng)建一個空的 JSONArray 對象 reJsonArr ,用于存儲解析后的表格數(shù)據(jù);
- 使用 PDDocument.load 方法加載指定路徑的 PDF 文件,并使用 try-with-resources 語句創(chuàng)建一個 PDDocument 對象 document ;
- 使用 ObjectExtractor 從 document 中提取頁面迭代器 pi ;
- 使用 while 循環(huán)遍歷每個頁面,使用 pi.hasNext 方法判斷是否還有下一個頁面,如果有則進入循環(huán);
- 使用 pi.next 方法獲取當(dāng)前頁面對象 page ;
- 使用 SPREADSHEEET_EXTRACTION_ALGORITHM 解析 page 中的所有表格,并將結(jié)果存儲在 tableList 中;
- 使用 for 循環(huán)遍歷 tableList 中的每個表格,對于每個表格執(zhí)行以下操作: a. 使用 table.getRows 方法獲取表格中的每一行,并將結(jié)果存儲在 rowList 中; b. 使用 for 循環(huán)遍歷 rowList 中的每一行,從 customStart 位置開始,對于每一行執(zhí)行以下操作: i. 使用 rowList.get 方法獲取行中的每個單元格,并將結(jié)果存儲在 cellList 中; ii. 將 cellList 、 rowIndex 和 reJsonArr 作為參數(shù)傳遞給回調(diào)函數(shù) callback 的 handler 方法進行處理;
- 使用 try-catch 語句捕獲可能發(fā)生的 IOException 異常,并記錄錯誤信息;
- 使用 finally 語句移除 THREAD_LOCAL 中的數(shù)據(jù);
- 返回解析后的 JSONArray 對象 reJsonArr ;
這里要加上一個 callback.handler 回調(diào)函數(shù)主要的目的是為了將“單元格操作”跟 pdf 解析兩部分代碼進行解耦,那么這個回調(diào)接口的接口定義如下:
@FunctionalInterface public interface PdfCellCustomProcess { /** * @description: 自定義單元格回調(diào)處理 * @return {*} */ void handler(List<RectangularTextContainer> cellList, int rowIndex, JSONArray reJsonArr); }
其中 cellList 傳入的是這一行的所有單元格的集合,rowIndex 傳入的是當(dāng)前行碼,reJsonArr 是返回值。具體的實現(xiàn)代碼如下:
public class PdfUtil { ... /** * @description: 解析 pdf 中簡單的表格并返回 json 數(shù)組 * @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(); // 遍歷單元格獲取每個單元格內(nèi)字段內(nèi)容 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 表達式,它作為參數(shù)傳遞給 parsePdfTable 方法。Lambda 表達式做了PdfCellCustomProcess 接口的實現(xiàn)。Lambda 表達式的代碼塊首先創(chuàng)建一個 JSONObject 對象,然后遍歷單元格列表,獲取每個單元格的文本內(nèi)容。
如果當(dāng)前行索引等于自定義起始行索引,將文本內(nèi)容添加到 headList 列表中;否則,將文本內(nèi)容作為鍵值對添加到j(luò)sonObj 對象中。最后,如果 jsonObj 對象不為空,則將其添加到 reArr 數(shù)組中。 代碼還包含了一些其他操作。如果當(dāng)前行索引等于自定義起始行索引,將 headList 列表設(shè)置為 THREAD_LOCAL 線程局部變量。最后,返回 reArr數(shù)組作為方法的結(jié)果。
最后只需要補上 main 方法調(diào)用即可獲取到解析后的 JsonArray 集合。但是直接輸出 JsonArray 數(shù)據(jù)并不直觀,于是我又寫了一個解析 JsonArray 數(shù)據(jù)的方法,并將里面的數(shù)據(jù)轉(zhuǎn)換為 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(); }
這個應(yīng)該比較好理解吧,這里就不再詳述了。
以上的代碼對于一般的 PDF 表格解析是基本沒有問題的,但是對于帶有合并單元格的解析就不能滿足了。合并單元格需要考慮橫向合并、縱向合并和混合合并三種合并模式,不是說 tabula-java 的 sdk 不能做只是比較麻煩,在 tabula-java 方案中我們可以獲取到單元格的高和寬,那么先做一次全遍歷獲取二維數(shù)組對于單元格定位后,根據(jù)高和寬進行虛擬表格的建設(shè),最后根據(jù)二維數(shù)組對數(shù)據(jù)進行回填即可。這也是用回調(diào)將單元格操作分離的原因之一,為了后面做合并單元格解析做準(zhǔn)備的。
但其實上面說這么多,合并單元格解析的代碼我還沒寫呢(以上都是我吹的),等完成后再給大家分享。
到此這篇關(guān)于java利用Tabula實現(xiàn)對PDF內(nèi)表格數(shù)據(jù)提取的文章就介紹到這了,更多相關(guān)java PDF提取數(shù)據(jù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot實現(xiàn)Read Through模式的操作過程
Read Through模式通常是指一種緩存策略,其中當(dāng)應(yīng)用程序嘗試讀取數(shù)據(jù)時,緩存系統(tǒng)首先被檢查以查看數(shù)據(jù)是否已經(jīng)存在于緩存中,這篇文章主要介紹了SpringBoot實現(xiàn)Read Through模式,需要的朋友可以參考下2024-07-07Java?RabbitMQ的持久化和發(fā)布確認(rèn)詳解
這篇文章主要為大家詳細介紹了RabbitMQ的持久化和發(fā)布確認(rèn),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-03-03SpringBoot mybatis 實現(xiàn)多級樹形菜單的示例代碼
這篇文章主要介紹了SpringBoot mybatis 實現(xiàn)多級樹形菜單的示例代碼,代碼簡單易懂,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-05-05mybatis中association和collection的使用與區(qū)別
在 MyBatis 中,<association>?和?<collection>?是用于配置結(jié)果映射中關(guān)聯(lián)關(guān)系的兩個元素,本文主要介紹了mybatis中<association>和<collection>的使用與區(qū)別,具有一定的參考價值,感興趣的可以了解一下2024-01-01java抓取鼠標(biāo)事件和鼠標(biāo)滾輪事件示例
這篇文章主要介紹了java抓取鼠標(biāo)事件和鼠標(biāo)滾輪事件示例,需要的朋友可以參考下2014-05-05spring cloud config 配置中心快速實現(xiàn)過程解析
這篇文章主要介紹了spring cloud config 配置中心快速實現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-08-08