nodejs實現一個word文件解析器

超級索尼發表於2018-08-13

動機

之前專案裡遇到一個需求,需要前端上傳一個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文件,內容如下

nodejs實現一個word文件解析器

然後執行上述程式碼進行解壓縮,得到如下的檔案,由下圖可以看出生成了好幾個資料夾,word的內容其實是在word資料夾裡的document.xml檔案內(這裡解壓縮後其實原始檔還在,並沒有消失)

nodejs實現一個word文件解析器

進入word資料夾後的內容

nodejs實現一個word文件解析器
我們繼續開啟document.xml檔案來一探究竟裡面到底是啥?注意要用瀏覽器直接開啟,如果用ide開啟顯示出的所有內容都在一行,無法閱讀!

nodejs實現一個word文件解析器
上圖只是word文件的一部分,會發現word文件內看著只有幾段文字,但是xml中卻是長篇大論,仔細分析下也很正常,xml全稱可擴充套件標記語言,其被設計為傳輸和儲存資料,它僅僅是一個純文字的表示,而word中內容格式千變萬化,肯定需要一種方法來有效描述這些內容的格式,因此採用了xml來描述

我們嘗試一下將測試文件四個字加粗變色傾斜字型,如下圖

nodejs實現一個word文件解析器
然後再進行解壓縮,得到docuemnt.xml並檢視對應的內容,如下

nodejs實現一個word文件解析器
這就很明顯了,<w:b/>表示文字加粗,<w:i/>表示文字傾斜,<w:color>表示文字的顏色,所以這麼4個字就需要這幾行xml來描述,因此長篇大論的xml也就不足為奇

提取內容

上面說到了xml僅僅是一個文字的表示,我們可以用如下程式碼讀取整個xml的內容,結果是一個string

var contentXml = zip.readAsText("word/document.xml");
複製程式碼

接下來是重點,如何提取我們想要的內容呢,答案是正規表示式,首先我們得分析一下word文件的結構,word文件其實是由叫做Paragraph的段落所構成,在vb中可以很輕鬆的獲取並修改段落,官網傳送門點此

nodejs實現一個word文件解析器

那麼到底怎麼樣才是一個Paragraph呢,其實很簡單,仔細觀察word文件,見到下圖中的小箭頭了麼,每個小箭頭前面的內容就是一個段落,那麼下圖中一共有16個Paragraph,當然有些段落是空的,沒有任何內容

nodejs實現一個word文件解析器
我們再來研究xml的結構,收起展開的xml,如下圖,發現<w:p></w:p>這麼個標籤就是表示的一個段落,中間還有些<w:p>藏在表格內,這麼一看錶格前面3個段落,後面3個段落,和上圖是對應的

nodejs實現一個word文件解析器
因此,我們就可以提取出每個段落的文字並返回一個陣列,每一項就是一個段落的內容,這樣就能夠完整的解析出整個word的內容,關鍵在於如何提取每個<w:p>的內容,我們繼續展開一個<w:p>進行觀察,如下圖,發現內容雖多,其實文字都儲存在<w:t>中間,因此思路就清晰了,首先用正規表示式提取出所有<w:p>的內容,再針對每個<w:p>的內容,進行進一步正則提取,提取出其裡面所有<w:t>的內容,並拼接在一起構成一個段落的總內容

nodejs實現一個word文件解析器

具體程式碼

下面是具體的提取程式碼

//引數是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描述,所以需要特殊處理

nodejs實現一個word文件解析器

程式碼量其實很少,關鍵在於正則的編寫,上述docx文件提取後的輸出結果如下

nodejs實現一個word文件解析器

最後我把這個工具寫成了一個npm包,地址點這裡

相關文章