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”并解压缩它的内容,您将看到以下文件结构:

我们全新的测试 DOCX 结构。

尽管我们创建了一个简单的文档,但 Microsoft Word 中的保存过程已经生成了 XML 格式的默认主题、文档属性、字体表等等。

DOCX 中的所有文件都是 XML 文件,即使是带有“.rels”扩展名的文件。
鸣叫

首先,让我们删除未使用的内容并关注包含主要文本元素的document.xml 。 删除文件时,请确保已从其他 xml 文件中删除了对该文件的所有关系引用。 这是一个关于我如何清除对 app.xml 和 core.xml 的依赖项的代码差异示例。 如果您有任何未解决/缺失的引用,MSWord 将认为该文件已损坏。

这是我们简化的最小 DOCX 文档的结构(这里是 github 上的项目):

我们简化的 DOCX 结构。

让我们从顶部按文件分解它:

_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/* 。 要获得角色属性的最终结果,您应该:

  1. 使用默认运行/段落属性
  2. 附加运行/段落样式属性
  3. 附加本地运行/段落属性
  4. 在段落属性上附加结果运行属性

当我说“附加”B 到 A 时,我的意思是遍历所有 B 属性并覆盖所有 A 的属性,使所有非相交属性保持原样。

另一个默认属性可能位于的位置是<w:style>标记中w:type="paragraph"w:default="1" 。 请注意,运行中的字符本身永远不会有默认样式,因此<w:style w:type="character" w:default="1">实际上不会影响任何文本。

鸣叫

运行中的字符可以从其段落继承,并且都可以从styles.xml 继承。

1554402290400-dbb29eef3ba6035df7ad726dfc99b2af.png)

运行中的字符可以从其段落继承,并且都可以从styles.xml 继承。

切换属性

一些属性是“切换”属性,例如<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:majorFonta: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>中的任何文本,如果您不想删除图像,请小心使用锚点。

内联与浮动。

MS Word 的图像选项将图像对齐称为“文本环绕模式”。

表格的 XML 标记类似于 HTML 表格标记—— 与 <table> 相同, 与 <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:afterw:before标签指定。 请注意,行间距是由w:line指定的,但这不是人们可能期望的行的大小。 要获得线条的大小,请取当前字体高度,乘以w:line并除以 12。
  • DOCX 文件不包含有关分页的信息。 除非您计算每行需要多少空间来确定页数,否则您不会找到文档中的页数。 如果您需要找到页面上每个字符的准确坐标,请务必考虑所有间距、缩进和大小。
  • 如果您实现了一个处理表格的全功能 DOCX 布局器,请注意表格跨多个页面时的特殊情况。 导致页面溢出的单元格也会影响其他单元格。
  • 创建用于计算表格列宽的最佳算法是一个具有挑战性的数学问题,并且文字处理器和布​​局器通常使用一些次优的实现。 我建议使用 W3C HTML 表格文档中的算法作为第一近似值。 我还没有找到对 MS Word 使用的算法的描述,并且微软已经随着时间的推移对算法进行了微调,因此不同版本的 Word 可能会稍微不同地布置表格。

如果不清楚:对 XML 进行逆向工程!

当这个或那个 XML 标记在 MS Word 中的工作方式不明显时,有两种主要方法可以弄清楚:

  • 逐步创建所需的内容。 从一个简单的 docx 文件开始。 将每个步骤保存到自己的文件中,例如1.docx2.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 上搜索或询问; 社区中有一些知识渊博的人。