控制 Open XML WordprocessingML 文档中的文本

摘要:   了解如何可靠地检索 Open XML WordprocessingML 文档中的文本。

上次修改时间: 2015年3月9日

适用范围: Office 2007 | Office 2010 | Open XML | Visual Studio Tools for Microsoft Office | Word 2007 | Word 2010

本文内容
简介
了解 WordprocessingML 中的文本内容
最佳实践:在处理前接受修订
了解 WordprocessingML 抽象化内容
层次结构级别的变化如何增加处理过程的复杂性
引入 LogicalChildrenContent 轴方法
实现 DescendantsTrimmed 轴方法
定义逻辑子项
使用 LogicalChildrenContent 轴方法
ExamineDocumentContent 示例
检索段落文本
LogicalChildrenContent 轴方法的两个有用重载
LogicalChildrenContent 方法返回的 XML 元素的标识
在文档中搜索文本
结论
其他资源

目录

  • 简介

  • 了解 WordprocessingML 中的文本内容

  • 最佳实践:在处理前接受修订

  • 了解 WordprocessingML 抽象化内容

  • 层次结构级别的变化如何增加处理过程的复杂性

  • 引入 LogicalChildrenContent 轴方法

  • 实现 DescendantsTrimmed 轴方法

  • 定义逻辑子项

  • 使用 LogicalChildrenContent 轴方法

  • ExamineDocumentContent 示例

  • 检索段落文本

  • LogicalChildrenContent 轴方法的两个有用重载

  • LogicalChildrenContent 方法返回的 XML 元素的标识

  • 在文档中搜索文本

  • 结论

  • 其他资源

单击以获取代码  下载代码(该链接可能指向英文页面)

简介

在 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# 代码。由于某些有用方法的语义定义很仔细,所以无论您使用何种语言和平台,均可轻松实现这些方法。

了解 WordprocessingML 中的文本内容

