最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

java利用Tabula實現(xiàn)對PDF內(nèi)表格數(shù)據(jù)提取

 更新時間:2023年09月14日 08:28:25   作者:kida_yuan  
Tabula是一個開源工具,用于從PDF文檔中提取表格數(shù)據(jù),下面小編就來和大家詳細(xì)介紹一下java如何通過Tabula對PDF文件內(nèi)表格進(jìn)行數(shù)據(jù)提取吧

某天項目組來了個需求說需要提取 PDF 文件中數(shù)據(jù)作為數(shù)據(jù)沉淀使用,這是因為第三方系統(tǒng)不提供數(shù)據(jù)接口所以只能夠出此下策。

就據(jù)我所知,PDF 文件內(nèi)數(shù)據(jù)提取目前有 3 種解決方案:

第一種,資金足夠的話可以直接通過人工智能對 PDF 內(nèi)容進(jìn)行解析,按照你需要的規(guī)格數(shù)據(jù)進(jìn)行輸出即可;

第二種,采用 OCR 識別技術(shù)對內(nèi)容進(jìn)行提??;

第三種,通過工具實現(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 等其他工具中進(jìn)行后續(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 方法判斷是否還有下一個頁面,如果有則進(jìn)入循環(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 方法進(jìn)行處理;
  • 使用 try-catch 語句捕獲可能發(fā)生的 IOException 異常,并記錄錯誤信息;
  • 使用 finally 語句移除 THREAD_LOCAL 中的數(shù)據(jù);
  • 返回解析后的 JSONArray 對象 reJsonArr ;

這里要加上一個 callback.handler 回調(diào)函數(shù)主要的目的是為了將“單元格操作”跟 pdf 解析兩部分代碼進(jìn)行解耦,那么這個回調(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 表達(dá)式,它作為參數(shù)傳遞給 parsePdfTable 方法。Lambda 表達(dá)式做了PdfCellCustomProcess 接口的實現(xiàn)。Lambda 表達(dá)式的代碼塊首先創(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é)果。

最后只需要補(bǔ)上 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ù)高和寬進(jìn)行虛擬表格的建設(shè),最后根據(jù)二維數(shù)組對數(shù)據(jù)進(jìn)行回填即可。這也是用回調(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)文章

  • Java實戰(zhàn)寵物店在線交易平臺的實現(xiàn)流程

    Java實戰(zhàn)寵物店在線交易平臺的實現(xiàn)流程

    讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+Springboot+maven+Mysql+FreeMarker實現(xiàn)一個寵物在線交易系統(tǒng),大家可以在過程中查缺補(bǔ)漏,提升水平
    2022-01-01
  • java面試題之try中含return語句時代碼的執(zhí)行順序詳解

    java面試題之try中含return語句時代碼的執(zhí)行順序詳解

    這篇文章主要介紹了關(guān)于java中的一道面試題,這套題就是在try中含return語句時代碼的執(zhí)行順序,這個問題看似簡單,卻暗藏殺機(jī)??!文中通過一個個例子詳細(xì)介紹了其中玄機(jī),需要的朋友可以參考學(xué)習(xí),下面來一起看看吧。
    2017-04-04
  • springBoot整合rabbitmq測試常用模型小結(jié)

    springBoot整合rabbitmq測試常用模型小結(jié)

    這篇文章主要介紹了springBoot整合rabbitmq并測試五種常用模型,本文主要針對前五種常用模型,在spirngboot框架的基礎(chǔ)上整合rabbitmq并進(jìn)行測試使用,需要的朋友可以參考下
    2022-01-01
  • 解讀Spring定義Bean的兩種方式:<bean>和@Bean

    解讀Spring定義Bean的兩種方式:<bean>和@Bean

    這篇文章主要介紹了Spring定義Bean的兩種方式:<bean>和@Bean,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-04-04
  • Java 繼承方法實例詳解

    Java 繼承方法實例詳解

    這篇文章主要介紹了Java繼承中方法實例,非常的實用,這里推薦給大家,有需要的小伙伴可以參考下
    2017-04-04
  • 詳解Springboot應(yīng)用啟動以及關(guān)閉時完成某些操作

    詳解Springboot應(yīng)用啟動以及關(guān)閉時完成某些操作

    這篇文章主要介紹了詳解Springboot應(yīng)用啟動以及關(guān)閉時完成某些操作,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-11-11
  • java 如何使用org.w3c.dom操作XML文件

    java 如何使用org.w3c.dom操作XML文件

    這篇文章主要介紹了java 如何使用org.w3c.dom操作XML文件,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-08-08
  • Spring AOP與AspectJ的對比及應(yīng)用詳解

    Spring AOP與AspectJ的對比及應(yīng)用詳解

    這篇文章主要為大家介紹了Spring AOP與AspectJ的對比及應(yīng)用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-02-02
  • SpringBoot如何正確配置并運行Kafka

    SpringBoot如何正確配置并運行Kafka

    這篇文章主要介紹了SpringBoot如何正確配置并運行Kafka問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-07-07
  • java.lang.Long cannot be cast to java.lang.Integer數(shù)據(jù)類型轉(zhuǎn)換異常解決辦法

    java.lang.Long cannot be cast to ja

    本文主要介紹了java.lang.Long cannot be cast to java.lang.Integer數(shù)據(jù)類型轉(zhuǎn)換異常解決辦法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-07-07

最新評論

临高县| 通道| 汝阳县| 托里县| 馆陶县| 四平市| 黎川县| 义乌市| 永安市| 雷州市| 蓝田县| 吴桥县| 长白| 延安市| 阿荣旗| 虹口区| 东乡县| 呼和浩特市| 靖宇县| 玉树县| 通州区| 洪江市| 舒兰市| 澎湖县| 儋州市| 伊金霍洛旗| 黑龙江省| 张家口市| 偏关县| 台南市| 新昌县| 永宁县| 博兴县| 尖扎县| 临高县| 平江县| 永嘉县| 黔江区| 石柱| 五河县| 南澳县|