node js 批量處理pdf,提取關鍵資訊,並匯出excel

下雨天DY發表於2018-07-05

batchPDF

a node programing that fetch key infomation from more than two thousand pdf documents,and output in excel

需求描述:處理同一目錄下的2000個pdf檔案,提取每個檔案中的一些關鍵資訊(單位名稱,統一社會信用程式碼,行業類別),並整理資料匯出為excel表格。


  最近在看node檔案處理,恰好發現校友群裡有個土木專業的同學提出這麼一個問題,當時的第一想法就是我也許可以做,然後就找到了那個同學問清楚了明確需求,並且要了部分pdf檔案,開始做......   我的第一想法就是,首先讀取目錄下的檔案,然後對每個檔案內容,進行正則匹配,找出目的資訊,然後再匯出。事實上也是這麼回事,基本上分為三步:

  1. 讀取檔案
  2. 解析檔案,匹配關鍵字。
  3. 匯出excel

讀取檔案

  node讀取pdf檔案,引入了'pdf2json':

npm install pdf2json --save
複製程式碼

使用這個包,可以將pdf解析為json格式,從而得到檔案的內容

const PDFParser = require('pdf2json');
const src = './pdf';

var pdfParser = new PDFParser(this, 1);
pdfParser.loadPDF(`${src}/${path}`);
pdfParser.on('pdfParser_dataError', errData =>reject( new Error(errData.parserError)));
pdfParser.on('pdfParser_dataReady', () => {
    let data = pdfParser.getRawTextContent();

});
複製程式碼

使用正規表示式匹配出關鍵字

  目標是找出每個檔案中的“單位名稱”、”統一社會信用程式碼“、“行業類別”,仔細分析上一過程中輸出的結果:

輸出結果

  因為要處理的檔案內容格式都非常嚴謹,我們所要獲取的資訊都在第三頁,解析出的json資料中,目標文字分佈在page(1)和page(3)中,且目標文字格式都是key:value的格式,每一個文字都換行,所以處理起來就方便多了,最終匹配的是以“單位名稱:”開頭的一個或者多個非空字元,由於要匹配三個值,所以用(red|blue|green)這種方式來查詢目標值。

let result = data.match(/(統一社會信用程式碼|單位名稱|行業類別):[\S]*/g);
複製程式碼

match匹配最終得到一個陣列:

result = ['統一社會信用程式碼:xxx','單位名稱:xxx','行業類別:xxx']
複製程式碼

匯出為excel表格

  網上有很多js程式碼將table匯出為excel的程式碼,這裡使用了'node-xlsx',安裝:

npm install node-xlsx --save
複製程式碼

使用這個是因為簡單,並且也符合需求,上手快。

const xlsx = require('node-xlsx');
var buffer = xlsx.build([{name: 'company', data: list}]);
fs.writeFileSync('list.csv', buffer, 'binary');
複製程式碼

  三行程式碼就搞定了,就得到了一個csv格式的excel,剩下的處理就是對list的處理了,傳入的list需為一個二維陣列,陣列的第一項為表頭,其他項為每一行對應得資料,也為陣列格式。整理的list如下:

[
  ['序號','統一社會信用程式碼','單位名稱','行業類別'],
  ['xxx','xxx','xxx','xxxx']
]
複製程式碼

  解析PDF的過程為非同步,所以在批量處理大量檔案的情況下,要考慮記憶體洩漏問題,每次只處理五個,處理完成之後再去處理剩餘的檔案,直到全部完成處理,輸出為excel。 當檔案數量超過30個,報錯資訊如下:

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
複製程式碼

報錯

