一、应用场景:有什么用处?

我在从事教育信息化的工作中,经常会用到word的解析。

有人会纳闷,我们都是用word编辑文字,你却去解析它。你今天必须解释一下,解析……是什么意思?

解析就是解剖并分析。解析word,就是提取word里面的结构和内容。

举个例子,不管是考驾照,还是考资格证,我们都有过在设备上做题的经历。

这些题目,你之所以能在前端看到,其实是有人在后端一条条录入的。就像你提交个人信息报备那样。

上面的这种方式的录入,在我们行业,是会被骂的!

尽管你觉得,这并没有什么不妥。

因为对于试卷,录入员拿到的是这样的原稿。

一套试卷有几十道题,一道题又有十来个属性,这么算来,一套题得填上百个框框(请用重庆话读:框框儿)。

其实,程序员完全可以通过技术手段,自己去读出word的内容。

理论上,word里展示的所有内容,程序员都能拿到。拿到数据之后,家境差点的公司,通过代码逻辑去找试卷名、选项这类属性。家境好点儿的企业,通过人工智能的NLP文本分类(我就是这么干的),可以更好地实现智能分类。

反正,两种途径都能实现。

而此时,录入员只负责编辑word就可以了。这个过程就叫做“word导入”。

能导入的前提,是可以解析出来word的内容。能解析word的前提,就是docx格式。

二、docx格式:为什么出现?

不知道您各位有没有印象,从某个时期开始,微软办公文件名的小尾巴多了一个“x”。原来word、PowerPoint还有excel,后缀名分别是doc、ppt、xls。后来,就突然涌现出了docx、pptx、xlsx。

到目前为止,这些x们占据了主流市场。

为什么?为什么要这样?我仿佛听到Office家族在抱着扫帚悲伤地唱:你伤害了Word,还Excel而过,你Access的贪婪,我PowerPoint……

原来的doc格式是加密的,只有微软自己家的软件才能打开。

后来微软觉得,这样并没有让自己很神圣,反而限制了自己的发展。

比如,金山WPS也是搞办公软件的,它的发展很快,积累了很多用户。不少人开始用wps了,word的用户面临被瓜分。于是,微软就基于Office Open XML标准,把文档家族做了兼容。

因为新标准是采用的xml格式记录信息的,所以就在.doc后面加了个x。新标准之后,即便是微软创建的文档,WPS也能打开。这样,用户只关心文档就行,不用在意用哪个软件,这就实现了用户共享。

反正,是这个意思,我猜的,别全信……

三、文件结构:怎么来解析?

那么,docx格式究竟是什么样子的呢?

它看起来是个文件,其实,它是个压缩包。

下面就是一个docx文档,是我亲手编辑的。

我再亲自做一下解压缩:将.docx改为.zip,右键选择解压文件,它就露出原形了。

上图演示了解压缩,并打开了word/media文件夹,这是文档里出现的图片。分析发现,在docx中同一张图复制多次,media里面只保留一张原图。这说明,它是个勤俭节约的好孩子。承载同样多的内容,docx格式比doc的体积要小。

正如我们看到,解压后它是有目录结构的。

放心,我不会挨个把文件都给你介绍一遍。因为,那并没有什么用。即便有用,你也记不住。即便你记得住,我也说不明白……为什么要学习那么全面的知识呢?汉字总计10万多个,常用字只有2500个,够用了。

我说几个比较关键的点。

3.1 主文件 document.xml

位于word下的document.xml文件,是docx的主战场。可以说,文档中你能看到的所有内容,在这里都有直接或者间接的记录。

document.xml是一个XML格式的文档。

我们来打开它,咱们先只打开一级。

为了你能看下去,我对数据做了处理,保证你只能看到关键信息。

<w:document>    <w:body>        <w:p>...</w:p>        <w:p>...</w:p>        <w:tbl>...</w:tbl>        <w:p>...</w:p>        ...        <w:sectPr>...</w:sectPr>    </w:body></w:document>

我们看到,body体里,只有3种标签,分别是<w:p><w:tbl><w:sectPr>

一个docx文档,基本由这三种成分组成。w指的是wordp指的是paragraph段落,tbl表示table表格。sectPr全称是section primp,这个千万别记,容易扰乱思路。

清空你的大脑,docx文档里就两种大类,一种叫<w:p>段落,另一种叫<w:tbl>表格。

3.2 段落标签 w:p

在docx中,段落是最常见的,是文档中最主要的组成单元。

和你理解的段落一样。不换行就属于一个段落。即便是“咔咔咔”敲上6个回车,虽然没有内容,那它也属于6个段落,在xml中是6个<w:p></w:p>

