nodejs实现一个word文档解析器思路详解

JavaScript023

nodejs实现一个word文档解析器思路详解,第1张

之前项目里遇到一个需求,需要前端上传一个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来描述

我们尝试一下将

测试文档

四个字加粗变色倾斜字体,如下图

然后再进行解压缩,得到docuemnt.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

,当然有些段落是空的,没有任何内容

我们再来研究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

=

[]

//如果文件存在

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包,地址点这里

工作中经常会遇到根据不同数据导出文档的情况,现在我们就来看一下基于 docxtemplater 来导出word文档的方法,使用起来非常便捷。

下面简单介绍一下这几个插件的功能:

下面列出几中常见的 docxtemplater 语法

word模板文件中语法:

word模板文件中语法:

word模板文件中语法:

word模板文件中语法:

在这里关于模板文件的路径 tempDocxPath 我们要着重说一下。

在使用的过程中应该有不少人会遇到这种报错: Can't find end of central directory : is this a zip file ?

那这到底是什么原因导致的呢?

获得模板文件的二进制内容的方法,JSZipUtils.getBinaryContent(path, option) 提供path和option两个参数。我们来看一下path,前端开发最首先想到的可能是绝对路径或者相对路径,你可能还会用到@这个符号作为根目录使用,但在这里你显然不能这么用。