動機
之前專案裡遇到一個需求,需要前端上傳一個word文件,然後後端提取出該文件的指定位置的內容並儲存。這裡後端用的是nodejs,開始接到這個需求,發現無從下手,主要是沒有處理過word這種型別的文件,怎麼解析? Excel倒是有相關的庫可以用,而且很簡單
思路
搜尋了好一會兒,在npm上發現了一個叫做adm-zip
的包,這個包可以解壓縮word文件,原來word文件也是可以解壓縮的,之前一直不知道,通過如下程式碼就可以將word文件解壓縮,並進一步提取內容
var admZip = require('adm-zip');
const zip = new admZip('test.docx');
//將該docx解壓到指定資料夾result下
zip.extractAllTo("./result", /*overwrite*/true);
複製程式碼
首先我們新建一個docx文件,內容如下
然後執行上述程式碼進行解壓縮,得到如下的檔案,由下圖可以看出生成了好幾個資料夾,word的內容其實是在word資料夾裡的document.xml檔案內(這裡解壓縮後其實原始檔還在,並沒有消失)
進入word資料夾後的內容
我們繼續開啟document.xml檔案來一探究竟裡面到底是啥?注意要用瀏覽器直接開啟,如果用ide開啟顯示出的所有內容都在一行,無法閱讀! 上圖只是word文件的一部分,會發現word文件內看著只有幾段文字,但是xml中卻是長篇大論,仔細分析下也很正常,xml全稱可擴充套件標記語言,其被設計為傳輸和儲存資料,它僅僅是一個純文字的表示,而word中內容格式千變萬化,肯定需要一種方法來有效描述這些內容的格式,因此採用了xml來描述我們嘗試一下將測試文件
四個字加粗變色傾斜字型,如下圖
<w:b/>
表示文字加粗,<w:i/>
表示文字傾斜,<w:color>
表示文字的顏色,所以這麼4個字就需要這幾行xml來描述,因此長篇大論的xml也就不足為奇
提取內容
上面說到了xml僅僅是一個文字的表示,我們可以用如下程式碼讀取整個xml的內容,結果是一個string
var contentXml = zip.readAsText("word/document.xml");
複製程式碼
接下來是重點,如何提取我們想要的內容呢,答案是正規表示式,首先我們得分析一下word文件的結構,word文件其實是由叫做Paragraph
的段落所構成,在vb中可以很輕鬆的獲取並修改段落,官網傳送門點此
那麼到底怎麼樣才是一個Paragraph
呢,其實很簡單,仔細觀察word文件,見到下圖中的小箭頭了麼,每個小箭頭前面的內容就是一個段落,那麼下圖中一共有16個Paragraph
,當然有些段落是空的,沒有任何內容
<w:p></w:p>
這麼個標籤就是表示的一個段落,中間還有些<w:p>
藏在表格內,這麼一看錶格前面3個段落,後面3個段落,和上圖是對應的
因此,我們就可以提取出每個段落的文字並返回一個陣列,每一項就是一個段落的內容,這樣就能夠完整的解析出整個word的內容,關鍵在於如何提取每個<w:p>
的內容,我們繼續展開一個<w:p>
進行觀察,如下圖,發現內容雖多,其實文字都儲存在<w:t>
中間,因此思路就清晰了,首先用正規表示式提取出所有<w:p>的內容,再針對每個<w:p>的內容,進行進一步正則提取,提取出其裡面所有<w:t>的內容,並拼接在一起構成一個段落的總內容
具體程式碼
下面是具體的提取程式碼
//引數是word檔名,第二個引數是回撥錶示解析完成
var parser = function parseWordDocument(absoluteWordPath,callback){
//返回內容的陣列
var resultList = [];
//如果檔案存在
fs.exists(absoluteWordPath, function(exists){
if(exists){
//解壓縮
const zip = new admZip(absoluteWordPath);
//將document.xml(解壓縮後得到的檔案)讀取為text內容
var contentXml = zip.readAsText("word/document.xml");
//正則匹配出對應的<w:p>裡面的內容,方法是先匹配<w:p>,再匹配裡面的<w:t>,將匹配到的加起來即可
//注意?表示非貪婪模式(儘可能少匹配字元),否則只能匹配到一個<w:p></w:p>
var matchedWP = contentXml.match(/<w:p.*?>.*?<\/w:p>/gi);
//繼續匹配每個<w:p></w:p>裡面的<w:t>,這裡必須判斷matchedWP存在否則報錯
if(matchedWP){
matchedWP.forEach(function(wpItem){
//注意這裡<w:t>的匹配,有可能是<w:t xml:space="preserve">這種格式,需要特殊處理
var matchedWT = wpItem.match(/(<w:t>.*?<\/w:t>)|(<w:t\s.[^>]*?>.*?<\/w:t>)/gi);
var textContent = '';
if(matchedWT){
matchedWT.forEach(function(wtItem){
//如果不是<w:t xml:space="preserve">格式
if(wtItem.indexOf('xml:space')===-1){
textContent+=wtItem.slice(5,-6);
}else{
textContent+=wtItem.slice(26,-6);
}
});
resultList.push(textContent)
}
});
//解析完成
callback(resultList)
}
}else{
callback(resultList)
}
});
};
複製程式碼
注意一下如果段落前有空格,那麼<w:t>
的格式是不同的,如下,多了這個space描述,所以需要特殊處理
程式碼量其實很少,關鍵在於正則的編寫,上述docx文件提取後的輸出結果如下
最後我把這個工具寫成了一個npm包,地址點這裡