在 Open XML 字处理文档中处理文本的过程看起来非常简单:文档中包含正文,正文包含段落和表格,表格中包含行和单元格,完全类似于 HTML,不是吗?然后再看,又好像很难。您会看到修订跟踪标记、编号列表和点符列表、内容控件、不影响文本的标记(例如书签和注释)。样式看起来不会影响文本,但如果存在编号列表和点符列表,它们则会影响文本。实际上,真实的情况应该是介于两者之间。有很多功能需要跟踪,但其中每种功能都能够自行执行,并不是太难。
即便如此,还是有一些基本思路和抽象化内容可以简化您对字处理标记的理解。无论您是通过 Open XML SDK 2.0 强类型对象模型、结合使用 欢迎使用 Open XML SDK 2.0 for Microsoft Office 和 LINQ to XML,还是通过其他一些平台(例如 Java 或 PHP)来使用字处理标记,这些抽象化内容都是相关的。我们可以编写处理这些抽象化内容的代码。该代码能够以有序且可预测的方式仅公开那些您感兴趣的元素。在本文中,我提供了采用 LINQ to XML 和 Open XML SDK 2.0 强类型对象模型编写的 Microsoft Visual C# 代码。由于某些有用方法的语义定义很仔细,所以无论您使用何种语言和平台,均可轻松实现这些方法。
在文档的正文部分,所有文本都包含在段落中。我们可以在以下三个位置找到段落:作为正文元素的子级 (w:body)、作为表中单元格的子级 (w:tc) 以及作为文本框内容的子级 (w:txbxContent)。单元格本身可以包含表格。主文档部分还存在其他一些文本实例。图片可以包含可选文本,SmartArt 图形可以包含文本。然而,这些文本段更为独立。与将多个字符串文本汇编成一个字符串有关的问题不适用于这些文本段。
文本内容的一个有趣变化就是段落可以包含运行,运行可以包含绘图,绘图可以包含文本框,而文本框又可以包含段落。这是 Open XML WordprocessingML 标记中唯一的一种情况,即,您会发现段落元素作为其他段落元素的后代。有关此内容的详细信息及相关问题,稍后再论。
简化 WordprocessingML 内容处理方式的首要一点就是应该首先接受所有跟踪修订。有关跟踪修订的语义的详细信息,请参阅Accepting Revisions in Open XML Word-Processing Documents。另请参阅 CodePlex 的 PowerTools for Open XML(该链接可能指向英文页面) 项目中有关接受跟踪修订的 Microsoft Visual C# 3.0 代码示例。单击"下载"选项卡,然后下载 RevisionAccepter.zip。
首先接受跟踪修订的最大好处就是,接受修订后便可放心忽略将您的内容处理方式复杂化的 40 多个元素。其中许多元素具有复杂语义。因此,最好先处理这些元素,然后再处理文档内容。直到我撰写该 MSDN 文章并编写接受修订的代码后,才终于明白这样做的道理,尽管过分简单的方法会导致检索到错误的段落文本。
在许多情况下,您希望在不修改文档的情况下对其进行查询。您可以使用一个简单的技术将文档读入字节数组,从字节数组创建一个大小可调的内存流,然后从内存流打开该文档。有关如何实现此过程的详细信息,请参阅博客通过首先接受修订来简化 Open XML WordprocessingML 查询(该链接可能指向英文页面)。本示例让您接受修订并查询文档,而不接触磁盘上的实际文档。
为帮助了解 WordprocessingML 标记,让我们先定义一些抽象化内容:
接受跟踪修订并决定忽略某些仅适用于高级应用场景的元素之后,就只需处理下面列表中的元素。
块级内容容器
块级内容容器是包含块级内容(例如段落或表格)的 WordprocessingML 元素。文档正文中只有三种块级内容容器元素:
元素 |
元素名称 |
Open XML SDK 2.0 类名称 命名空间:DocumentFormat.OpenXml.Wordprocessing |
---|---|---|
正文 |
w:body |
Body |
表格单元格 |
w:tc |
TableCell |
文本框内容 |
w:txbxContent |
TextBoxContent |
我说过,WordprocessingML 中还存在包含段落的其他块级内容容器,例如注释部分的 w:comment 元素和标题部分的 w:hdr 元素。但是,它们不位于文档的正文部分。因此,它们相对而言比较好处理。
块级内容
块级内容元素是 WordprocessingML 元素,它会占据布局界面的整个宽度。它们在顶部和底部处绑定,然后占据可用空间从左到右的整个宽度。结果,在文档的正常布局版面上,您不会在同一物理行中看到两个段落,也不会看到段落与表格并排显示。
此规则存在例外情况,但实际上这些明显的例外情况并不是真正的例外情况。如果使用多列页面布局,则您可以看到段落并排显示。在此情况下,段落或表格的布局的可用宽度是列,而不是整个页面。另一种情况是页面包含文本框,但在这种情况下,块级内容的布局的可用宽度不包括为文本框保留的空间。另外,文本框自身也有布局界面。
接受修订之后,仅存在两种块级内容元素。
元素 |
元素名称 |
Open XML SDK 2.0 类名称 命名空间:DocumentFormat.OpenXml.Wordprocessing |
---|---|---|
段落 |
w:p |
Paragraph |
表格 |
w:tbl |
Table |
本文中我未提及的另外两个块级内容元素用于数学公式。处理 MathML 文本内容并不是常见的需求。收集公式文本并将其汇总为一个字符串(与对待段落的方式相同)的需求量不大。相反,公式中的文本必须来自公式的上下文。在本文中,我不阐述如何处理 MathML 公式。
运行级内容容器
接受修订后,只有一个元素是运行级内容容器,即段落 (w:p) 元素。运行级内容容器定义运行级内容从左到右(在适当情况下,从右到左)的布局空间。例如,适当时可通过文字环绕对段落中字体不同的多个文本进行水平布局。请注意,段落既是块级内容元素,又是运行级内容容器元素,而表格只是块级内容元素,不是运行级内容容器元素。
运行级内容
运行级内容是段落(具有特定于段落子节的格式)中的内容。例如,运行具有特定的字体。接受修订之后,仅存在三种运行级内容元素。
元素 |
元素名称 |
Open XML SDK 2.0 类名称 命名空间:DocumentFormat.OpenXml.Wordprocessing |
---|---|---|
文本运行 |
w:r |
Run |
VML 绘图 |
w:pict |
Picture |
DrawingML 对象 |
w:drawing |
Drawing |
此元素列表的一个非直观的方面就是矢量标记语言 (VML) 图形对象或 DrawingML 对象不是运行级内容就是子运行级内容。它们都还可以作为后代 w:txbxContent 元素(也是块级内容容器)包含。
子运行级内容
子运行级内容包含作为运行一部分的那些 WordprocessingML 元素。例如,运行可以包含多个文本元素 (w:t)。
元素 |
元素名称 |
Open XML SDK 2.0 类名称 命名空间:DocumentFormat.OpenXml.Wordprocessing |
---|---|---|
中断 |
w:br |
Break |
回车符 |
w:cr |
CarriageReturnPicture |
日期块 – 长日期格式 |
w:daylong |
DayLong |
日期块 – 长日期格式 |
w:daylong |
DayLong |
日期块 – 短日期格式 |
w:dayShort |
DayShort |
DrawingML 对象 |
w:drawing |
Drawing |
日期块 – 长月份格式 |
w:monthLong |
MonthLong |
日期块 – 短月份格式 |
w:monthShort |
MonthShort |
不中断连字符 |
w:noBreakHyphen |
NoBreakHyphen |
页码块 |
w:pgNum |
PageNumber |
VML 绘图 |
w:pict |
Drawing |
绝对位置制表符 |
w:pTab |
PositionalTab |
可选连字符 |
w:softHyphen |
SoftHyphen |
符号字符 |
w:sym |
SymbolChar |
文本 |
w:t |
Text |
制表符 |
w:tab |
TabChar |
日期块 – 长年份格式 |
w:yearlong |
YearLong |
日期块 – 短年份格式 |
w:yearShort |
YearShort |
此列表还包含 VML 绘图和 DrawingML 对象,这些对象可以包含 w:txbxContent 元素(块级内容容器)作为后代。
一个简单的示例便可演示我们正尝试解决的问题。以下文档的第一个段落中包含一个内容控件和一个文本框:
下面的代码示例显示此段落的标记。有关此标记的详细信息,请参阅 ISO/IEC 29500-1:2008(该链接可能指向英文页面) 或标准 ECMA-376:Office Open XML 文件格式第二版本(ECMA-376 第二版)(该链接可能指向英文页面)。
注释 |
---|
已省略无关标记以便更好地展示问题。 |
在该示例中,文本框中的文本与内容控件内的文本位于相同的段落。同时也与内容控件外的文本位于同一段落中。内容控件导致文本元素出现在不同级别的层次结构中。您必须编写处理层次结构中这种差别的代码。这是展示此问题的一个示例。有多个 WordprocessingML 抽象化内容可以导致文本内容出现不同级别的缩进。因此,我们需要开发解决此问题的通用解决方案。
注释 |
---|
使用 Value 属性检索段落 (w:p) 元素的文本是不正确的。 |
返回的文本不正确。
问题不在于我们看到了两次文本框中的内容。问题在于我们看到了文本框。文本框中的文本实际上不属于段落的一部分。它是独立存在的。
您不能循环访问段落的子运行,因为内容控件导致文本运行出现在不同级别的标记层次结构中。
您可以编写代码将此问题处理为特殊情况。然而,这不会为任何其他导致文本内容出现在不同级别层次结构的构造返回正确的结果。相反,我们需要通用的抽象化内容以便处理文档内容。
标准 ECMA-376:Office Open XML 文件格式第一版本 (ECMA-376)(该链接可能指向英文页面) 具有与 XML 层次结构中内容所处位置关联的相同问题。作为后代包含文本框的元素与段落中其他子运行级内容同级。本文中介绍的抽象化内容也同样适用于 ECMA-376 标记。
为了解决此问题,我编写了返回元素的逻辑子内容的轴方法。逻辑子项包括包含在其他元素(可提升内容层次结构的级别)中的内容,例如控件。因此,该逻辑子内容轴与 LINQ to XML(或 XPath)子轴不同。提升层次结构级别的实际元素(w:sdt、w:fldsimple、w:hyperlink)不包括在返回的集合中。我们需要实际内容,而不是那些包含内容的其他元素。
提示 |
---|
我借用了 LINQ to XML 中的术语轴方法。在 XML 文档的上下文中,轴是适用于任何给定元素的概念,有一组特定的相关元素,而轴方法返回这些相关元素的集合。例如,对于给定的 XML 元素,有一组特定的子元素,一组特定的后代以及一组特定的上级。后代、子元素和上级是某些 LINQ to XML 轴方法的基础。 |
下面列示的内容在您检索正文元素的逻辑子内容时,突出显示返回集合中的元素。文本框内的段落元素不包括在逻辑子项中。这是因为段落是包含它的文本框内容元素 (w:txbxContent) 的逻辑子项。文本框内容元素是 VML 像素 (w:pict) 的逻辑子项,而该元素是包含它的运行的逻辑后代。
下面列示的内容突出显示第二个段落的逻辑子内容。逻辑子项中不包含第一个运行的任何后代。
该段落中第一个运行的逻辑子元素是 mc:AlternateContent 元素。
将 mc:AlternateContent 作为逻辑子内容元素之一非常有用,因为它可能包含有关处理内容的替代方法的信息。mc:AlternateContent 元素的逻辑子项为其包含的绘图:
DrawingML 对象的逻辑子项是文本框内容 (w:txbxContents)。其子项是所含的段落。通过以这种方式定义逻辑子轴,可方便地针对任何段落精确组合文本。
实现逻辑子轴方法的第一步是实现返回后代元素(其中,后代为已修整)集合的方法。任何为指定标记后代的元素都不包括在返回的集合中。另一个 DescendantsTrimmed 方法的重载将委托用作参数,它允许您指定将 lambda 表达式作为谓词,以便您可以根据多个标记进行修整。我定义此方法的语义时,将已修整元素本身包括在返回的集合中。
下面的代码示例演示 DescendantsTrimmed 轴方法的语义。在此轴方法中,修整作为 txbxContent 元素后代的元素。显示每个元素的元素名称的代码示例对上级进行计数,以便正确地缩进元素名称。
使用 DescendantsTrimmed 轴方法,您可以实现仅返回一组特定元素的逻辑子项的轴方法。以下是我定义逻辑子项的方法:
-
@H_301_75@
w:document 元素的唯一逻辑子项是 w:body 元素。
@H_301_75@块级内容容器(w:body、w:tc、w:txbxContent)的逻辑子项是块级内容(w:p、w:tbl)。
@H_301_75@表格 (w:tbl) 的逻辑子项是它的行 (w:tr)。
@H_301_75@行 (w:tr) 的逻辑子项是它的单元格 (w:tc)。
@H_301_75@段落 (w:p) 的逻辑子项是它的运行 (w:r)。
@H_301_75@运行 (w:r) 的逻辑子项是子运行级内容(w:t、w:pict、w:drawing 等等)。请参阅文本前面的列表。此外,为符合 Office 2010 和 ISO/IEC 29500,mc:AlternateContent 元素也是运行的子项。我实现了随附的代码以使其同时符合 ECMA-376 第一版和 ISO/IEC 29500(ECMA-376 第二版)。
@H_301_75@替换内容元素的逻辑子项是 mc:Choice 元素中的绘图或图片。您需要处理 mc:Choice 元素的内容,而不是 mc.Fallback 元素。
@H_301_75@VML 图形对象 (w:pict) 或 DrawingML 对象 (w:drawing) 的逻辑子项是任何包含的文本框内容元素 (w:txbxContent)。如果您必须处理 VML 对象或 DrawingML 对象的其他特定部分,则可以重新定义 LogicalChildrenContent 方法以包括返回集合中必须处理的元素。
此第一个示例以递归方式循环访问文档中的所有逻辑内容,并使用正确的缩进显示每个元素的名称。如果元素为文本元素 (w:t),则该功能会打印元素的文本内容。
请注意,此示例首先通过调用 RevisionAccepter.AcceptRevisions 方法接受修订。此示例通过先将文档读入字节数组,然后从字节数组初始化大小可调的内存流来使用打开字处理文档的方法。这样可以允许示例打开可编辑参数设置为 true 的文档,继而允许示例接受修订。如果示例直接打开文档进行编辑,则示例会通过接受修订而修改现有文档,这也是运行时不希望出现的一个副作用。如果示例是在只读模式下打开文档,则接受修订将失败(引发异常)。
当我对问题文档运行此示例时,我会看到以下内容。
我们看到,经过各种编辑会话之后,各种运行均拆分成多个运行。我们可以看到文本框及其内容出现在适当的位置上。
我们可以使用 欢迎使用 Open XML SDK 2.0 for Microsoft Office 的强类型对象模型来实现相同的轴方法。使用逻辑内容轴的代码如下所示。
当我对问题文档运行此示例时,我会看到以下内容。
您经常需要处理文档,在一次操作检索所有段落、每个段落下的所有运行以及每个运行的所有文本元素,然后组合每个段落的关联文本。
为了让此过程尽可能简单,我编写了 LogicalChildrenContent 方法的另一个重载。我将该方法编写为一个扩展方法以便以参数形式获取内容元素的集合,并作为集合返回源集合中每个元素的一组逻辑子元素,这样很有用。此扩展方法相当于 LINQ to XML 中的 Elements 扩展方法(返回源集合中每个元素的所有子元素)。该扩展方法实现起来非常简单。
使用 欢迎使用 Open XML SDK 2.0 for Microsoft Office 的强类型对象模型实现的相同轴方法如下所示。
使用另一个扩展方法(StringConcatenate 方法)也很有用,它是一个字符串聚合操作。
现在,我们可以编写一个小程序来检索正文元素的所有子段落,并检索每个段落的文本。通过结合使用 RevisionAccepter 方法和 LogicalChildrenContent 轴,我们知道可以正确地检索每个段落的文本。
当我对问题文档运行此程序时,我会看到以下内容。
使用 欢迎使用 Open XML SDK 2.0 for Microsoft Office 的示例如下所示。
您可以通过定义 LogicalChildrenContent 轴方法的两个其他重载来简化最后一个示例。常见的操作是检索段落的所有运行和检索运行的所有文本元素。因此,如果我们定义两个按指定标记名称筛选的其他扩展方法,则能进一步简化代码。
欢迎使用 Open XML SDK 2.0 for Microsoft Office 中实现的其他扩展方法如下所示。
简化后的查询如下所示。