出現問題的原因:

  1. 網上有一種回答是:解析的是一個大的檔案,轉換為json後,相當於操作一個巨大的物件,所以會報錯,但是檔案數量小的時候,解析是正常的,所以這種假設可以排除。
  2. 記憶體溢位,程式執行所需要的記憶體超出了系統分配給記憶體大小

  解釋:由於node是基於V8引擎,在node中通過javascript使用記憶體時只能使用部分記憶體,64位系統下約為1.4GB,32位系統下約為0.7GB,當執行的程式佔用系統資源過高,超出了V8對node預設的記憶體限制大小就會報上圖所示錯誤。

  如果是編譯專案,V8提供的預設記憶體大小不夠用,可以去修改 --max-old-space-size,但是我目前的需求是處理2000多個pdf檔案,解析為json,所以使用的記憶體大小是不確定的,不能採取這種方案。

  我的理解:node js 很多操作都是非同步執行的,而非同步操作的結果就是不能確保執行完成的時間,所以當多個非同步操作同時進行的時候,不能確保執行完成的順序,但是執行的結果又是相互關聯的,所以在執行的時候,會一直佔用記憶體,而不被垃圾回收機制回收,造成記憶體洩漏。(也有一種可能是佇列裡等待執行的任務太多了。。。)


錯誤的程式碼

const PDFParser = require('pdf2json');
const fs = require('fs');
const src = './pdf';
const xlsx = require('node-xlsx');
let list = [['序號','統一社會信用程式碼','單位名稱','行業類別']];
let index = 1;
let len = 0;

fs.readdir(src, (err, files) => {
    len = files.length;
    files.forEach(item => {
        var pdfParser = new PDFParser(this, 1);
        pdfParser.loadPDF(`${src}/${item}`);
        pdfParser.on('pdfParser_dataError', errData => console.error(errData.parserError)); pdfParser.on('pdfParser_dataReady', () => {
            let data = pdfParser.getRawTextContent();
            let result = data.match(/(統一社會信用程式碼|單位名稱|行業類別):[\S]*/g);
            for (let i = 0 ;i < 3;++i){
                result[i] = result[i].split(':')[1];
            }
            list.push(result);
            ++index;
            if( index === len){
                var buffer = xlsx.build([{name: 'company', data: list}]); // Returns a buffer
                fs.writeFileSync('list.csv', buffer, 'binary');
            }
        });
    });
});
複製程式碼

  但是究竟這個非同步操作的併發量的上限是多少,不能確定,有一個同學嘗試過,讀取PDF檔案的時候,上限是30,分析以上結果,進行改進,改進之後,每次執行五個非同步操作,執行完成之後再繼續執行下一個五個非同步函式。

  測試過,這種方式處理100個檔案時沒有問題的,對比了兩種方式方法,以34個檔案為測試用例:

方法 | 檔案數量 | 讀取時間(s) | CPU | 記憶體

  • | :-: |:-: -: | :-: | :-: 方法一 | 34| 26.817 | 暴漲(14%-42%) | 最大(1591MB) 方法二 | 34| 19.374 | (36%)平穩 | 最大(300MB)

改進後核心程式碼

ConvertToJSON(path){
    return new Promise((resolve,reject) => {
        var pdfParser = new PDFParser(this, 1);
        pdfParser.loadPDF(`${src}/${path}`);
        pdfParser.on('pdfParser_dataError', errData =>reject( new Error(errData.parserError)));
        pdfParser.on('pdfParser_dataReady', () => {
            // 省略處理部分
            resolve(result);
        });
    }).catch(error => {
        console.log(error);
    });
}

seek(callback){
    let arr = this.files.splice(0,5);
    let all = [];
    arr.forEach(item => {
        all.push(this.ConvertToJSON(item));
    });
    let promise = Promise.all(all);
    promise.then(result => {
       // 省略處理部分
        return this.files.length === 0 ? callback(this.list) : this.seek(callback);
    });
}
複製程式碼

原始碼地址,歡迎指正。   能夠幫助到別人同時自己又嘗試了新鮮事物,所以覺得很開心。

參考文件:

  1. Node.js v8.9.3 文件
  2. nodejs將PDF檔案轉換成txt文字,並利用python處理轉換後的文字檔案
  3. node-xlsx
  4. nodeJs記憶體洩漏問題詳解
  5. Node.js 中的 UnhandledPromiseRejectionWarning 問題
  6. 4種JavaScript的記憶體洩露及避免方法
  7. JavaScript 工作原理之二-如何在 V8 引擎中書寫最優程式碼的 5 條小技巧(譯)

在此鳴謝大學好友邢旭磊。

我的個人部落格:下雨天DY的前端成長記

相關文章