DOCX 的非正式介绍
已发表: 2022-03-11大约有 10 亿人使用 Microsoft Office,DOCX 格式是在办公室之间交换文档文件的最流行的事实标准。 其最接近的竞争对手——ODT 格式——仅被 Open/LibreOffice 和一些开源产品支持,使其远非标准。 PDF 格式不是竞争对手,因为 PDF 无法编辑且不包含完整的文档结构,因此只能进行有限的本地更改,如水印、签名等。 这就是为什么大多数业务文档都是以 DOCX 格式创建的; 没有替代它的好方法。
虽然 DOCX 是一种复杂的格式,但您可能希望手动解析它以完成更简单的任务,例如索引、转换为 TXT 和进行其他小的修改。 我想为您提供有关 DOCX 内部结构的足够信息,这样您就不必参考 ECMA 规范,这是一本 5,000 页的庞大手册。
理解该格式的最佳方法是使用 MSWord 创建一个简单的单字文档,并观察编辑文档如何更改底层 XML。 您会遇到 DOCX 在 MS Word 中格式不正确且您不知道原因的情况,或者遇到不明显如何生成所需格式的情况。 准确地查看和理解 XML 中正在发生的事情将对此有所帮助。
我在协作 DOCX 编辑器 CollabOffice 上工作了大约一年,我想与开发人员社区分享其中的一些知识。 在本文中,我将解释 DOCX 文件结构,总结分散在 Internet 上的信息。 本文是庞大、复杂的 ECMA 规范和当前可用的简单互联网教程之间的中介。 您可以在我的 github 帐户的toptal-docx项目中找到本文随附的文件。
一个简单的 DOCX 文件
DOCX 文件是 XML 文件的 ZIP 存档。 如果您创建一个新的空 Microsoft Word 文档,在里面写一个单词“Test”并解压缩它的内容,您将看到以下文件结构:
尽管我们创建了一个简单的文档,但 Microsoft Word 中的保存过程已经生成了 XML 格式的默认主题、文档属性、字体表等等。
首先,让我们删除未使用的内容并关注包含主要文本元素的document.xml 。 删除文件时,请确保已从其他 xml 文件中删除了对该文件的所有关系引用。 这是一个关于我如何清除对 app.xml 和 core.xml 的依赖项的代码差异示例。 如果您有任何未解决/缺失的引用,MSWord 将认为该文件已损坏。
这是我们简化的最小 DOCX 文档的结构(这里是 github 上的项目):
让我们从顶部按文件分解它:
_rels/.rels
这定义了告诉 MS Word 在哪里查找文档内容的参考。 在这种情况下,它引用word/document.xml :
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"> <Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/> </Relationships>_rels/document.xml.rels
此文件定义对嵌入在文档内容中的资源(例如图像)的引用。 我们的简单文档没有嵌入资源,因此关系标签为空:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"> </Relationships>[内容类型].xml
[Content_Types].xml包含有关文档内媒体类型的信息。 因为我们只有文本内容,所以很简单:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"> <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/> <Default Extension="xml" ContentType="application/xml"/> <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/> </Types>文档.xml
最后,这里是包含文档文本内容的主要 XML。 为了清楚起见,我删除了一些命名空间声明,但您可以在 github 项目中找到该文件的完整版本。 在该文件中,您会发现文档中的某些命名空间引用未使用,但您不应删除它们,因为 MS Word 需要它们。
这是我们的简化示例:
<w:document> <w:body> <w:pw:rsidR="005F670F" w:rsidRDefault="005F79F5"> <w:r><w:t>Test</w:t></w:r> </w:p> <w:sectPr w:rsidR="005F670F"> <w:pgSz w:w="12240" w:h="15840"/> <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="720" w:footer="720" w:gutter="0"/> <w:cols w:space="720"/> <w:docGrid w:linePitch="360"/> </w:sectPr> </w:body> </w:document> 主节点<w:document>代表文档本身, <w:body>包含段落,并且嵌套在<w:body>中的是由<w:sectPr>定义的页面尺寸。
<w:rsidR>是一个可以忽略的属性; 它被 MS Word 内部使用。
让我们看一个包含三个段落的更复杂的文档。 我在 Microsoft Word 的屏幕截图中用相同颜色突出显示了 XML,因此您可以看到相关性:
<w:pw:rsidR="0081206C" w:rsidRDefault="00E10CAE"> <w:r> <w:t xml:space="preserve">这是我们示例的第一段。 默认是左对齐,现在介绍一下</w:t> </w:r> <w:r> <w:rPr> <w:rFonts w:ascii="Arial" w:hAnsi="Arial" w:cs="Arial"/> <w:color w:val="000000"/> </w:rPr> <w:t>有些粗体</w:t> </w:r> <w:r> <w:rPr> <w:rFonts w:ascii="Arial" w:hAnsi="Arial" w:cs="Arial"/> <w:b/> <w:color w:val="000000"/> </w:rPr> <w:t xml:space="preserve"> 文本</w:t> </w:r> <w:r> <w:rPr> <w:rFonts w:ascii="Arial" w:hAnsi="Arial" w:cs="Arial"/> <w:color w:val="000000"/> </w:rPr> <w:t xml:space="preserve">, </w:t> </w:r> <w:proofErr w:type="gramStart"/> <w:r> <w:t xml:space="preserve">也改成</w:t> </w:r> <w:rw:rsidRPr="00E10CAE"> <w:rPr><w:rFonts w:ascii="Impact" w:hAnsi="Impact"/> </w:rPr> <w:t>字体样式</w:t> </w:r> <w:r> <w:rPr> <w:rFonts w:ascii="Impact" w:hAnsi="Impact"/> </w:rPr> <w:t xml:space="preserve"> </w:t> </w:r> <w:r> <w:t>到“影响”。</w:t></w:r> </w:p> <w:pw:rsidR="00E10CAE" w:rsidRDefault="00E10CAE"> <w:r> <w:t>这是新段落。</w:t> </w:r></w:p > <w:pw:rsidR="00E10CAE" w:rsidRPr="00E10CAE" w:rsidRDefault="00E10CAE"> <w:r> <w:t>这又是一段,有点长。</w:t> </w:r> </w:p>
段落结构
一个简单的文档由段落组成,一个段落由运行(一系列具有相同字体、颜色等的文本)组成,运行由字符组成(例如<w:t> )。 <w:t>标签里面可能有几个字符,并且在同一次运行中可能有几个字符。
同样,我们可以忽略<w:rsidR> 。
文本属性
基本文本属性是字体、大小、颜色、样式等。 大约有 40 个标签用于指定文本外观。 正如您在我们的三段示例中所见,每次运行在<w:rPr>内都有自己的属性,指定<w:color> 、 <w:rFonts>和粗体<w:b> 。
需要注意的重要一点是,属性区分了两组字符,普通脚本和复杂脚本(例如阿拉伯语),并且属性具有不同的标记,具体取决于它所影响的字符类型。
大多数普通脚本属性标签都有一个匹配的复杂脚本标签,并添加了一个“C”,指定该属性用于复杂脚本。 例如: <w:i> (斜体)变为<w:iCs> ,普通脚本的粗体标记<w:b>变为复杂脚本的<w:bCs> 。
风格
Microsoft Word 中有一个专门用于样式的完整工具栏:正常、无间距、标题 1、标题 2、标题等。 这些样式存储在/word/styles.xml中(注意:在我们简单示例的第一步中,我们从 DOCX 中删除了这个 XML。创建一个新的 DOCX 来查看这个)。
将文本定义为样式后,您将在段落属性标签<w:pPr>中找到对该样式的引用。 这是一个示例,其中我使用样式标题 1 定义了我的文本:
<w:p> <w:pPr> <w:pStyle w:val="Heading1"/> </w:pPr> <w:r> <w:t>My heading 1</w:t> </w:r> </w:p> 这是来自styles.xml的样式本身:
<w:style w:type="paragraph" w:style> <w:name w:val="heading 1"/> <w:basedOn w:val="Normal"/> <w:next w:val="Normal"/> <w:link w:val="Heading1Char"/> <w:uiPriority w:val="9"/> <w:qFormat/> <w:rsid w:val="002F7F18"/> <w:pPr> <w:keepNext/> <w:keepLines/> <w:spacing w:before="480" w:after="0"/> <w:outlineLvl w:val="0"/> </w:pPr> <w:rPr> <w:rFonts w:asciiTheme="majorHAnsi" w:eastAsiaTheme="majorEastAsia" w:hAnsiTheme="majorHAnsi" w:cstheme="majorBidi"/> <w:b/> <w:bCs/> <w:color w:val="365F91" w:themeColor="accent1" w:themeShade="BF"/> <w:sz w:val="28"/> <w:szCs w:val="28"/> </w:rPr> </w:style> <w:style/w:rPr/w:b> xpath 指定字体为粗体, <w:style/w:rPr/w:color>表示字体颜色。 <w:basedOn>指示 MSWord 对任何缺失的属性使用“普通”样式。

