65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Word 文档中查找和替换文本

starIconstarIconstarIconstarIconstarIcon

5.00/5 (41投票s)

2016年6月14日

CPOL

4分钟阅读

viewsIcon

112705

downloadIcon

3210

纯 .NET 解决方案,用于对 Word 文档(DOCX 文件格式)执行查找和替换文本

(最后更新于 2016 年 6 月 14 日)

引言

在 Word 文档中搜索文本并将其替换为 .NET 应用程序中的文本是一项相当常见的任务。本文将介绍我们可以使用的各种方法,并展示我们如何仅使用 .NET Framework(不使用任何第三方代码)来搜索和替换 Word 文档中的文本。要理解实现细节,需要具备 WordprocessingML 的基本知识

Find And Replace Word Document

详细说明

如果我们可以选择使用 Word 自动化(这需要安装 MS Word),那么我们可以通过 Word Interop 提供的 API 来实现查找和替换功能,如此处所示。

另一种方法是将 DOCX 文件(document.xml)的整个主部分读取为 string,然后对其进行查找和替换,如此处所示。这种简单的方法可能足够了,但当要搜索的文本不是单个 XML 元素的值时,就会出现问题。例如,考虑以下 DOCX 文件:

图 1:Hello World 示例文档

文档的主部分看起来会像这样:

<p>
  <r>
    <rPr><color val="FF0000"/></rPr>
    <t>Hello </t>
  </r>
  <r>
    <rPr><color val="0000FF"/></rPr>
    <t>World</t>
  </r>
</p>

另一种情况,例如,是这样的:

<p>
  <r>
    <t>Hello</t>
    <t> </t>
    <t>World</t>
  </r>
</p>

因此,我们在 Word 文档中查找的文本可能跨越多个元素,我们在搜索时需要考虑这一点。

实现

我们将打开 Word 文档,并将其呈现为一个 FlatDocument 对象。该对象将读取文档部分(如正文、页眉、页脚、注释等),并将它们存储为 XDocument 对象的集合。

FlatDocument 对象还将创建一组 FlatTextRange 对象,这些对象表示文档文本内容的可搜索部分(单个 FlatTextRange 可以表示单个段落、单个超链接等)。每个 FlatTextRange 将包含 FlatText 对象,这些对象具有索引的文本内容(FlatText.StartIndexFlatText.EndIndex 表示 FlatTextFlatTextRange 文本中的位置)。

步骤

  1. 打开 Word 文档

    public sealed class FlatDocument : IDisposable
    {
        public FlatDocument(string path) :
            this(File.Open(path, FileMode.Open, FileAccess.ReadWrite)) { }
    
        public FlatDocument(Stream stream)
        {
            this.documents = XDocumentCollection.Open(stream);
            this.ranges = new List<FlatTextRange>();
    
            this.CreateFlatTextRanges();
        }
    
        // ...
    }
  2. 遍历支持的文档部分(正文、页眉、页脚、注释、尾注和脚注,这些都加载为 XDocument 对象)的 Run 元素,并创建 FlatTextRangeFlatText 实例。

    public sealed class FlatDocument : IDisposable
    {
        private void CreateFlatTextRanges()
        {
            foreach (XDocument document in this.documents)
            {
                FlatTextRange currentRange = null;
                foreach (XElement run in document.Descendants(FlatConstants.RunElementName))
                {
                    if (!run.HasElements)
                        continue;
    
                    FlatText flatText = FlattenRunElement(run);
                    if (flatText == null)
                        continue;
    
                    // If the current Run doesn't belong to the same parent
                    // (like a paragraph, hyperlink, etc.),
                    // create a new FlatTextRange, otherwise use the current one.
                    if (currentRange == null || currentRange.Parent != run.Parent)
                        currentRange = this.CreateFlatTextRange(run.Parent);
                    currentRange.AddFlatText(flatText);
                }
            }
        }
    
        // ...
    }
  3. 展平 Run 元素,该操作将单个 Run 元素拆分为多个连续的 Run 元素,每个 Run 元素只有一个内容子元素(以及可选的第一个 RunProperties 子元素)。从展平的 Run 元素创建 FlatText 对象。

    Flatten Run element

    图 2:展平的 Run 元素

    Flat Objects

    图 3:Flat 对象

    Flat Objects Text Content

    图 4:Flat 对象文本内容
    public sealed class FlatDocument : IDisposable
    {
        private static FlatText FlattenRunElement(XElement run)
        {
            XElement[] childs = run.Elements().ToArray();
            XElement runProperties = childs[0].Name == 
                                     FlatConstants.RunPropertiesElementName ?
                childs[0] : null;
    
            int childCount = childs.Length;
            int flatChildCount = 1 + (runProperties != null ? 1 : 0);
    
            // Break the current Run into multiple Run elements that have one child,
            // or two children if it has RunProperties element as a first child.
            while (childCount > flatChildCount)
            {
                // Move the last child element from the current Run into the new Run,
                // which is added after the current Run.
                XElement child = childs[childCount - 1];
                run.AddAfterSelf(
                    new XElement(FlatConstants.RunElementName,
                        runProperties != null ? new XElement(runProperties) : null,
                        new XElement(child)));
    
                child.Remove();
                --childCount;
            }
    
            XElement remainingChild = childs[childCount - 1];
            return remainingChild.Name == FlatConstants.TextElementName ?
                new FlatText(remainingChild) : null;
        }
    
        // ...
    }
  4. FlatTextRange 实例上执行查找和替换。

    public sealed class FlatDocument : IDisposable
    {
        public void FindAndReplace(string find, string replace)
        {
            this.FindAndReplace(find, replace, StringComparison.CurrentCulture);
        }
    
        public void FindAndReplace
        (string find, string replace, StringComparison comparisonType)
        {
            this.ranges.ForEach
            (range => range.FindAndReplace(find, replace, comparisonType));
        }
    
        // ...
    }
     
    internal sealed class FlatTextRange
    {
        public void FindAndReplace
        (string find, string replace, StringComparison comparisonType)
        {
            int searchStartIndex = -1, searchEndIndex = -1, searchPosition = 0;
            while ((searchStartIndex =
                this.rangeText.ToString().IndexOf
                     (find, searchPosition, comparisonType)) != -1)
            {
                searchEndIndex = searchStartIndex + find.Length - 1;
    
                // Find FlatText that contains the beginning of the searched text.
                LinkedListNode<FlatText> node = this.FindNode(searchStartIndex);
                FlatText flatText = node.Value;
    
                ReplaceText(flatText, searchStartIndex, searchEndIndex, replace);
    
                // Remove next FlatTexts that contain parts of the searched text.
                this.RemoveNodes(node, searchEndIndex);
    
                this.ResetRangeText();
                searchPosition = searchStartIndex + replace.Length;
            }
        }
    
        // ...
    }
  5. 最后,FlatDocument.Dispose 将保存 XDocument 部分并关闭 Word 文档。