另外,包含图片、流程图、公式等元素的内容,也是包含在段落中的。换句话说,它们都是小弟。

我们能用代码取到段落的信息吗?当然能!(这自问自答,被怀疑是凑字数)

我们用哪类编程语言都可以做到,因为仅仅就是读取XML文件。

但是对于教学而言,用python无疑是最佳的选择。

# 导入解析xml的库import xml.dom.minidom as xdom# 加载文档xp = xdom.parse('word/document.xml')# 获取文档根节点root=xp.documentElement# 获取body节点们bodys = root.getElementsByTagName("w:body") # 因为getElements返回多个对象,我们只有一个body = bodys[0]# 循环遍历body下的节点for i,ele in enumerate(body.childNodes):    e_name = ele.nodeName    # 打印{序号} -> {节点名称} is {对象}    print(i,"->",e_name,"is",ele)

我们看一下打印结果,它和源文档完全对应,一个都没有少。

下面该详细说一下w:p的小弟们了。

3.3 文本标签 w:t

在纷杂的xml文件中,可以扒拉出来一个叫<w:t>标签。

<w:p>    <w:r>        <w:t>word的doc格式原来是不开源的,后来改成了docx格式后,是开源的。</w:t>        ……    </w:r>    ……</w:p>

这里面存储的内容,就是word里面的文字,t就是text的简称。

你在docx里面看到的每一个字,基本上都是被<w:t></w:r>所包裹的。

也就是说,如果我们拿出所有<w:t>标签内的文本,我们就做到了纯文本docx的解析。

代码,其实很简单,就是在<w:p>元素中,扫描<w:t>的标签并取出其内容。

上面我们已经拿到了body标签,所以从那里继续。

# 循环body下的大单元 w:p段落,w:tbl表格for i,ele in enumerate(body.childNodes):    # 找到包含w:t的标签,可能是多个    wts = ele.getElementsByTagName("w:t")     ele_text = "" # 记录大单元内所有文本    for wt in wts: # 循环        ele_text = ele_text + wt.text    print(ele_text) # 打印输出

看看代码对应的效果。

恭喜你,你已经学会了解析docx的文本了。

是的,提取一个docx的文本,就是这么简单。你现在就可以写一个程序,可以做到把docx转为txt。

但是……有一点我要告诉你,运行代码里的wt.text可能会报错,为了便于你理解,我特意写了伪代码。实际上,要从<w:t>中取出文本内容,可以像下面这样:

import re # 导入正则库# 构建一个正则,去除<>标签pattern_del_tag = re.compile(r'<[^>]+>',re.S)# 把<w:t>元素转为xml格式<w:t>xxx</w:t>,然后去标签t_text =  pattern_del_tag.sub('', wt.toxml())

我认为这么讲,你反而能理解。因为,不用在理解什么叫<w:t>的时候,还要分散注意力到正则表达式。

3.4 连续块 w:r

上面我们讲了如何去解析文字。但是,那太简单了。

文字是有样式的。

都在同一段w:p内的文字,它们的样式,可能不一样。

比如前两个字是红色,那么这两个字样式一样。但是,后两个字是绿色,和前面又不一样。

为了解决这个问题,docx把具有相同样式的文字,用<w:r>标签包裹。

r代表run。关于这个run的解释,很多国内文档都直接翻译为“运行”。

其实,run在英文中有很多解释。我觉得在这里更适合它的释义应该是:“一段”、“一系列”、“连续上演”。因此,我个人给这个标签起名叫:连续块。表示在这个标签之内的文本,是一个系列的,他们的特点是连续不间断的。

代码依然是处理xml文件的那一套,不是找标签就是拿属性。

还有其他的样式标签,你可以自己研究。我这里先抛砖引玉,举两个例子。

比如上面例子中的字体颜色,一般在<w:rPr>标签内。w依然表示wordr表示runPr表示Primp,是修饰、装饰的意思。<w:rPr>这个标签的释义就是:连续块的样式说明。类似的还有<w:pPr>,表示对段落paragraph的样式说明。<w:tcPr>,表示对表格单元格table cell的样式说明。

我们来看一下,连续块修饰<w:rPr>是如何定义的。

比如对于粗体、斜体的说明。

用代码进行判断,主要是tag的读取。能找到tag,就说明有此种样式的标记。

w_i = w_rPr.getElementsByTagName("w:i")if w_i:    print("是斜体")w_b = w_rPr.getElementsByTagName("w:b")if w_i:    print("是粗体")

再比如对于各种线条的说明。

上面的例子,如果用代码进行判断的话,除了对 tag存在的判断,还需要获取属性值w:val,表示用了哪一类具体的样式。

