在.NET应用程序中搜索和替换Word文档的文本是一个常见的任务。本文将介绍几种可以实现这一功能的方法,并展示如何仅使用.NET框架(不使用任何第三方代码)来搜索和替换Word文档的文本。要理解实现细节,需要具备基本的WordprocessingML知识。
如果可以选择使用Word自动化(这需要安装MS Word),那么可以使用Word互操作提供的API来实现查找和替换功能,如这里所示。另一种方法是读取整个DOCX文件的主部分(document.xml)作为字符串,并在它上面执行查找和替换,如这里所示。这种简单的方法可能足够了,但当搜索的文本不是单个XML元素的值时,就会出现问题,例如,考虑以下DOCX文件:
文档的主部分看起来可能如下所示:
Hello
World
另一种情况是:
Hello
World
因此,在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();
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;
// 如果当前Run不属于同一个父级(如段落、超链接等),则创建一个新的FlatTextRange,否则使用当前的一个。
if (currentRange == null || currentRange.Parent != run.Parent)
currentRange = this.CreateFlatTextRange(run.Parent);
currentRange.AddFlatText(flatText);
}
}
}
// ...
}
压平Run元素,将单个Run元素分割成多个连续的Run元素,每个元素有一个单一的内容子元素(可选地是第一个RunProperties子元素)。从压平的Run元素创建一个FlatText对象:
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);
// 将当前Run分解成多个Run元素,每个元素有一个子元素,
// 或者如果它有RunProperties元素作为第一个子元素,则有两个子元素。
while (childCount > flatChildCount)
{
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));
}
// ...
}
最后,FlatDocument.Dispose将保存XDocument部分并关闭Word文档。
以下示例代码展示了如何使用FlatDocument:
class Program
{
static void Main(string[] args)
{
// 打开Word文件。
using (var flatDocument = new FlatDocument("Sample.docx"))
{
// 搜索并替换文档的文本内容。
flatDocument.FindAndReplace("Hello Word", "New Value 1");
flatDocument.FindAndReplace("Foo Bar", "New Value 2");
// ...
// 保存Word文件。
}
}
}
上述算法的一个替代方法是将单个Run元素分割成多个连续的Run元素,每个元素有一个子元素(与上述相同),但在这种情况下,一个单一的子元素将只包含一个单一的字符:
H
e
l
l
o
然后将迭代这些元素,寻找匹配字符的序列。可以在以下文章中找到这种方法的详细信息和实现:
在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可以是的不同元素类型(如
、