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

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

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

某天項(xiàng)目組來(lái)了個(gè)需求說(shuō)需要提取 PDF 文件中數(shù)據(jù)作為數(shù)據(jù)沉淀使用,這是因?yàn)榈谌较到y(tǒng)不提供數(shù)據(jù)接口所以只能夠出此下策。

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

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

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

第三種,通過(guò)工具實(shí)現(xiàn)(也是我將為您呈現(xiàn)的)。在開(kāi)源社區(qū)中 PDFbox 人氣很高,文字的識(shí)別率也很不錯(cuò),但是對(duì)于表格支持不太友好,涉及到表格數(shù)據(jù)提取的我選用了 Tabula 來(lái)實(shí)現(xiàn);

Tabula 是什么

Tabula是一個(gè)開(kāi)源工具,用于從PDF文檔中提取表格數(shù)據(jù)。它的主要技術(shù)包括:

  • PDF 解析:Tabula 使用 Java 的 PDFBox 庫(kù)來(lái)解析 PDF 文檔的內(nèi)容和布局。它可以定位到每個(gè)頁(yè)的文本塊和圖像的坐標(biāo);
  • 表格識(shí)別:Tabula 通過(guò)分析頁(yè)面上的線條和文本塊的布局來(lái)識(shí)別表格的結(jié)構(gòu)。它會(huì)查找垂直和水平的線條作為列和行的分隔符;
  • 單元格提?。涸诖_定了表格的結(jié)構(gòu)后,Tabula 會(huì)分析每個(gè)單元格對(duì)應(yīng)的文本塊,并提取出單元格中的文本內(nèi)容;
  • 數(shù)據(jù)整理:Tabula 會(huì)嘗試自動(dòng)整理從表格中提取的數(shù)據(jù),例如:縱向和橫向合并單元格,處理跨頁(yè)的表格等。它也會(huì)執(zhí)行一定的文本清理;
  • 導(dǎo)出格式:Tabula 支持將提取出來(lái)的數(shù)據(jù)導(dǎo)出為 CSV 和 JSON 格式。用戶(hù)可以導(dǎo)入到 Excel 等其他工具中進(jìn)行后續(xù)分析。
  • 優(yōu)化算法:Tabula 在表格分析和數(shù)據(jù)提取方面使用了一些優(yōu)化的算法和啟發(fā)式規(guī)則,以提高正確率。同時(shí)它也提供了交互式的編輯接口供用戶(hù)校正結(jié)果。

怎么用 Tabula

首先肯定是引入 pom 文件依賴(lài),如下圖:

<dependency>
  <groupId>technology.tabula</groupId>
  <artifactId>tabula</artifactId>
  <version>1.0.5</version>
</dependency>