w_u = w_rPr.getElementsByTagName("w:u")if w_u:    # 获取属性值用 getAttribute    line_value = w_u[0].getAttribute("w:val")    if line_value == "single":        print("是下划线")    if line_value == "double":        print("是双划线")    if line_value == "wave":        print("是波浪形线")    if line_value == "dotted":        print("是虚线")

我可以很负责任地说,只要是文档中呈现的信息,在xml文件中都可以找到对应的标注。

我可以“吧啦吧啦”全告诉你,但是会影响你看其他的内容。有兴趣你可以去查资料,都是手册类型的资料,很方便。

我倒是觉得,你自己在word中标记一下,然后解压缩观察xml文件的变化,这样子学的更牢。反正我就是这么学来的。

以下是我用html把一个word文档做了复原。

这是docx原文档:

这是解析docx文档后呈现的html页面:

我们可以看到,包括字号、字体、字色、标线都可以复原。

只剩下两个重要的内容没有说了。那就是图片和表格。

3.5 图像标签 w:drawing

docx中的图片是如何从xml中提取出来的呢?

你在连续块<w:r>中会发现有一个<w:drawing>标签。这里面主要存放的,就是图画相关的信息。

图片仅仅是<w:drawing>中的一个小分类。除了图片,还有图表、形状、流程图等。

今天,咱们说个最简单的,那就是如何提取图片。

图片的标签是<pic:pic>,他在xml中如下定义:

<w:drawing>    <pic:pic>        <pic:blipFill>            <a:blip r:embed="rId9"/>        </pic:blipFill>    </pic:pic></w:drawing>

其中,图片文件就藏在<a:blip r:embed="rId9"/>中,里面的rId9就是捕获图片的线索。

还记不记得解压缩时,那个media文件夹,里面有好多图片。

是有图片,但这也不是rId9呀?

别急,有个文件专门做关联这件事情。它就是解压缩之后的word/_rels/document.xml.rels文件。

打开这个文件:

<?xml version="1.0" encoding="UTF-8" standalone="true"?><Relationships>    ……    <Relationship Target="media/image1.png" Id="rId9"/>    <Relationship Target="media/image3.png" Id="rId11"/>    <Relationship Target="media/image2.GIF" Id="rId10"/></Relationships>

哈哈,这里有档案,记录了哪个id指向哪个文件。于是,我们解析这个文件,就可以拿到对应关系。

然后,遇到picid等于rId9的,就把media/image1.png这个图片文件展示出来。

这,就实现了图片的解析。

可以鼓掌了!

3.6 表格标签 w:tbl

表格标签<w:tbl>的地位很重,它和段落标签<w:p>平级。

分析整个文档的顶层组成元素,我们不难发现,除了<w:tbl>就是<w:p>

一开始,我不理解。为什么图表、流程图那么复杂的元素,却不配得到表格那么高的地位。天理何在?

后来,我解析到一个表格后发现。我,好像低估了<w:tbl>大佬的地位。

如果,你看到上面的图没有笑喷。那说明,你可能没有看懂本文。或者是我的笑点出了问题。

单纯表格的结构,其实不复杂。但是,表格里每一个单元格,却可以容纳另一个word文档。表格里面,图片、图表、文本样式,甚至表格里再来一个表格,什么都可以添加,它甚至是包含了<w:p>大佬。<w:tbl><w:p>并列,它反而是委屈的。

我们来看一下表格<w:tbl>的基本结构。

<w:tbl>    <w:tblGrid>        <w:gridCol/>        <w:gridCol/>    </w:tblGrid>    <w:tr>        <w:tc><w:p>...</w:p></w:tc>        <w:tc>...</w:tc>    </w:tr>    <w:tr>    ...    </w:tr></w:tbl>

节点元素中,主要有两部分内容,一个<w:tblGrid>介绍表格列的个数。另一个是<w:tr>包含表格行的信息。 trtable row的缩写。

其中,<w:tr>里面有<w:tc>,这里面是格子的内容。我们发现其内容,居然是一个它的兄弟节点<w:p>。在此,给表格大佬深深地鞠上一躬。

tctable cell的缩写,表示单元格的意思。有人可能会说,tctable column的缩写,表示表格的列。当我后面论述带有合并单元格的表格时,从结构上看,我们会感觉table cell更适合语境。

先说基本表格的解析,如下图所示:

解析的代码参考如下:

w_table = body.childNodes[0] # 拿到表格节点# 获取所有的行w_trs = w_table.getElementsByTagName("w:tr")rows_text = [] # 存放行的文本for r_index, tr in enumerate(w_trs):    # 获取所有的单元格    cells = tr.getElementsByTagName("w:tc")    cells_text = [] # 存放单元格的文本    for c_index, cell in enumerate(cells):        # 获取所有的文本        wts = cell.getElementsByTagName("w:t")         for wt in wts: # 把文本拼接            cells_text.append(wt.text)    rows_text.append(cells_text)print(rows_text) # 打印结果,二维数组[[r1c1,r1c2],[r2c1,r2c2]]

运行一下代码,结果如下:

这里面我又挖坑了,除了前面讲w:t时的wt.text陷阱之外。w:tc的内容应该是去解析完整的w:p结构,而这里我只取了w:t文本。这样最简单。因为,我们是要理解表格的结构。因此,其他的可以假装看不见。

但是,也由此可见,做好表格解析的前提,是做好段落解析。因为表格单元格里是段落。

我相信,有了表格的解析结果(二维数组),你很容易就可以把它还原成表格页面用于展示。只需要循环就好,第一层循环行,第二层循环列。

我还是用1分钟写一下代码吧:

rows_text = [['名称', '后缀名'], ['Word', 'docx']]table_html = ["<table>"] for row in rows_text:    table_html.append("<tr>")    for cell in row:        table_html.append("<td>"+cell+"</td>")    table_html.append("</tr>")table_html.append("</table>")table_html = "".join(table_html)print(table_html)# <table># <tr><td>名称</td><td>后缀名</td></tr># <tr><td>Word</td><td>docx</td></tr># </table>

据我所知,世界上的表格除了上面那种简单的,还有一些稍微复杂的。那就是带有合并单元格的表格。

比如下面这种:

这类表格的解析稍微复杂一些,它们属于复杂里面最简单的。

我们来看看他们的xml数据是如何定义的。

首先看带有跨行的表格的例子。

对于跨行的情况,我们发现表格的xml数据,该有的行和列,数量都没有变。只是在要合并的单元格上标记了一个<w:vMerge>标签。

这个标签表示有跨行的单元格。v表示vertical,是竖直方向的意思。vMerge表示竖直合并。这个标签里面还有属性值w:val,当值为restart时表示此单元格开始出现合并,continue表示此单元格没有结束,继续保持,直到遇到非continue情况。

再来看看跨列的情况。

跨列因为发生在行内,是行内矛盾,不影响其他行。所以,我们看到只有在第1行的第1格中,采用<w:gridSpan>标签,说明本行有跨列的单元格。val值是2,表示跨2个单位。

为什么我前面说,tctable cell的缩写。我的依据就在这里。其实这个表格的结构是2行2列。如果c指的是column的话,它应该有2个<w:tc>,后一个复用前一个。但是,我们看上图里的结构,它只有一个<w:tc>。那我们称呼它叫单元格更贴切,因为它只有一个框。你可以反驳我,我会立马说,你说的对,但以后依然称呼它单元格。

带有合并单元格的页面还原,逻辑稍微复杂。但是底层还是和普通表格一样,通过循环行和列。你只需要遇到合并的时候,搞一个colspan=2或者rowspan=3就行。

此处,我不再给大家贴代码了。原理已经讲得很透彻了,算是留一个作业。你可以自己思考一下,会有更多的收获。

唉呀,还是给大家一个小提示吧,仅供参考。因为我搞合并单元格时,花费了2天时间才完成。我把一个关键点提供给大家:对于复杂表格,最终的数据结构用一维数组更恰当。这个结构可以像这样[{"x_start":0, "x_end":1, "y_start":0, "y_end":0, "content":"不要问我"}]

如果你看完了,2个小时还搞不定。那请您不要来问我,问我也是回复一个狗头表情。

四、其他妙用:藏初恋照片?

首先声明,这招我没用过啊。我只是论述它的可行性。

我的很多读者都结婚了,最差也有女朋友了,他们还都怕老婆。

但是,谁没有热血青春呢?

有人就想存着初恋照片,夜深人静的时候看一看。但是,又怕被现任发现。

怎么办?手机相册不安全!文件加密码或者修改后缀名属于不打自招。

那么,你千万不要放到docx中。它只不过是个压缩包。解压缩之后,加点文件进去,改回来还是正常的文档。看不出来有任何问题,经得起组织考验和检查。

我有这么个文档,很清白的一个文档。

解压缩之后,里面包含有图片,甚至pdf都有。

唉,程序员的思维境界就是这么的朴实无华又低级趣味。要是被销售员看到,他们该引发商业战了。

我是ITF男孩,带你从IT男角度看世界。文章未经允许,禁止转载。

举报/反馈

ITF男孩

1590获赞 882粉丝
带你走进,IT男的世界
关注
0
0
收藏
分享