Find and Replace Text in a Word Document

From the following link, you can download the latest version of C# source code for the FindAndReplace .NET application.

Introduction

Searching a Word document's text and replacing it with text from a .NET application is a rather a common task. This article will mention the various approaches, that we can use and also show, how we can search and replace Word document's text, using only .NET Framework (without using any third-party code). To follow the implementation details, a basic knowledge of WordprocessingML is required.



Details

If we have the option to use Word Automation (which requires having MS Word installed), then we can achieve the find and replace functionality with an API, provided by Word Interop, as demonstrated here.

Another way would be to read the whole main part of the DOCX file (document.xml) as a string and perform a find and replace on it, as demonstrated here. This simple approach may be enough, but a problem occurs, when the searched text is not the value of single XML element, for example, consider the following DOCX file:

Hello World sample document
Figure 1- Hello World sample document

The document's main part will look, as shown below:

  1. <p>  
  2.   <r>  
  3.     <rPr><color val="FF0000"/></rPr>  
  4.     <t>Hello </t>  
  5.   </r>  
  6.   <r>  
  7.     <rPr><color val="0000FF"/></rPr>  
  8.     <t>World</t>  
  9.   </r>  
  10. </p>  
Another situation, for example, is shown below:
  1. <p>  
  2.   <r>  
  3.     <t>Hello</t>  
  4.     <t> </t>  
  5.     <t>World</t>  
  6.   </r>  
  7. </p>  
Therefore, the text, that we're looking for inside our Word document, may span over multiple elements and we need to consider this, when searching for it.

Implementation

We'll open the Word document and present it with a FlatDocument object. This object will read the document parts (like the body, headers, footers, comments, etc.) and store them as a collection of XDocument objects.

The FlatDocument object will also create a set of FlatTextRange objects, that represent searchable parts of the document's text content (a single FlatTextRange can represent a single paragraph, a single hyperlink etc.). Each FlatTextRange will contain FlatText objects, that have an indexed text content (FlatText. StartIndex and FlatText.EndIndex represent the FlatText's text location inside the FlatTextRange's text).

Steps 
  1. Open Word document.
    1. public sealed class FlatDocument : IDisposable  
    2. {  
    3.     public FlatDocument(string path) :  
    4.         this(File.Open(path, FileMode.Open, FileAccess.ReadWrite)) { }  
    5.   
    6.     public FlatDocument(Stream stream)  
    7.     {  
    8.         this.documents = XDocumentCollection.Open(stream);  
    9.         this.ranges = new List<FlatTextRange>();  
    10.   
    11.         this.CreateFlatTextRanges();  
    12.     }  
    13.   
    14.     // ...  
    15. }  
  2. Iterate through the run; elements of the supported document parts (body, headers, footers, comments, endnotes and footnotes, which are loaded as XDocument objects), create FlatTextRange and FlatText instances.
    1. public sealed class FlatDocument : IDisposable  
    2. {  
    3.     private void CreateFlatTextRanges()  
    4.     {  
    5.         foreach (XDocument document in this.documents)  
    6.         {  
    7.             FlatTextRange currentRange = null;  
    8.             foreach (XElement run in document.Descendants(FlatConstants.RunElementName))  
    9.             {  
    10.                 if (!run.HasElements)  
    11.                     continue;  
    12.   
    13.                 FlatText flatText = FlattenRunElement(run);  
    14.                 if (flatText == null)  
    15.                     continue;  
    16.   
    17.                 // If the current Run doesn't belong to the same parent  
    18.                 // (like a paragraph, hyperlink, etc.),  
    19.                 // create a new FlatTextRange, otherwise use the current one.  
    20.                 if (currentRange == null || currentRange.Parent != run.Parent)  
    21.                     currentRange = this.CreateFlatTextRange(run.Parent);  
    22.                 currentRange.AddFlatText(flatText);  
    23.             }  
    24.         }  
    25.     }  
    26.   
    27.     // ...  
    28. }  
  3. Flatten run elements, which splits a single run element into the multiple sequential run elements, that has a single content child element (and optionally the first RunProperties child element). Create a FlatText object from the flat run element.

    Flatten Run element
    Figure 2 - Flatten run element is depicted.

    Flat objects
    Figure 3 - Flat objects is depicted.

    Flat objects text content
    Figure 4 - Flat objects text content is depicted.
    1. public sealed class FlatDocument : IDisposable  
    2. {  
    3.     private static FlatText FlattenRunElement(XElement run)  
    4.     {  
    5.         XElement[] childs = run.Elements().ToArray();  
    6.         XElement runProperties = childs[0].Name == FlatConstants.RunPropertiesElementName ?  
    7.             childs[0] : null;  
    8.   
    9.         int childCount = childs.Length;  
    10.         int flatChildCount = 1 + (runProperties != null ? 1 : 0);  
    11.   
    12.         // Break the current Run into multiple Run elements that have one child,  
    13.         // or two children if it has RunProperties element as a first child.  
    14.         while (childCount > flatChildCount)  
    15.         {  
    16.             // Move the last child element from the current Run into the new Run,  
    17.             // which is added after the current Run.  
    18.             XElement child = childs[childCount - 1];  
    19.             run.AddAfterSelf(  
    20.                 new XElement(FlatConstants.RunElementName,  
    21.                     runProperties != null ? new XElement(runProperties) : null,  
    22.                     new XElement(child)));  
    23.   
    24.             child.Remove();  
    25.             --childCount;  
    26.         }  
    27.   
    28.         XElement remainingChild = childs[childCount - 1];  
    29.         return remainingChild.Name == FlatConstants.TextElementName ?  
    30.             new FlatText(remainingChild) : null;  
    31.     }  
    32.   
    33.     // ...  
    34. }  
  4. Perform find and replace over FlatTextRange instances.
    1. public sealed class FlatDocument : IDisposable  
    2. {  
    3.     public void FindAndReplace(string find, string replace)  
    4.     {  
    5.         this.FindAndReplace(find, replace, StringComparison.CurrentCulture);  
    6.     }  
    7.   
    8.     public void FindAndReplace(string find, string replace, StringComparison comparisonType)  
    9.     {  
    10.         this.ranges.ForEach(range => range.FindAndReplace(find, replace, comparisonType));  
    11.     }  
    12.   
    13.     // ...  
    14. }  
    15.    
    16. internal sealed class FlatTextRange  
    17. {  
    18.     public void FindAndReplace(string find, string replace, StringComparison comparisonType)  
    19.     {  
    20.         int searchStartIndex = -1, searchEndIndex = -1, searchPosition = 0;  
    21.         while ((searchStartIndex =  
    22.             this.rangeText.ToString().IndexOf(find, searchPosition, comparisonType)) != -1)  
    23.         {  
    24.             searchEndIndex = searchStartIndex + find.Length - 1;  
    25.   
    26.             // Find FlatText that contains the beginning of the searched text.  
    27.             LinkedListNode<FlatText> node = this.FindNode(searchStartIndex);  
    28.             FlatText flatText = node.Value;  
    29.   
    30.             ReplaceText(flatText, searchStartIndex, searchEndIndex, replace);  
    31.   
    32.             // Remove next FlatTexts that contain parts of the searched text.  
    33.             this.RemoveNodes(node, searchEndIndex);  
    34.   
    35.             this.ResetRangeText();  
    36.             searchPosition = searchStartIndex + replace.Length;  
    37.         }  
    38.     }  
    39.   
    40.     // ...  
    41. }  