在文档的正文部分,所有文本都包含在段落中。我们可以在以下三个位置找到段落:作为正文元素的子级 (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 标记,让我们先定义一些抽象化内容:

  • 块级内容容器

  • 块级内容

  • 运行级内容容器

  • 运行级内容

  • 子运行级内容

接受跟踪修订并决定忽略某些仅适用于高级应用场景的元素之后,就只需处理下面列表中的元素。

块级内容容器

块级内容容器是包含块级内容(例如段落或表格)的 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:p

Paragraph

运行级内容

运行级内容是段落(具有特定于段落子节的格式)中的内容。例如,运行具有特定的字体。接受修订之后,仅存在三种运行级内容元素。

运行级内容元素

元素

元素名称

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 元素(块级内容容器)作为后代。

层次结构级别的变化如何增加处理过程的复杂性

一个简单的示例便可演示我们正尝试解决的问题。以下文档的第一个段落中包含一个内容控件和一个文本框:

图 1. 包含内容控件和文本框的文档

包含内容控件和文本框的文档

下面的代码示例显示此段落的标记。有关此标记的详细信息,请参阅 ISO/IEC 29500-1:2008(该链接可能指向英文页面)标准 ECMA-376:Office Open XML 文件格式第二版本(ECMA-376 第二版)(该链接可能指向英文页面)

备注

已省略无关标记以便更好地展示问题。

<w:p>
  <w:pPr>
    <w:ind w:right="3600"/>
  </w:pPr>
  <w:r>
    <w:rPr>
      <w:noProof/>
    </w:rPr>
    <mc:AlternateContent>
      <mc:Choice Requires="wps">
        <w:drawing>
          <!-- . . . -->
          <wps:txbx>
            <w:txbxContent>
              <w:p>
                <w:r>
                  <w:t>Text in text box</w:t>
                </w:r>
              </w:p>
            </w:txbxContent>
          </wps:txbx>
          <!-- . . . -->
        </w:drawing>
      </mc:Choice>
      <mc:Fallback>
        <w:pict>
          <!-- . . . -->
          <v:textbox>
            <w:txbxContent>
              <w:p>
                <w:r>
                  <w:t>Text in text box</w:t>
                </w:r>
              </w:p>
            </w:txbxContent>
          </v:textbox>
          <w10:wrap type="square"/>
          <!-- . . . -->
        </w:pict>
      </mc:Fallback>
    </mc:AlternateContent>
  </w:r>
  <w:sdt>
    <w:sdtContent>
      <w:r>
        <w:t>Text in content control.</w:t>
      </w:r>
    </w:sdtContent>
  </w:sdt>
  <w:r>
    <w:t xml:space="preserve"> Text following the content control.</w:t>
  </w:r>
</w:p>

在该示例中,文本框中的文本与内容控件内的文本位于相同的段落。同时也与内容控件外的文本位于同一段落中。内容控件导致文本元素出现在不同级别的层次结构中。您必须编写处理层次结构中这种差别的代码。这是展示此问题的一个示例。有多个 WordprocessingML 抽象化内容可以导致文本内容出现不同级别的缩进。因此,我们需要开发解决此问题的通用解决方案。

备注

使用 Value 属性检索段落 (w:p) 元素的文本是不正确的。

using (WordprocessingDocument doc = WordprocessingDocument.Open("Test.docx", false))
{
    XElement root = doc.MainDocumentPart.GetXDocument().Root;
    XElement paragraph = root.Descendants(W.p).First();
    Console.WriteLine(paragraph.Value);
}

返回的文本不正确。

图 2. 使用段落值的错误结果

错误结果

问题不在于我们看到了两次文本框中的内容。问题在于我们看到了文本框。文本框中的文本实际上不属于段落的一部分。它是独立存在的。

您不能循环访问段落的子运行,因为内容控件导致文本运行出现在不同级别的标记层次结构中。

using (WordprocessingDocument doc = WordprocessingDocument.Open("Test.docx", false))
{
    XElement root = doc.MainDocumentPart.GetXDocument().Root;
    XElement paragraph = root.Descendants(W.p).First();
    StringBuilder sb = new StringBuilder();
    foreach (XElement t in paragraph.Elements(W.r).Elements(W.t))
        sb.Append((string)t);
    Console.WriteLine(sb.ToString());
}

这不包括内容控件中的文本。

图 3. 段落并置子运行的错误结果

错误结果

您可以编写代码将此问题处理为特殊情况。然而,这不会为任何其他导致文本内容出现在不同级别层次结构的构造返回正确的结果。相反,我们需要通用的抽象化内容以便处理文档内容。

标准 ECMA-376:Office Open XML 文件格式第一版本 (ECMA-376)(该链接可能指向英文页面) 具有与 XML 层次结构中内容所处位置关联的相同问题。作为后代包含文本框的元素与段落中其他子运行级内容同级。本文中介绍的抽象化内容也同样适用于 ECMA-376 标记。

<w:p>
  <w:pPr>
    <w:ind w:right="3600"/>
  </w:pPr>
  <w:r>
    <w:pict>
      <v:shape . . .>
        <v:textbox>
          <w:txbxContent>
            <w:p>
              <w:r>
                <w:t>Text in text box</w:t>
              </w:r>
            </w:p>
          </w:txbxContent>
        </v:textbox>
        <w10:wrap type="square"/>
      </v:shape>
    </w:pict>
  </w:r>
  <w:sdt>
    <w:sdtContent>
      <w:r w:rsidR="00C578DC">
        <w:t>Text in content control.</w:t>
      </w:r>
    </w:sdtContent>
  </w:sdt>
  <w:r>
    <w:t xml:space="preserve"> Text following the content control.</w:t>
  </w:r>
</w:p>

引入 LogicalChildrenContent 轴方法

为了解决此问题,我编写了返回元素的逻辑子内容的轴方法。逻辑子项包括包含在其他元素(可提升内容层次结构的级别)中的内容,例如控件。因此,该逻辑子内容轴与 LINQ to XML(或 XPath)子轴不同。提升层次结构级别的实际元素(w:sdt、w:fldsimple、w:hyperlink)不包括在返回的集合中。我们需要实际内容,而不是那些包含内容的其他元素。

提示

我借用了 LINQ to XML 中的术语轴方法。在 XML 文档的上下文中,轴是适用于任何给定元素的概念,有一组特定的相关元素,而轴方法返回这些相关元素的集合。例如,对于给定的 XML 元素,有一组特定的子元素,一组特定的后代以及一组特定的上级。后代、子元素和上级是某些 LINQ to XML 轴方法的基础。

下面列示的内容在您检索正文元素的逻辑子内容时,突出显示返回集合中的元素。文本框内的段落元素包括在逻辑子项中。这是因为段落是包含它的文本框内容元素 (w:txbxContent) 的逻辑子项。文本框内容元素是 VML 像素 (w:pict) 的逻辑子项,而该元素是包含它的运行的逻辑后代。

<w:body>
  <w:sdt>
    <w:sdtPr>
      <w:id w:val="172579038"/>
      <w:placeholder>
        <w:docPart w:val="DefaultPlaceholder_22675703"/>
      </w:placeholder>
    </w:sdtPr>
    <w:sdtEndPr/>
    <w:sdtContent>
     <w:p>
        <w:r>
          <w:t>Paragraph in content control.</w:t>
        </w:r>
      </w:p>
    </w:sdtContent>
  </w:sdt>
 <w:p>
    <w:pPr>
      <w:ind w:right="3600"/>
    </w:pPr>
    <w:r>
      <w:rPr>
        <w:noProof/>
      </w:rPr>
      <mc:AlternateContent>
        <mc:Choice Requires="wps">
          <w:drawing>
            . . .
            <wps:txbx>
              <w:txbxContent>
                <w:p>
                  <w:r>
                    <w:t>Text in text box</w:t>
                  </w:r>
                </w:p>
              </w:txbxContent>
            </wps:txbx>
            . . .
          </w:drawing>
        </mc:Choice>
        <mc:Fallback>
          <w:pict>
            . . .
            <v:textbox>
              <w:txbxContent>
               <w:p>
                  <w:r>
                    <w:t>Text in text box</w:t>
                  </w:r>
                </w:p>
              </w:txbxContent>
            </v:textbox>
            <w10:wrap type="square"/>
            . . .
          </w:pict>
        </mc:Fallback>
      </mc:AlternateContent>
    </w:r>
    <w:sdt>
      <w:sdtContent>
        <w:r>
          <w:t>Text in content control.</w:t>
        </w:r>
      </w:sdtContent>
    </w:sdt>
    <w:r>
      <w:t xml:space="preserve"> Text following the content control.</w:t>
    </w:r>
  </w:p>
 <w:p>
    <w:r>
      <w:t>Text in a following paragraph.</w:t>
    </w:r>
  </w:p>
</w:body>

下面列示的内容突出显示第二个段落的逻辑子内容。逻辑子项中不包含第一个运行的任何后代。

  . . .
 <w:p>
    <w:pPr>
      <w:ind w:right="3600"/>
    </w:pPr>
   <w:r>
      <w:rPr>
        <w:noProof/>
      </w:rPr>
      <mc:AlternateContent>
        <mc:Choice Requires="wps">
          <w:drawing>
            . . .
            <wps:txbx>
              <w:txbxContent>
                <w:p>
                  <w:r>
                    <w:t>Text in text box</w:t>
                  </w:r>
                </w:p>
              </w:txbxContent>
            </wps:txbx>
            . . .
          </w:drawing>
        </mc:Choice>
        <mc:Fallback>
          <w:pict>
            . . .
            <v:textbox>
              <w:txbxContent>
                <w:p>
                  <w:r>
                    <w:t>Text in text box</w:t>
                  </w:r>
                </w:p>
              </w:txbxContent>
            </v:textbox>
            <w10:wrap type="square"/>
            . . .
          </w:pict>
        </mc:Fallback>
      </mc:AlternateContent>
    </w:r>
    <w:sdt>
      <w:sdtContent>
       <w:r>
          <w:t>Text in content control.</w:t>
        </w:r>
      </w:sdtContent>
    </w:sdt>
   <w:r>
      <w:t xml:space="preserve"> Text following the content control.</w:t>
    </w:r>
  </w:p>

该段落中第一个运行的逻辑子元素是 mc:AlternateContent 元素。

   <w:r>
      <w:rPr>
        <w:noProof/>
      </w:rPr>
     <mc:AlternateContent>
        <mc:Choice Requires="wps">
          <w:drawing>
            . . .
            <wps:txbx>
              <w:txbxContent>
                <w:p>
                  <w:r>
                    <w:t>Text in text box</w:t>
                  </w:r>
                </w:p>
              </w:txbxContent>
            </wps:txbx>
            . . .
          </w:drawing>
        </mc:Choice>
        <mc:Fallback>
          <w:pict>
            . . .
            <v:textbox>
              <w:txbxContent>
                <w:p>
                  <w:r>
                    <w:t>Text in text box</w:t>
                  </w:r>
                </w:p>
              </w:txbxContent>
            </v:textbox>
            <w10:wrap type="square"/>
            . . .
          </w:pict>
        </mc:Fallback>
      </mc:AlternateContent>
    </w:r>

将 mc:AlternateContent 作为逻辑子内容元素之一非常有用,因为它可能包含有关处理内容的替代方法的信息。mc:AlternateContent 元素的逻辑子项为其包含的绘图:

    <w:r>
      <w:rPr>
        <w:noProof/>
      </w:rPr>
     <mc:AlternateContent>
        <mc:Choice Requires="wps">
         <w:drawing>
            . . .
            <wps:txbx>
              <w:txbxContent>
                <w:p>
                  <w:r>
                    <w:t>Text in text box</w:t>
                  </w:r>
                </w:p>
              </w:txbxContent>
            </wps:txbx>
            . . .
          </w:drawing>
        </mc:Choice>
        <mc:Fallback>
          <w:pict>
            . . .
            <v:textbox>
              <w:txbxContent>
                <w:p>
                  <w:r>
                    <w:t>Text in text box</w:t>
                  </w:r>
                </w:p>
              </w:txbxContent>
            </v:textbox>
            <w10:wrap type="square"/>
            . . .
          </w:pict>
        </mc:Fallback>
      </mc:AlternateContent>
    </w:r>

DrawingML 对象的逻辑子项是文本框内容 (w:txbxContents)。其子项是所含的段落。通过以这种方式定义逻辑子轴,可方便地针对任何段落精确组合文本。

实现 DescendantsTrimmed 轴方法

实现逻辑子轴方法的第一步是实现返回后代元素(其中,后代为已修整)集合的方法。任何为指定标记后代的元素都不包括在返回的集合中。另一个 DescendantsTrimmed 方法的重载将委托用作参数,它允许您指定将 lambda 表达式作为谓词,以便您可以根据多个标记进行修整。我定义此方法的语义时,将已修整元素本身包括在返回的集合中。

下面的代码示例演示 DescendantsTrimmed 轴方法的语义。在此轴方法中,修整作为 txbxContent 元素后代的元素。显示每个元素的元素名称的代码示例对上级进行计数,以便正确地缩进元素名称。

XElement doc = XElement.Parse(
    @"<body>
        <p>
          <r>
            <t>Text before the text box.</t>
          </r>
          <r>
            <pict>
              <txbxContent>
                <p>
                  <r>
                    <t>Text in a text box.</t>
                  </r>
                </p>
              </txbxContent>
            </pict>
          </r>
          <r>
            <t>Text after the text box.</t>
          </r>
        </p>
      </body>");
foreach (XElement c in doc.DescendantsTrimmed("txbxContent"))
    Console.WriteLine("{0}{1}", "".PadRight(c.Ancestors().Count() * 2), c.Name);

此示例显示返回的集合中每个元素名称的缩进列表。

  p
    r
      t
    r
      pict
        txbxContent
    r
      t

定义逻辑子项

使用 DescendantsTrimmed 轴方法,您可以实现仅返回一组特定元素的逻辑子项的轴方法。以下是我定义逻辑子项的方法:

  • w:document 元素的唯一逻辑子项是 w:body 元素。

  • 块级内容容器(w:body、w:tc、w:txbxContent)的逻辑子项是块级内容(w:p、w:tbl)。

  • 表格 (w:tbl) 的逻辑子项是它的行 (w:tr)。

  • 行 (w:tr) 的逻辑子项是它的单元格 (w:tc)。

  • 段落 (w:p) 的逻辑子项是它的运行 (w:r)。

  • 运行 (w:r) 的逻辑子项是子运行级内容(w:t、w:pict、w:drawing 等等)。请参阅文本前面的列表。此外,为符合 Office 2010 和 ISO/IEC 29500,mc:AlternateContent 元素也是运行的子项。我实现了随附的代码以使其同时符合 ECMA-376 第一版和 ISO/IEC 29500(ECMA-376 第二版)。

  • 替换内容元素的逻辑子项是 mc:Choice 元素中的绘图或图片。您需要处理 mc:Choice 元素的内容,而不是 mc.Fallback 元素。

  • VML 图形对象 (w:pict) 或 DrawingML 对象 (w:drawing) 的逻辑子项是任何包含的文本框内容元素 (w:txbxContent)。如果您必须处理 VML 对象或 DrawingML 对象的其他特定部分,则可以重新定义 LogicalChildrenContent 方法以包括返回集合中必须处理的元素。

使用 LogicalChildrenContent 轴方法

在学习 LogicalChildrenContent 方法的实现之前,了解其使用方法非常有用。

下图显示存在问题的示例文档。

图 4. 包含内容控件和文本框的文档

包含内容控件和文本框的文档

ExamineDocumentContent 示例

此第一个示例以递归方式循环访问文档中的所有逻辑内容,并使用正确的缩进显示每个元素的名称。如果元素为文本元素 (w:t),则该功能会打印元素的文本内容。

请注意,此示例首先通过调用 RevisionAccepter.AcceptRevisions 方法接受修订。此示例通过先将文档读入字节数组,然后从字节数组初始化大小可调的内存流来使用打开字处理文档的方法。这样可以允许示例打开可编辑参数设置为 true 的文档,继而允许示例接受修订。如果示例直接打开文档进行编辑,则示例会通过接受修订而修改现有文档,这也是运行时不希望出现的一个副作用。如果示例是在只读模式下打开文档,则接受修订将失败(引发异常)。

static void IterateContent(XElement element, int depth)
{
    if (element.Name == W.t)
        Console.WriteLine("{0}{1} >{2}<", "".PadRight(depth * 2), element.Name.LocalName,
            (string)element);
    else
        Console.WriteLine("{0}{1}", "".PadRight(depth * 2), element.Name.LocalName);
    foreach (XElement item in element.LogicalChildrenContent())
        IterateContent(item, depth + 1);
}

static void Main(string[] args)
{
    byte[] docByteArray = File.ReadAllBytes("Test.docx");
    using (MemoryStream memoryStream = new MemoryStream())
    {
        memoryStream.Write(docByteArray, 0, docByteArray.Length);
        using (WordprocessingDocument doc =
            WordprocessingDocument.Open(memoryStream, true))
        {
            RevisionAccepter.AcceptRevisions(doc);
            IterateContent(doc.MainDocumentPart.GetXDocument().Root, 0);
        }
    }
}

当我对问题文档运行此示例时,我会看到以下内容。

document
  body
    p
      r
        t >Paragraph in <
      r
        t >content control.<
    p
      r
        AlternateContent
          drawing
            txbxContent
              p
                r
                  t >Text in text box<
      r
        t >Text in content control. <
      r
        t >Text following the content control.<
    p
      r
        t >Text in a following<
      r
        t > paragraph.<

我们看到,经过各种编辑会话之后,各种运行均拆分成多个运行。我们可以看到文本框及其内容出现在适当的位置上。

我们可以使用 欢迎使用 Open XML SDK 2.0 for Microsoft Office 的强类型对象模型来实现相同的轴方法。使用逻辑内容轴的代码如下所示。

static void IterateContent(OpenXmlElement element, int depth)
{
    if (element.GetType() == typeof(Text))
        Console.WriteLine("{0}{1} >{2}<", "".PadRight(depth * 2),
            element.GetType().Name, ((Text)element).Text);
    else
        Console.WriteLine("{0}{1}", "".PadRight(depth * 2),
            element.GetType().Name);
    foreach (var item in element.LogicalChildrenContent())
        IterateContent(item, depth + 1);
}

static void Main(string[] args)
{
    byte[] docByteArray = File.ReadAllBytes("Test7.docx");
    using (MemoryStream memoryStream = new MemoryStream())
    {
        memoryStream.Write(docByteArray, 0, docByteArray.Length);
        using (WordprocessingDocument doc =
            WordprocessingDocument.Open(memoryStream, true))
        {
            RevisionAccepter.AcceptRevisions(doc);
            IterateContent(doc.MainDocumentPart.Document, 0);
        }
    }
}

当我对问题文档运行此示例时,我会看到以下内容。

Document
  Body
    Paragraph
      Run
        Text >Paragraph in <
      Run
        Text >content control.<
    Paragraph
      Run
        AlternateContent
          Drawing
            TextBoxContent
              Paragraph
                Run
                  Text >Text in text box<
      Run
        Text >Text in content control. <
      Run
        Text >Text following the content control.<
    Paragraph
      Run
        Text >Text in a following<
      Run
        Text > paragraph.<

检索段落文本

您经常需要处理文档,在一次操作检索所有段落、每个段落下的所有运行以及每个运行的所有文本元素,然后组合每个段落的关联文本。

为了让此过程尽可能简单,我编写了 LogicalChildrenContent 方法的另一个重载。我将该方法编写为一个扩展方法以便以参数形式获取内容元素的集合,并作为集合返回源集合中每个元素的一组逻辑子元素,这样很有用。此扩展方法相当于 LINQ to XML 中的 Elements 扩展方法(返回源集合中每个元素的所有子元素)。该扩展方法实现起来非常简单。

public static IEnumerable<XElement> LogicalChildrenContent(this IEnumerable<XElement> source)
{
    foreach (XElement e1 in source)
        foreach (XElement e2 in e1.LogicalChildrenContent())
            yield return e2;
}

使用 欢迎使用 Open XML SDK 2.0 for Microsoft Office 的强类型对象模型实现的相同轴方法如下所示。

public static IEnumerable<OpenXmlElement> LogicalChildrenContent(
    this IEnumerable<OpenXmlElement> source)
{
    foreach (OpenXmlElement e1 in source)
        foreach (OpenXmlElement e2 in e1.LogicalChildrenContent())
            yield return e2;
}

使用另一个扩展方法(StringConcatenate 方法)也很有用,它是一个字符串聚合操作。

public static string StringConcatenate(this IEnumerable<string> source)
{
    StringBuilder sb = new StringBuilder();
    foreach (string s in source)
        sb.Append(s);
    return sb.ToString();
}

现在,我们可以编写一个小程序来检索正文元素的所有子段落,并检索每个段落的文本。通过结合使用 RevisionAccepter 方法和 LogicalChildrenContent 轴,我们知道可以正确地检索每个段落的文本。

static void Main(string[] args)
{
    byte[] docByteArray = File.ReadAllBytes("Test.docx");
    using (MemoryStream memoryStream = new MemoryStream())
    {
        memoryStream.Write(docByteArray, 0, docByteArray.Length);
        using (WordprocessingDocument doc =
            WordprocessingDocument.Open(memoryStream, true))
        {
            RevisionAccepter.AcceptRevisions(doc);
            XElement root = doc.MainDocumentPart.GetXDocument().Root;
            XElement body = root.LogicalChildrenContent().First();
            foreach (XElement blockLevelContentElement in body.LogicalChildrenContent())
            {
                if (blockLevelContentElement.Name == W.p)
                {
                    var text = blockLevelContentElement
                        .LogicalChildrenContent()
                        .Where(e => e.Name == W.r)
                        .LogicalChildrenContent()
                        .Where(e => e.Name == W.t)
                        .Select(t => (string)t)
                        .StringConcatenate();
                    Console.WriteLine("Paragraph text >{0}<", text);
                    continue;
                }
                // If element is not a paragraph, it must be a table.
                Console.WriteLine("Table");
            }
        }
    }
}

当我对问题文档运行此程序时,我会看到以下内容。

Paragraph text >Paragraph in content control.<
Paragraph text >Text in content control. Text following the content control.<
Paragraph text >Text in a following paragraph.<

使用 欢迎使用 Open XML SDK 2.0 for Microsoft Office 的示例如下所示。

static void Main(string[] args)
{
    byte[] docByteArray = File.ReadAllBytes("Test7.docx");
    using (MemoryStream memoryStream = new MemoryStream())
    {
        memoryStream.Write(docByteArray, 0, docByteArray.Length);
        using (WordprocessingDocument doc =
            WordprocessingDocument.Open(memoryStream, true))
        {
            RevisionAccepter.AcceptRevisions(doc);
            OpenXmlElement root = doc.MainDocumentPart.Document;
            Body body = (Body)root.LogicalChildrenContent().First();
            foreach (OpenXmlElement blockLevelContentElement in
                body.LogicalChildrenContent())
            {
                if (blockLevelContentElement is Paragraph)
                {
                    var text = blockLevelContentElement
                        .LogicalChildrenContent()
                        .OfType<Run>()
                        .Cast<OpenXmlElement>()
                        .LogicalChildrenContent()
                        .OfType<Text>()
                        .Select(t => t.Text)
                        .StringConcatenate();
                    Console.WriteLine("Paragraph text >{0}<", text);
                    continue;
                }
                // If element is not a paragraph, it must be a table.
                Console.WriteLine("Table");
            }
        }
    }
}

此示例不检查后代块级内容容器的运行,因此所设计的示例不显示文本框中的文本。

LogicalChildrenContent 轴方法的两个有用重载

您可以通过定义 LogicalChildrenContent 轴方法的两个其他重载来简化最后一个示例。常见的操作是检索段落的所有运行和检索运行的所有文本元素。因此,如果我们定义两个按指定标记名称筛选的其他扩展方法,则能进一步简化代码。

public static IEnumerable<XElement> LogicalChildrenContent(this XElement element,
    XName name)
{
    return element.LogicalChildrenContent().Where(e => e.Name == name);
}

public static IEnumerable<XElement> LogicalChildrenContent(
    this IEnumerable<XElement> source, XName name)
{
    foreach (XElement e1 in source)
        foreach (XElement e2 in e1.LogicalChildrenContent(name))
            yield return e2;
}

使用这些扩展方法时,查询简化情况如下。

var text = blockLevelContentElement
   .LogicalChildrenContent(W.r)
   .LogicalChildrenContent(W.t)
    .Select(t => (string)t)
    .StringConcatenate();

此查询生成与上一个示例相同的输出。

欢迎使用 Open XML SDK 2.0 for Microsoft Office 中实现的其他扩展方法如下所示。

public static IEnumerable<OpenXmlElement> LogicalChildrenContent(
    this OpenXmlElement element, System.Type typeName)
{
    return element.LogicalChildrenContent().Where(e => e.GetType() == typeName);
}

public static IEnumerable<OpenXmlElement> LogicalChildrenContent(
    this IEnumerable<OpenXmlElement> source, Type typeName)
{
    foreach (OpenXmlElement e1 in source)
        foreach (OpenXmlElement e2 in e1.LogicalChildrenContent(typeName))
            yield return e2;
}

简化后的查询如下所示。

var text = blockLevelContentElement
   .LogicalChildrenContent(typeof(Run))
   .LogicalChildrenContent(typeof(Text))
   .OfType<Text>()
    .Select(t => t.Text)
    .StringConcatenate();

LogicalChildrenContent 方法返回的 XML 元素的标识

有一个关于 LogicalChildrenContent 方法返回的元素的重要说明需要指出来。元素是 WordprocessingML 文档中的实际元素,不是副本也不是克隆。这意味着如果您要针对样式的各种属性进行额外的筛选,则很容易实现。

在文档中搜索文本

我们现在可以编写一个示例,以搜索文档中的某个特定字符串。如果文档包含修订跟踪、内容控件、超链接或任何组合段落文本时存在问题的其他元素,此示例也会正常运行。另外,它能够正确查找跨块级内容容器的文本。

static void IterateContentAndSearch(XElement element, string searchString)
{
    if (element.Name == W.p)
    {
        string paragraphText = element
            .LogicalChildrenContent(W.r)
            .LogicalChildrenContent(W.t)
            .Select(s => (string)s)
            .StringConcatenate();
        if (paragraphText.Contains(searchString))
            Console.WriteLine("Found {0}, paragraph: >{1}<", searchString, paragraphText);
    }
    foreach (XElement item in element.LogicalChildrenContent())
        IterateContentAndSearch(item, searchString);
}

static void Main(string[] args)
{
    byte[] docByteArray = File.ReadAllBytes("Test.docx");
    using (MemoryStream memoryStream = new MemoryStream())
    {
        memoryStream.Write(docByteArray, 0, docByteArray.Length);
        using (WordprocessingDocument doc =
            WordprocessingDocument.Open(memoryStream, true))
        {
            RevisionAccepter.AcceptRevisions(doc);
            IterateContentAndSearch(doc.MainDocumentPart.GetXDocument().Root, "control");
        }
    }
}

使用 欢迎使用 Open XML SDK 2.0 for Microsoft Office 的相同示例如下所示。

static void IterateContentAndSearch(OpenXmlElement element, string searchString)
{
    if (element is Paragraph)
    {
        string paragraphText = element
            .LogicalChildrenContent(typeof(Run))
            .LogicalChildrenContent(typeof(Text))
            .OfType<Text>()
            .Select(s => s.Text)
            .StringConcatenate();
        if (paragraphText.Contains(searchString))
            Console.WriteLine("Found {0}, paragraph: >{1}<", searchString, paragraphText);
    }
    foreach (OpenXmlElement item in element.LogicalChildrenContent())
        IterateContentAndSearch(item, searchString);
}

static void Main(string[] args)
{
    byte[] docByteArray = File.ReadAllBytes("Test.docx");
    using (MemoryStream memoryStream = new MemoryStream())
    {
        memoryStream.Write(docByteArray, 0, docByteArray.Length);
        using (WordprocessingDocument doc =
            WordprocessingDocument.Open(memoryStream, true))
        {
            RevisionAccepter.AcceptRevisions(doc);
            IterateContentAndSearch(doc.MainDocumentPart.Document, "control");
        }
    }
}

结论

开发处理 Open XML WordprocessingML 的程序时,只考虑文档的实际内容通常非常有用。本文定义了我认为包含文档逻辑内容的元素。我还定义了轴方法 LogicalChildrenContent 的四种重载。

若要轻松且可靠地处理 Open XML WordprocessingML 文档,接受跟踪修订很重要。这使得我们编写的代码可以忽略 40 多个用于跟踪修订的元素和属性(包括一些具有复杂语义的元素和属性)。使用这些轴方法并接受跟踪修订将使我们可以编写能够可靠地从 Open XML WordprocessingML 文档提取内容的小程序。

其他资源

单击以获取代码  下载代码(该链接可能指向英文页面)

请参阅 MSDN 上的 Open XML 开发中心(该链接可能指向英文页面)以获得文章、操作方法视频和许多博客文章的链接。下面的链接提供 Open XML SDK 2.0 入门的重要信息:

-
下载:Open XML SDK 2.0(该链接可能指向英文页面)

-
文章:Creating Documents by Using the Open XML Format SDK 2.0 (Part 1 of 3)

-
文章:Creating Documents by Using the Open XML Format SDK 2.0 (Part 2 of 3)

-
文章:Creating Documents by Using the Open XML Format SDK 2.0 (Part 3 of 3)