用法

以下示例代码演示了如何使用 FlatDocument

class Program
{
    static void Main(string[] args)
    {
        // Open the Word file.
        using (var flatDocument = new FlatDocument("Sample.docx"))
        {
            // Search and replace the document's text content.
            flatDocument.FindAndReplace("Hello Word", "New Value 1");
            flatDocument.FindAndReplace("Foo Bar", "New Value 2");
            // ...
			
            // Save the Word file on Dispose.
        }
    }
}

关注点

上述算法的另一种替代方法是将单个 Run 元素拆分为多个连续的 Run 元素,每个 Run 元素只有一个子元素(与上述相同),但在这种情况下,单个子元素只包含一个字符

<p>
  <r>
    <t>H</t>
  </r>
  <r>
    <t>e</t>
  </r>
  <r>
    <t>l</t>
  </r>
  <r>
    <t>l</t>
  </r>
  <r>
    <t>o</t>
  </r>
  <!--
      ...
  -->
</p>

然后,我们将遍历这些元素,同时寻找匹配字符的序列。您可以在以下文章中找到此方法的详细信息和实现:在 Open XML WordprocessingML 文档中搜索和替换文本
这种方法实际上在Open XML PowerToolsTextReplacer 类)中使用。

然而,这两种算法的问题在于它们不适用于跨越多个段落的内容。在这种情况下,我们需要展平 Word 文档的全部内容,以成功搜索所需文本GemBox.Document 是一个用于处理 Word 文件的 .NET 组件,它提供了一个可以通过 ContentRange 类访问的内容模型层次结构。通过它,我们可以搜索跨越多个段落的内容。有关详细信息,请参阅以下文章:使用 C# 或 VB.NET 在 Word 中查找和替换
通过这种方法,我们实际上能够找到任意内容并将其替换为所需的任意内容(包括表格、图片、段落、HTML 格式文本、RTF 格式文本等)。

改进

  • 当前,替换文本的格式将与找到文本开头使用的格式相同。但是,我们可以考虑提供一个 FindAndReplace 重载方法,该方法接受所需的格式(例如:FlatDocument.FindAndReplace(string find, string replace, TextFormat format))。提供格式后,我们需要根据它创建一个新的 RunProperties 元素。
  • 目前,搜索和替换文本中的任何特殊字符(如制表符、换行符、不间断连字符等)均未被考虑。为此,FlatText 应了解 FlatText.textElement 可以是不同的元素类型(如 <tab/><br/><noBreakHyphen/> 等),并根据它返回适当的 FlatText.Text 值。
  • 请随时在评论中提出其他改进建议!

历史

  • 2016 年 6 月 14 日:初始版本
© . All rights reserved.