nodejs实现⼀个word⽂档解析器
动机
之前项⽬⾥遇到⼀个需求,需要前端上传⼀个word⽂档,然后后端提取出该⽂档的指定位置的内容并保存。这⾥后端⽤的是nodejs,开始接到这个需求,发现⽆从下⼿,主要是没有处理过word这种类型的⽂档,怎么解析? Excel倒是有相关的库可以⽤,⽽且很简单
思路
搜索了好⼀会⼉,在npm上发现了⼀个叫做adm-zip的包,这个包可以解压缩word⽂档,原来word⽂档也是可以解压缩的,之前⼀直不知道,通过如下代码就可以将word⽂档解压缩,并进⼀步提取内容
var admZip = require('adm-zip');
const zip = new admZip('test.docx');
//将该docx解压到指定⽂件夹result下
复制代码
⾸先我们新建⼀个docx⽂档,内容如下
然后运⾏上述代码进⾏解压缩,得到如下的⽂件,由下图可以看出⽣成了好⼏个⽂件夹,word的内容其实是在word⽂件夹⾥的l⽂件内(这⾥解压缩后其实源⽂件还在,并没有消失)
进⼊word⽂件夹后的内容
我们继续打开l⽂件来⼀探究竟⾥⾯到底是啥?注意要⽤浏览器直接打开,如果⽤ide打开显⽰出的所有内容都在⼀⾏,⽆法阅读!
上图只是word⽂档的⼀部分,会发现word⽂档内看着只有⼏段⽂字,但是xml中却是长篇⼤论,仔细分析下也很正常,xml全称可扩展标记语⾔,其被设计为传输和存储数据,它仅仅是⼀个纯⽂本的表⽰,⽽word中内容格式千变万化,肯定需要⼀种⽅法来有效描述这些内容的格式,因此采⽤了xml来描述
我们尝试⼀下将测试⽂档四个字加粗变⾊倾斜字体,如下图
然后再进⾏解压缩,得到l并查看对应的内容,如下
这就很明显了, <w:b/>表⽰⽂字加粗, <w:i/>表⽰⽂字倾斜, <w:color>表⽰⽂字的颜⾊,所以这么4个字就需要这⼏⾏xml来描述,因此长篇⼤论的xml也就不⾜为奇
提取内容
上⾯说到了xml仅仅是⼀个⽂本的表⽰,我们可以⽤如下代码读取整个xml的内容,结果是⼀个string
var contentXml = adAsText("l");
复制代码
接下来是重点,如何提取我们想要的内容呢,答案是正则表达式,⾸先我们得分析⼀下word⽂档的结构,word⽂档其实是由叫
做Paragraph的段落所构成,在vb中可以很轻松的获取并修改段落,官⽹传送门
那么到底怎么样才是⼀个Paragraph呢,其实很简单,仔细观察word⽂档,见到下图中的⼩箭头了么,每个⼩箭头前⾯的内容就是⼀个段落,那么下图中⼀共有16个Paragraph,当然有些段落是空的,没有任何内容
我们再来研究xml的结构,收起展开的xml,如下图,发现 <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 = [];
//如果⽂件存在
if(exists){
//解压缩
const zip = new admZip(absoluteWordPath);
//将l(解压缩后得到的⽂件)读取为text内容
var contentXml = adAsText("l");
//正则匹配出对应的<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){
xml文件怎么打开textContent+=wtItem.slice(5,-6);
}else{
textContent+=wtItem.slice(26,-6);
}
});
resultList.push(textContent)
}
});
//解析完成
callback(resultList)
}
}else{
callback(resultList)
}
});
};
复制代码
注意⼀下如果段落前有空格,那么<w:t>的格式是不同的,如下,多了这个space描述,所以需要特殊处理
代码量其实很少,关键在于正则的编写,上述docx⽂档提取后的输出结果如下
最后我把这个⼯具写成了⼀个npm包,地址