接著就可以創(chuàng)建 PDF 工具類(lèi)了(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 基本上都是這樣來(lái)解析 pdf 中的表格的,所以可以將程序提取出來(lái),直到 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(); // 存儲(chǔ)解析后的JSON數(shù)組
            try (PDDocument document = PDDocument.load(new File(pdfPath))) {
                  PageIterator pi = new ObjectExtractor(document).extract(); // 獲取頁(yè)面迭代器
                  // 遍歷所有頁(yè)面
                  while (pi.hasNext()) {
                        Page page = pi.next(); // 獲取當(dāng)前頁(yè)
                        List<Table> tableList = SPREADSHEEET_EXTRACTION_ALGORITHM.extract(page); // 解析頁(yè)面上的所有表格
                        // 遍歷所有表格
                        for (Table table : tableList) {
                              List<List<RectangularTextContainer>> rowList = table.getRows(); // 獲取表格中的每一行
                              // 遍歷所有行并獲取每個(gè)單元格信息
                              for (int rowIndex = customStart; rowIndex < rowList.size(); rowIndex++) {
                                    List<RectangularTextContainer> cellList = rowList.get(rowIndex); // 獲取行中的每個(gè)單元格
                                    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)樣例代碼來(lái)實(shí)現(xiàn) pdf 表格解析先。大致的思路就是:

  • 創(chuàng)建一個(gè)空的 JSONArray 對(duì)象 reJsonArr ,用于存儲(chǔ)解析后的表格數(shù)據(jù);
  • 使用 PDDocument.load 方法加載指定路徑的 PDF 文件,并使用 try-with-resources 語(yǔ)句創(chuàng)建一個(gè) PDDocument 對(duì)象 document ;
  • 使用 ObjectExtractor 從 document 中提取頁(yè)面迭代器 pi ;
  • 使用 while 循環(huán)遍歷每個(gè)頁(yè)面,使用 pi.hasNext 方法判斷是否還有下一個(gè)頁(yè)面,如果有則進(jìn)入循環(huán);
  • 使用 pi.next 方法獲取當(dāng)前頁(yè)面對(duì)象 page ;
  • 使用 SPREADSHEEET_EXTRACTION_ALGORITHM 解析 page 中的所有表格,并將結(jié)果存儲(chǔ)在 tableList 中;
  • 使用 for 循環(huán)遍歷 tableList 中的每個(gè)表格,對(duì)于每個(gè)表格執(zhí)行以下操作: a. 使用 table.getRows 方法獲取表格中的每一行,并將結(jié)果存儲(chǔ)在 rowList 中; b. 使用 for 循環(huán)遍歷 rowList 中的每一行,從 customStart 位置開(kāi)始,對(duì)于每一行執(zhí)行以下操作: i. 使用 rowList.get 方法獲取行中的每個(gè)單元格,并將結(jié)果存儲(chǔ)在 cellList 中; ii. 將 cellList 、 rowIndex 和 reJsonArr 作為參數(shù)傳遞給回調(diào)函數(shù) callback 的 handler 方法進(jìn)行處理;
  • 使用 try-catch 語(yǔ)句捕獲可能發(fā)生的 IOException 異常,并記錄錯(cuò)誤信息;
  • 使用 finally 語(yǔ)句移除 THREAD_LOCAL 中的數(shù)據(jù);
  • 返回解析后的 JSONArray 對(duì)象 reJsonArr ;

這里要加上一個(gè) callback.handler 回調(diào)函數(shù)主要的目的是為了將“單元格操作”跟 pdf 解析兩部分代碼進(jìn)行解耦,那么這個(gè)回調(diào)接口的接口定義如下:

@FunctionalInterface
public interface PdfCellCustomProcess {
      /**
       * @description: 自定義單元格回調(diào)處理
       * @return {*}
       */
      void handler(List<RectangularTextContainer> cellList, int rowIndex, JSONArray reJsonArr);
}

其中 cellList 傳入的是這一行的所有單元格的集合,rowIndex 傳入的是當(dāng)前行碼,reJsonArr 是返回值。具體的實(shí)現(xiàn)代碼如下:

public class PdfUtil {
      ...
      /**
       * @description: 解析 pdf 中簡(jiǎn)單的表格并返回 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();
                  // 遍歷單元格獲取每個(gè)單元格內(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);
                  }
            });
      }
     ...
}

代碼的主要部分是一個(gè) Lambda 表達(dá)式,它作為參數(shù)傳遞給 parsePdfTable 方法。Lambda 表達(dá)式做了PdfCellCustomProcess 接口的實(shí)現(xiàn)。Lambda 表達(dá)式的代碼塊首先創(chuàng)建一個(gè) JSONObject 對(duì)象,然后遍歷單元格列表,獲取每個(gè)單元格的文本內(nèi)容。

如果當(dāng)前行索引等于自定義起始行索引,將文本內(nèi)容添加到 headList 列表中;否則,將文本內(nèi)容作為鍵值對(duì)添加到j(luò)sonObj 對(duì)象中。最后,如果 jsonObj 對(duì)象不為空,則將其添加到 reArr 數(shù)組中。 代碼還包含了一些其他操作。如果當(dāng)前行索引等于自定義起始行索引,將 headList 列表設(shè)置為 THREAD_LOCAL 線程局部變量。最后,返回 reArr數(shù)組作為方法的結(jié)果。

最后只需要補(bǔ)上 main 方法調(diào)用即可獲取到解析后的 JsonArray 集合。但是直接輸出 JsonArray 數(shù)據(jù)并不直觀,于是我又寫(xiě)了一個(gè)解析 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();
}

這個(gè)應(yīng)該比較好理解吧,這里就不再詳述了。

以上的代碼對(duì)于一般的 PDF 表格解析是基本沒(méi)有問(wèn)題的,但是對(duì)于帶有合并單元格的解析就不能滿(mǎn)足了。合并單元格需要考慮橫向合并、縱向合并和混合合并三種合并模式,不是說(shuō) tabula-java 的 sdk 不能做只是比較麻煩,在 tabula-java 方案中我們可以獲取到單元格的高和寬,那么先做一次全遍歷獲取二維數(shù)組對(duì)于單元格定位后,根據(jù)高和寬進(jìn)行虛擬表格的建設(shè),最后根據(jù)二維數(shù)組對(duì)數(shù)據(jù)進(jìn)行回填即可。這也是用回調(diào)將單元格操作分離的原因之一,為了后面做合并單元格解析做準(zhǔn)備的。

但其實(shí)上面說(shuō)這么多,合并單元格解析的代碼我還沒(méi)寫(xiě)呢(以上都是我吹的),等完成后再給大家分享。

到此這篇關(guān)于java利用Tabula實(shí)現(xiàn)對(duì)PDF內(nèi)表格數(shù)據(jù)提取的文章就介紹到這了,更多相關(guān)java PDF提取數(shù)據(jù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

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

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

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

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

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

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

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

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

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

    Java 繼承方法實(shí)例詳解

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

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

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

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

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

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

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

    SpringBoot如何正確配置并運(yùn)行Kafka

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

    java.lang.Long cannot be cast to ja

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

最新評(píng)論

嘉义市| 鹿邑县| 如东县| 海伦市| 南漳县| 青冈县| 万荣县| 建德市| 邓州市| 尼木县| 九台市| 赞皇县| 微山县| 兴安县| 海口市| 台中市| 江陵县| 江孜县| 泾川县| 景东| 绍兴市| 垣曲县| 钟山县| 忻城县| 卢氏县| 砀山县| 洛扎县| 平度市| 岢岚县| 恩施市| 隆德县| 宁明县| 乌拉特前旗| 望谟县| 榆社县| 蓬莱市| 班玛县| 安阳县| 广宁县| 通州市| 成武县|