在 Word 文档中查找和替换文本
纯 .NET 解决方案,用于对 Word 文档(DOCX 文件格式)执行查找和替换文本
(最后更新于 2016 年 6 月 14 日)
引言
在 Word 文档中搜索文本并将其替换为 .NET 应用程序中的文本是一项相当常见的任务。本文将介绍我们可以使用的各种方法,并展示我们如何仅使用 .NET Framework(不使用任何第三方代码)来搜索和替换 Word 文档中的文本。要理解实现细节,需要具备 WordprocessingML 的基本知识。
详细说明
如果我们可以选择使用 Word 自动化(这需要安装 MS Word),那么我们可以通过 Word Interop 提供的 API 来实现查找和替换功能,如此处所示。
另一种方法是将 DOCX 文件(document.xml)的整个主部分读取为 string
,然后对其进行查找和替换,如此处所示。这种简单的方法可能足够了,但当要搜索的文本不是单个 XML 元素的值时,就会出现问题。例如,考虑以下 DOCX 文件:
文档的主部分看起来会像这样:
<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.StartIndex
和 FlatText.EndIndex
表示 FlatText
在 FlatTextRange
文本中的位置)。
步骤
-
打开 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(); } // ... }
-
遍历支持的文档部分(正文、页眉、页脚、注释、尾注和脚注,这些都加载为
XDocument
对象)的Run
元素,并创建FlatTextRange
和FlatText
实例。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); } } } // ... }
-
展平
Run
元素,该操作将单个 Run 元素拆分为多个连续的 Run 元素,每个 Run 元素只有一个内容子元素(以及可选的第一个RunProperties
子元素)。从展平的Run
元素创建FlatText
对象。图 2:展平的 Run 元素图 3:Flat 对象图 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; } // ... }
-
在
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; } } // ... }
- 最后,
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 PowerTools(TextReplacer
类)中使用。
然而,这两种算法的问题在于它们不适用于跨越多个段落的内容。在这种情况下,我们需要展平 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 日:初始版本