Finally, FlatDocument.Dispose will save the XDocument parts and close the Word document.

Usage

The following sample code demonstrates, how to use FlatDocument.

  1. class Program  
  2. {  
  3.     static void Main(string[] args)  
  4.     {  
  5.         // Open the Word file.  
  6.         using (var flatDocument = new FlatDocument("Sample.docx"))  
  7.         {  
  8.             // Search and replace the document's text content.  
  9.             flatDocument.FindAndReplace("Hello Word""New Value 1");  
  10.             flatDocument.FindAndReplace("Foo Bar""New Value 2");  
  11.             // ...  
  12.               
  13.             // Save the Word file on Dispose.  
  14.         }  
  15.     }  
  16. }  
Points of Interest

An alternative algorithm to the one, shown above is to split a single run element into multiple sequential run elements, that have a single child (the same as above), but in this case a single child element would contain only a single character.
  1. <p>  
  2.   <r>  
  3.     <t>H</t>  
  4.   </r>  
  5.   <r>  
  6.     <t>e</t>  
  7.   </r>  
  8.   <r>  
  9.     <t>l</t>  
  10.   </r>  
  11.   <r>  
  12.     <t>l</t>  
  13.   </r>  
  14.   <r>  
  15.     <t>o</t>  
  16.   </r>  
  17.   <!--  
  18.       ...  
  19.   -->  
  20. </p>  
We would then iterate through those elements, while looking for a sequence of matched characters. You can find the details and an implementation of this approach in the article.

This approach is actually used in the Open XML PowerTools (TextReplacer class).

However, the problem with both of these algorithms is that they do not work on the content, that spans over multiple paragraphs. In this case, we would need to flatten the entire content of the Word document to search for the required text successfully. GemBox.Document is a .NET component for processing Word files, that presents a document with a content model hierarchy, that can be accessed as a flat content through the ContentRange class. With it, we are able to search for the content, that spans over multiple paragraphs. For details, see the article.

With this approach, we are actually able to find any arbitrary content and replace it with any desired content (including tables, pictures, paragraphs, HTML formatted text, RTF formatted text, etc.).

Improvements 
  • Currently the replace text will have the same formatting, as used at the beginning of the found text. However, we can consider providing a FindAndReplace overload method, that will accept the desired formatting (for example, something like: FlatDocument.FindAndReplace (string find, string replace, TextFormat format)). When the formatting is provided, we need to create a new RunProperties element, based on it.

  • Currently, any special characters (like tabs, line breaks, non-breaking hyphens, etc.) in both the search and replace texts are not considered. For this, FlatText should be aware of the different element types, that FlatText.textElement can be (like <tab/>, <br/>, <noBreakHyphen/>, etc.) and return the appropriate FlatText.Text value, based on it.

  • Please feel free to post other suggestions for improvements in the comments!