财产继承
文本属性是继承的。 运行有自己的属性( w:p/w:r/w:rPr/* ),但它也继承了段落的属性( w:r/w:pPr/* ),并且两者都可以从/word/styles.xml引用样式属性/word/styles.xml 。
<w:r> <w:rPr> <w:rStyle w:val="DefaultParagraphFont"/> <w:sz w:val="16"/> </w:rPr> <w:tab/> </w:r> 段落和运行以默认属性开头: w:styles/w:docDefaults/w:rPrDefault/*和w:styles/w:docDefaults/w:pPrDefault/* 。 要获得角色属性的最终结果,您应该:
- 使用默认运行/段落属性
- 附加运行/段落样式属性
- 附加本地运行/段落属性
- 在段落属性上附加结果运行属性
当我说“附加”B 到 A 时,我的意思是遍历所有 B 属性并覆盖所有 A 的属性,使所有非相交属性保持原样。
另一个默认属性可能位于的位置是<w:style>标记中w:type="paragraph"和w:default="1" 。 请注意,运行中的字符本身永远不会有默认样式,因此<w:style w:type="character" w:default="1">实际上不会影响任何文本。
1554402290400-dbb29eef3ba6035df7ad726dfc99b2af.png)
切换属性
一些属性是“切换”属性,例如<w:b> (粗体)或<w:i> (斜体); 这些属性的行为类似于 XOR 运算符。
这意味着如果父样式为粗体,而子样式为粗体,则结果将是常规的非粗体文本。
您必须进行大量测试和逆向工程才能正确处理切换属性。 查看 ECMA-376 Open XML 规范的第 17.7.3 段以获取切换属性的正式详细规则/
字体
字体遵循与其他文本属性相同的通用规则,但字体属性默认值在单独的主题文件中指定,在word/_rels/document.xml.rels下引用如下:
<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/> 根据上述参考,默认字体名称将在word/theme/themes1.xml中的<a:theme>标签、 a:themeElements/a:fontScheme/a:majorFont或a:minorFont标签内找到。
默认字体大小为 10,除非w:docDefaults/w:rPrDefault标记丢失,否则大小为 11。
文本对齐
文本对齐由<w:jc>标记指定,有四种可用的w:val模式: "left" 、 "center" 、 "right"和"both" 。
"left"是默认模式; 文本从段落矩形的左侧开始(通常是页面宽度)。 (本段左对齐,这是标准的。)
可以预见, "center"模式将所有字符居中在页面宽度内。 (同样,本段举例说明了居中对齐。)
在"right"模式下,段落文本与右边距对齐。 (请注意此文本如何与右侧对齐。)
"both"模式在单词之间放置额外的间距,以便行变得更宽并占据整个段落宽度,但左对齐的最后一行除外。 (本段就是一个例子。)
图片
DOCX 支持两种图像:内联和浮动。
内联图像与其他字符一起出现在段落中,使用<w:drawing>而不是使用<w:t> (文本)。 您可以使用以下 xpath 语法找到图像 ID:
w:drawing/wp:inline/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip/@r:embed
图像 ID 用于在word/_rels/document.xml.rels文件中查找文件名,它应该指向 word/media 子文件夹中的 gif/jpeg 文件。 (查看github项目的word/_rels/document.xml.rels文件,可以看到图片ID。)
浮动图像相对于段落放置,文本在它们周围流动。 (这是带有浮动图像的 github 项目示例文档。)
浮动图像使用<wp:anchor>而不是<w:drawing> ,因此如果您删除<w:p>中的任何文本,如果您不想删除图像,请小心使用锚点。
表
表格的 XML 标记类似于 HTML 表格标记—— 与 <tr> 等匹配。
<w:tbl> ,表本身,具有表属性<w:tblPr> ,每个列属性由<w:gridCol>内部<w:tblGrid> 。 行一个接一个地作为<w:tr>标记,并且每一行应该具有与<w:tblGrid>中指定的相同数量的列:
<w:tbl> <w:tblPr> <w:tblW w:w="5000" w:type="pct" /> </w:tblPr> <w:tblGrid><w:gridCol/><w:gridCol/></w:tblGrid> <w:tr> <w:tc><w:p><w:r><w:t>left</w:t></w:r></w:p></w:tc> <w:tc><w:p><w:r><w:t>right</w:t></w:r></w:p></w:tc> </w:tr> </w:tbl> 表格列的宽度可以在<w:tblW>标记中指定,但如果您没有定义它,MS Word 将使用其内部算法来找到最小有效表格大小的列的最佳宽度。
单位
DOCX 中的许多 XML 属性指定大小或距离。 虽然它们是 XML 中的整数,但它们都有不同的单位,因此需要进行一些转换。 这个主题很复杂,所以我推荐 Lars Corneliussen 的这篇关于 DOCX 文件中的单元的文章。 他展示的表格很有用,尽管有一点印刷错误:英寸应该是 pt/72,而不是 pt*72。
这是一个备忘单:
| 常见的 DOCX XML 单位转换 | ||||||
| 20分 | 积分 DXA/20 | 英寸 点/72 | 厘米 在* 2,54 | 字体一半大小 pt/144 | 鸸 在* 914400 | |
| 例子 | 11906 | 595.3 | 8,27… | 21.00086… | 4,135 | 7562088 |
| 使用这个的标签 | pgSz/pgMar/w:间距 | w:sz | wp:范围,a:ext | |||
实现布局器的提示
如果要转换 DOCX 文件(例如转换为 PDF)、在画布上绘制或计算页数,则必须实现布局器。 布局器是一种从 DOCX 文件中计算字符位置的算法。
如果您需要 100% 保真度渲染,这是一项复杂的任务。 实现一个好的布局器所需的时间以人年为单位,但如果您只需要一个简单、有限的布局器,则可以相对快速地完成。
一个布局器填充一个父矩形,它通常是页面的一个矩形。 它一个一个地添加运行中的单词。 当当前行溢出时,它会开始一个新的。 如果段落对于父矩形来说太高,则将其换行到下一页。
如果您决定实施布局器,请记住以下重要事项:
- 布局器应注意文本对齐和浮动在图像上的文本
- 它应该能够处理嵌套对象,例如嵌套表
- 如果您想为此类图像提供全面支持,您必须实现一个布局器,其中至少包含两次传递,第一步收集浮动图像的位置,第二步用文本字符填充空白空间。
- 注意缩进和间距。 每个段落前后都有间距,这些数字由
w:spacing标签指定。 垂直间距由w:after和w:before标签指定。 请注意,行间距是由w:line指定的,但这不是人们可能期望的行的大小。 要获得线条的大小,请取当前字体高度,乘以w:line并除以 12。 - DOCX 文件不包含有关分页的信息。 除非您计算每行需要多少空间来确定页数,否则您不会找到文档中的页数。 如果您需要找到页面上每个字符的准确坐标,请务必考虑所有间距、缩进和大小。
- 如果您实现了一个处理表格的全功能 DOCX 布局器,请注意表格跨多个页面时的特殊情况。 导致页面溢出的单元格也会影响其他单元格。
- 创建用于计算表格列宽的最佳算法是一个具有挑战性的数学问题,并且文字处理器和布局器通常使用一些次优的实现。 我建议使用 W3C HTML 表格文档中的算法作为第一近似值。 我还没有找到对 MS Word 使用的算法的描述,并且微软已经随着时间的推移对算法进行了微调,因此不同版本的 Word 可能会稍微不同地布置表格。
如果不清楚:对 XML 进行逆向工程!
当这个或那个 XML 标记在 MS Word 中的工作方式不明显时,有两种主要方法可以弄清楚:
逐步创建所需的内容。 从一个简单的 docx 文件开始。 将每个步骤保存到自己的文件中,例如
1.docx、2.docx。 解压缩它们并使用可视差异工具进行文件夹比较,以查看更改后出现的标签。 (对于商业选项,请尝试 Araxis Merge,或者对于免费选项,WinMerge。)如果您生成 MS Word 不喜欢的 DOCX 文件,请向后工作。 逐步简化您的 XML。 在某些时候,您将了解 MS Word 发现的哪些更改不正确。
DOCX 相当复杂,不是吗?
它很复杂,而且微软的许可证禁止在服务器端使用 MS Word 来处理 DOCX——这对于商业产品来说是相当标准的。 然而,Microsoft 提供了 XSLT 文件来处理大多数 DOCX 标签,但它不会为您提供 100% 甚至 99% 的保真度。 不支持图像上的文本环绕等过程,但您将能够支持大多数文档。 (如果您不需要复杂性,请考虑使用 Markdown 作为替代方案。)
如果您有足够的预算(没有免费的 DOCX 渲染引擎),您可能希望使用 Aspose 或 docx4j 等商业产品。 最受欢迎的免费解决方案是 LibreOffice,用于在 DOCX 和其他格式(包括 PDF)之间进行转换。 不幸的是,LibreOffice 在转换过程中包含许多小错误,而且由于它是一个复杂的开源 C++ 产品,因此修复保真度问题很慢而且很困难。
或者,如果您发现 DOCX 布局过于复杂而无法自己实现,您也可以将其转换为 HTML 并使用浏览器进行渲染。 您还可以考虑 Toptal 的一位自由 XML 开发人员。
DOCX 资源供进一步阅读
- ECMA DOCX 规范
- 从 C# 进行 DOCX 操作的 OpenXML 库。 它不包含有关布局或呈现代码的信息,但提供了与 DOCX 中每个可能的 XML 节点匹配的类层次结构。
- 您可以随时使用 docx4j、OpenXML 和 docx 等关键字在 stackoverflow 上搜索或询问; 社区中有一些知识渊博的人。
