【问题标题】:Replace Text in Word document using Open Xml使用 Open Xml 替换 Word 文档中的文本
【发布时间】:2013-08-21 10:21:00
【问题描述】:

我已经从 word 模板创建了一个 docx 文件,现在我正在访问复制的 docx 文件并想用一些其他数据替换某些文本。

我无法获得有关如何从文档主要部分访问文本的提示?

任何帮助都将不胜感激。

下面是我到现在为止的代码。

private void CreateSampleWordDocument()
    {
        //string sourceFile = Path.Combine("D:\\GeneralLetter.dot");
        //string destinationFile = Path.Combine("D:\\New.doc");
        string sourceFile = Path.Combine("D:\\GeneralWelcomeLetter.docx");
        string destinationFile = Path.Combine("D:\\New.docx");
        try
        {
            // Create a copy of the template file and open the copy
            File.Copy(sourceFile, destinationFile, true);
            using (WordprocessingDocument document = WordprocessingDocument.Open(destinationFile, true))
            {
                // Change the document type to Document
                document.ChangeDocumentType(DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
                //Get the Main Part of the document
                MainDocumentPart mainPart = document.MainDocumentPart;
                mainPart.Document.Save();
            }
        }
        catch
        {
        }
    }

现在如何找到某些文本并替换它? 我无法通过链接获得,所以一些代码提示会很明显。

【问题讨论】:

    标签: templates c#-4.0 openxml


    【解决方案1】:

    只是为了让你知道如何做,请尝试:

      using ( WordprocessingDocument doc =
                        WordprocessingDocument.Open(@"yourpath\testdocument.docx", true))
                {
                    var body = doc.MainDocumentPart.Document.Body;
                    var paras = body.Elements<Paragraph>();
    
                    foreach (var para in paras)
                    {
                        foreach (var run in para.Elements<Run>())
                        {
                            foreach (var text in run.Elements<Text>())
                            {
                                if (text.Text.Contains("text-to-replace"))
                                {
                                    text.Text = text.Text.Replace("text-to-replace", "replaced-text");
                                }
                            }
                        }
                    }
                }
            }
    

    请注意文本区分大小写。替换后文本格式不会更改。希望对您有所帮助。

    【讨论】:

    • 我曾要求您回答我之前的问题以及您的链接对我有帮助,所以也在那里发布答案。
    • @flowerking :如果你有几分钟的时间,你能帮忙吗? stackoverflow.com/questions/26307691
    • 这只会在一次运行中替换文本。但是,文本可能会在不同的运行中被分割,在替换之前必须先将其连接起来。
    • 查看下面 sersat 的回复以获得更强大的解决方案:stackoverflow.com/a/31643162/421195
    【解决方案2】:

    也许这个解决方案更简单:
    1.StreamReader阅读所有文字,
    2. 使用Regex 不区分大小写地替换新文本而不是旧文本
    3.StreamWriter将修改后的文本再次写入文档。

     using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(document, true))
    {
        string docText = null;
        using (StreamReader sr = new StreamReader(wordDoc.MainDocumentPart.GetStream()))
            docText = sr.ReadToEnd();
    
        foreach (var t in findesReplaces)
            docText = new Regex(findText, RegexOptions.IgnoreCase).Replace(docText, replaceText);
    
        using (StreamWriter sw = new StreamWriter(wordDoc.MainDocumentPart.GetStream(FileMode.Create)))
            sw.Write(docText);
    }
    

    【讨论】:

    • @Roy 你认为现在更好吗?
    • 是的。感谢您为您的好答案添加说明
    • 如果我需要用“Paul Jones”替换“{{FullName}}”之类的内容,这将不起作用
    【解决方案3】:

    除了Flowerking的回答:

    当您的 Word 文件中有文本框时,他的解决方案将不起作用。因为文本框有TextBoxContent元素所以不会出现在Runs的foreach循环中。

    但是写的时候

    using ( WordprocessingDocument doc =
                        WordprocessingDocument.Open(@"yourpath\testdocument.docx", true))
    {
        var document = doc.MainDocumentPart.Document
    
        foreach (var text in document.Descendants<Text>()) // <<< Here
        {
            if (text.Text.Contains("text-to-replace"))
            {
                text.Text = text.Text.Replace("text-to-replace", "replaced-text");
            }
        } 
    }
            
    

    它将循环文档中的所有文本(无论是否在文本框中),因此它将替换文本。

    请注意,如果文本在 Runs 或 Textboxes 之间拆分,这也不起作用。对于这些情况,您需要更好的解决方案。拆分文本的一种解决方案是修复“模板”,有时,只需删除占位符并重新创建它就可以了。

    【讨论】:

    • 注意:你需要是using DocumentFormat.OpenXml.Wordprocessing(我的智能感知是建议一堆其他的东西)。
    • 这也有效: var text = doc.MainDocumentPart.Document.Descendants().Where(t => t.Text.Contains("text-to-replace")).FirstOrDefault ();
    • 文本被分割成多个运行是很常见的(尽管每个运行具有相同的属性)。除其他外,这是由拼写/语法检查器和编辑尝试次数引起的。当占位符被分隔时,文本拆分更常见,例如 [customer-name] 等。为了替换占位符,而不是使用标准文本,最好使用内容控件,呈现为 元素。在 content-controls 中,tag-name 文本永远不会被拆分,并且总是可以找到。
    • @mdc 对于内容控件,标签名称与文本不同。标签名称从不拆分。对于纯文本内容控件,在 XML 中,它是字段 ...。内容控件始终可以使用标签名称来识别。内容控制文本可以拆分为与段落中的文本可以拆分为多个运行相同的方式。
    • @mdc 可以在 1) 在 XML 中看到标记名 2) 在 Word 开发人员标记中的内容控件的属性中 3) 当您通过 OpenXml 找到内容控件时。不应更改标记名称 - 它是查找内容控件然后对其进行处理的 ID,通常用于替换文本或充当占位符以插入文本等。在我的答案中有更多详细信息 stackoverflow.com/a/66177392/5945199
    【解决方案4】:

    here 是来自 msdn 的解决方案。

    那里的例子:

    public static void SearchAndReplace(string document)
    {
        using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(document, true))
        {
            string docText = null;
            using (StreamReader sr = new StreamReader(wordDoc.MainDocumentPart.GetStream()))
            {
                docText = sr.ReadToEnd();
            }
    
            Regex regexText = new Regex("Hello world!");
            docText = regexText.Replace(docText, "Hi Everyone!");
    
            using (StreamWriter sw = new StreamWriter(wordDoc.MainDocumentPart.GetStream(FileMode.Create)))
            {
                sw.Write(docText);
            }
        }
    }
    

    【讨论】:

    • 如果 word 将您搜索的文本分成多个运行(或更糟),这基本上是无用的......
    • 我遇到了这个确切的问题@Santhos,甚至会抛出 RUN,我不知道 Word 在哪里拆分文本,这让我非常头疼。
    • @Eduardo 我的朋友试图解决它,但最终不得不手动完成所有运行并尝试撰写文本。如果 word 文件在您的控制之下,您可以编辑其 xml 并修复您需要替换的出现,以便它们不会跨越多次运行。
    【解决方案5】:

    如果您要查找的文本位于括号之间,并且 Word 会在多次运行中拆分您的文本...;

    搜索文本(ienumerable(of text))

    for (int i = 0; i <= SearchIn.Count - 1; i++) {
    
        if (!(i + 2 > SearchIn.Count - 1)) {
            Text TXT = SearchIn(i);
            Text TXT1 = SearchIn(i + 1);
            Text TXT2 = SearchIn(i + 2);
    
            if (Strings.Trim(TXT.Text) == "[" & Strings.Trim(TXT2.Text) == "]") {
                TXT1.Text = TXT.Text + TXT1.Text + TXT2.Text;
    
                TXT.Text = "";
                TXT2.Text = "";
            }
        }
    }
    

    【讨论】:

      【解决方案6】:

      这是一个可以跨文本运行(包括文本框)在打开的 xml(word)文档中查找和替换标签的解决方案

      namespace Demo
      {
          using System;
          using System.Collections.Generic;
          using System.IO;
          using System.Linq;
          using System.Text.RegularExpressions;
          using DocumentFormat.OpenXml.Packaging;
          using DocumentFormat.OpenXml.Wordprocessing;
      
          public class WordDocumentHelper
          {
              class DocumentTag
              {
                  public DocumentTag()
                  {
                      ReplacementText = "";
                  }
      
                  public string Tag { get; set; }
                  public string Table { get; set; }
                  public string Column { get; set; }
                  public string ReplacementText { get; set; }
      
                  public override string ToString()
                  {
                      return ReplacementText ?? (Tag ?? "");
                  }
              }
      
              private const string TAG_PATTERN = @"\[(.*?)[\.|\:](.*?)\]";
              private const string TAG_START = @"[";
              private const string TAG_END = @"]";
      
              /// <summary>
              /// Clones a document template into the temp folder and returns the newly created clone temp filename and path.
              /// </summary>
              /// <param name="templatePath"></param>
              /// <returns></returns>
              public string CloneTemplateForEditing(string templatePath)
              {
                  var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) + Path.GetExtension(templatePath);
                  File.Copy(templatePath, tempFile);
                  return tempFile;
              }
      
              /// <summary>
              /// Opens a given filename, replaces tags, and saves. 
              /// </summary>
              /// <param name="filename"></param>
              /// <returns>Number of tags found</returns>
              public int FindAndReplaceTags(string filename)
              {
                  var allTags = new List<DocumentTag>();
      
                  using (WordprocessingDocument doc = WordprocessingDocument.Open(path: filename, isEditable: true))
                  {
                      var document = doc.MainDocumentPart.Document;
      
                      // text may be split across multiple text runs so keep a collection of text objects
                      List<Text> tagParts = new List<Text>();
      
                      foreach (var text in document.Descendants<Text>())
                      {
                          // search for any fully formed tags in this text run
                          var fullTags = GetTags(text.Text);
      
                          // replace values for fully formed tags
                          fullTags.ForEach(t => {
                              t = GetTagReplacementValue(t);
                              text.Text = text.Text.Replace(t.Tag, t.ReplacementText);
                              allTags.Add(t);
                          });
      
                          // continue working on current partial tag
                          if (tagParts.Count > 0)
                          {
                              // working on a tag
                              var joinText = string.Join("", tagParts.Select(x => x.Text)) + text.Text;
      
                              // see if tag ends with this block
                              if (joinText.Contains(TAG_END))
                              {
                                  var joinTag = GetTags(joinText).FirstOrDefault(); // should be just one tag (or none)
                                  if (joinTag == null)
                                  {
                                      throw new Exception($"Misformed document tag in block '{string.Join("", tagParts.Select(x => x.Text)) + text.Text}' ");
                                  }
      
                                  joinTag = GetTagReplacementValue(joinTag);
                                  allTags.Add(joinTag);
      
                                  // replace first text run in the tagParts set with the replacement value. 
                                  // (This means the formatting used on the first character of the tag will be used)
                                  var firstRun = tagParts.First();
                                  firstRun.Text = firstRun.Text.Substring(0, firstRun.Text.LastIndexOf(TAG_START));
                                  firstRun.Text += joinTag.ReplacementText;
      
                                  // replace trailing text runs with empty strings
                                  tagParts.Skip(1).ToList().ForEach(x => x.Text = "");
      
                                  // replace all text up to and including the first index of TAG_END
                                  text.Text = text.Text.Substring(text.Text.IndexOf(TAG_END) + 1);
      
                                  // empty the tagParts list so we can start on a new tag
                                  tagParts.Clear();
                              }
                              else
                              {
                                  // no tag end so keep getting text runs
                                  tagParts.Add(text);
                              }
                          }
      
                          // search for new partial tags
                          if (text.Text.Contains("["))
                          {
                              if (tagParts.Any())
                              {
                                  throw new Exception($"Misformed document tag in block '{string.Join("", tagParts.Select(x => x.Text)) + text.Text}' ");
                              }
                              tagParts.Add(text);
                              continue;
                          }
      
                      }
      
                      // save the temp doc before closing
                      doc.Save();
                  }
      
                  return allTags.Count;
              }
      
              /// <summary>
              /// Gets a unique set of document tags found in the passed fileText using Regex
              /// </summary>
              /// <param name="fileText"></param>
              /// <returns></returns>
              private List<DocumentTag> GetTags(string fileText)
              {
                  List<DocumentTag> tags = new List<DocumentTag>();
      
                  if (string.IsNullOrWhiteSpace(fileText))
                  {
                      return tags;
                  }
      
                  // TODO: custom regex for tag matching 
                  // this example looks for tags in the formation "[table.column]" or "[table:column]" and captures the full tag, "table", and "column" into match Groups
                  MatchCollection matches = Regex.Matches(fileText, TAG_PATTERN);
                  foreach (Match match in matches)
                  {
                      try
                      {
      
                          if (match.Groups.Count < 3
                              || string.IsNullOrWhiteSpace(match.Groups[0].Value)
                              || string.IsNullOrWhiteSpace(match.Groups[1].Value)
                              || string.IsNullOrWhiteSpace(match.Groups[2].Value))
                          {
                              continue;
                          }
      
                          tags.Add(new DocumentTag
                          {
                              Tag = match.Groups[0].Value,
                              Table = match.Groups[1].Value,
                              Column = match.Groups[2].Value
                          });
                      }
                      catch
                      {
      
                      }
                  }
      
                  return tags;
              }
      
              /// <summary>
              /// Set the Tag replacement value of the pasted tag
              /// </summary>
              /// <returns></returns>
              private DocumentTag GetTagReplacementValue(DocumentTag tag)
              {
                  // TODO: custom routine to update tag Replacement Value
      
                  tag.ReplacementText = "foobar";
      
                  return tag;
              }
          }
      }
      

      【讨论】:

        【解决方案7】:
        Dim doc As WordprocessingDocument = WordprocessingDocument.Open("Chemin", True, New OpenSettings With {.AutoSave = True})
        
        Dim d As Document = doc.MainDocumentPart.Document
        
        Dim txt As Text = d.Descendants(Of Text).Where(Function(t) t.Text = "txtNom").FirstOrDefault
        
        If txt IsNot Nothing Then
         txt.Text = txt.Text.Replace("txtNom", "YASSINE OULARBI")
        End If
        
        doc.Close()
        

        【讨论】:

          【解决方案8】:

          我的课用于替换word文档中的长短语,该单词分成不同的文本块:

          类本身:

          using System.Collections.Generic;
          using System.Text;
          using DocumentFormat.OpenXml.Packaging;
          using DocumentFormat.OpenXml.Wordprocessing;
          
          namespace WebBackLibrary.Service
          {
              public class WordDocumentService
              {
                  private class WordMatchedPhrase
                  {
                      public int charStartInFirstPar { get; set; }
                      public int charEndInLastPar { get; set; }
          
                      public int firstCharParOccurance { get; set; }
                      public int lastCharParOccurance { get; set; }
                  }
          
                  public WordprocessingDocument ReplaceStringInWordDocumennt(WordprocessingDocument wordprocessingDocument, string replaceWhat, string replaceFor)
                  {
                      List<WordMatchedPhrase> matchedPhrases = FindWordMatchedPhrases(wordprocessingDocument, replaceWhat);
          
                      Document document = wordprocessingDocument.MainDocumentPart.Document;
                      int i = 0;
                      bool isInPhrase = false;
                      bool isInEndOfPhrase = false;
                      foreach (Text text in document.Descendants<Text>()) // <<< Here
                      {
                          char[] textChars = text.Text.ToCharArray();
                          List<WordMatchedPhrase> curParPhrases = matchedPhrases.FindAll(a => (a.firstCharParOccurance.Equals(i) || a.lastCharParOccurance.Equals(i)));
                          StringBuilder outStringBuilder = new StringBuilder();
                          
                          for (int c = 0; c < textChars.Length; c++)
                          {
                              if (isInEndOfPhrase)
                              {
                                  isInPhrase = false;
                                  isInEndOfPhrase = false;
                              }
          
                              foreach (var parPhrase in curParPhrases)
                              {
                                  if (c == parPhrase.charStartInFirstPar && i == parPhrase.firstCharParOccurance)
                                  {
                                      outStringBuilder.Append(replaceFor);
                                      isInPhrase = true;
                                  }
                                  if (c == parPhrase.charEndInLastPar && i == parPhrase.lastCharParOccurance)
                                  {
                                      isInEndOfPhrase = true;
                                  }
          
                              }
                              if (isInPhrase == false && isInEndOfPhrase == false)
                              {
                                  outStringBuilder.Append(textChars[c]);
                              }
                          }
                          text.Text = outStringBuilder.ToString();
                          i = i + 1;
                      }
          
                      return wordprocessingDocument;
                  }
          
                  private List<WordMatchedPhrase> FindWordMatchedPhrases(WordprocessingDocument wordprocessingDocument, string replaceWhat)
                  {
                      char[] replaceWhatChars = replaceWhat.ToCharArray();
                      int overlapsRequired = replaceWhatChars.Length;
                      int overlapsFound = 0;
                      int currentChar = 0;
                      int firstCharParOccurance = 0;
                      int lastCharParOccurance = 0;
                      int startChar = 0;
                      int endChar = 0;
                      List<WordMatchedPhrase> wordMatchedPhrases = new List<WordMatchedPhrase>();
                      //
                      Document document = wordprocessingDocument.MainDocumentPart.Document;
                      int i = 0;
                      foreach (Text text in document.Descendants<Text>()) // <<< Here
                      {
                          char[] textChars = text.Text.ToCharArray();
                          for (int c = 0; c < textChars.Length; c++)
                          {
                              char compareToChar = replaceWhatChars[currentChar];
                              if (textChars[c] == compareToChar)
                              {
                                  currentChar = currentChar + 1;
                                  if (currentChar == 1)
                                  {
                                      startChar = c;
                                      firstCharParOccurance = i;
                                  }
                                  if (currentChar == overlapsRequired)
                                  {
                                      endChar = c;
                                      lastCharParOccurance = i;
                                      WordMatchedPhrase matchedPhrase = new WordMatchedPhrase
                                      {
                                          firstCharParOccurance = firstCharParOccurance,
                                          lastCharParOccurance = lastCharParOccurance,
                                          charEndInLastPar = endChar,
                                          charStartInFirstPar = startChar
                                      };
                                      wordMatchedPhrases.Add(matchedPhrase);
                                      currentChar = 0;
                                  }
                              }
                              else
                              {
                                  currentChar = 0;
          
                              }
                          }
                          i = i + 1;
                      }
          
                      return wordMatchedPhrases;
          
                  }
          
              }
          }
          

          以及易于使用的示例:

          public void EditWordDocument(UserContents userContents)
                  {
                      string filePath = Path.Combine(userContents.PathOnDisk, userContents.FileName);
                      WordDocumentService wordDocumentService = new WordDocumentService();
                      if (userContents.ContentType.Contains("word") && File.Exists(filePath))
                      {
                          string saveAs = "modifiedTechWord.docx";
                          //
                          using (WordprocessingDocument doc = WordprocessingDocument.Open(filePath, true)) //open source word file
                          {
                              Document document = doc.MainDocumentPart.Document;
                              OpenXmlPackage res = doc.SaveAs(Path.Combine(userContents.PathOnDisk, saveAs)); // copy it
                              res.Close();
                          }
                          using (WordprocessingDocument doc = WordprocessingDocument.Open(Path.Combine(userContents.PathOnDisk, saveAs), true)) // open copy
                          {
                              string replaceWhat = "{transform:CandidateFio}";
                              string replaceFor = "ReplaceToFio";
                              var result = wordDocumentService.ReplaceStringInWordDocumennt(doc, replaceWhat, replaceFor); //replace words in copy
                          }
                      }
                  }
          

          【讨论】:

            【解决方案9】:

            到目前为止,我发现的最简单、最准确的方法是使用Open-Xml-PowerTools。就我个人而言,我使用的是 dotnet core,所以我使用this nuget package

            using OpenXmlPowerTools;
            // ...
            
            protected byte[] SearchAndReplace(byte[] file, IDictionary<string, string> translations)
            {
                WmlDocument doc = new WmlDocument(file.Length.ToString(), file);
            
                foreach (var translation in translations)
                    doc = doc.SearchAndReplace(translation.Key, translation.Value, true);
            
                return doc.DocumentByteArray;
            }
            

            使用示例:

            var templateDoc = File.ReadAllBytes("templateDoc.docx");
            var generatedDoc = SearchAndReplace(templateDoc, new Dictionary<string, string>(){
                {"text-to-replace-1", "replaced-text-1"},
                {"text-to-replace-2", "replaced-text-2"},
            });
            File.WriteAllBytes("generatedDoc.docx", generatedDoc);
            

            欲了解更多信息,请参阅Search and Replace Text in an Open XML WordprocessingML Document

            【讨论】:

            • 不起作用,它说param不能为0。发生在doc = doc.Search
            • 所以这对我有帮助,但我能够做得更简单。我不必将文件读入字节数组,也不必将其写出。只需按名称打开它,进行搜索和替换,然后保存它。它找到了所有内容(至少在我的第一次尝试中),包括部分单词为粗体且颜色不同的实例。
            【解决方案10】:

            我正在测试这个以生成文档,但我的占位符被拆分为运行和文本节点。我不想将整个文档加载为单个字符串以进行正则表达式查找/替换,因此我使用了 OpenXml api。我的想法是:

            1. 清理占位符节点作为对文档的一次性操作
            2. 每次生成时按节点值查找/替换,现在源是干净的。

            测试表明,占位符在运行和文本节点之间分开,但不是段落。我还发现后续占位符不共享文本节点,所以我没有处理。占位符遵循模式{{placeholder_name}}

            首先,我需要获取段落中的所有文本节点(根据@sertsedat):

                var nodes = paragraph.Descendants<Text>();
            

            测试表明此函数保留了顺序,这对我的用例来说是完美的,因为我可以遍历集合以查找开始/停止指示符,并将那些属于占位符的节点分组。

            分组函数在文本节点值中查找{{}},以识别属于占位符的应删除的节点,以及应忽略的其他节点。

            一旦找到一个节点的开始,所有后续节点,直到并包括终止,都需要删除(通过添加到TextNodes 列表中标记),这些节点的值包含在占位符 @ 987654326@,以及第一个/最后一个节点的不是占位符一部分的任何文本部分也需要保存(因此是字符串属性)。发现新占位符或序列末尾的任何不完整组都应引发错误。

            最后,我使用分组更新了原始文档

            foreach (var placeholder in GroupPlaceholders(paragraph.Descendants<Text>()))
            {
                var firstTextNode = placeholder.TextNodes[0];
                if (placeholder.PrecedingText != null)
                {
                    firstTextNode.Parent.InsertBefore(new Text(placeholder.PrecedingText), firstTextNode);
                }
                firstTextNode.Parent.InsertBefore(placeholder.PlaceholderText, firstTextNode);
                if (placeholder.SubsequentText != null)
                {
                    firstTextNode.Parent.InsertBefore(new Text(placeholder.SubsequentText), firstTextNode);
                }
                foreach (var textNode in placeholder.TextNodes) {
                    textNode.Remove();                      
                }
            }
            

            【讨论】:

              【解决方案11】:

              这里的大多数答案对于现实世界的文档都是错误的。

              有两种主要的解决方案。如果您可以控制源文档,请使用邮件合并字段进行查找/替换,而不是尝试使用文档中的文本。

              如果您不能使用邮件合并字段,解决方案是编写您自己的文本缓冲区来组合多个文本字段。这将允许您查找/替换文本字段之间拆分的文本,这种情况经常发生。

              由于可能发生的所有拆分组合,很难正确编写!但它已经为我工作了好几年,处理了数百万份文件。

              【讨论】:

              • 这没有提供问题的答案。要批评或要求作者澄清,请在他们的帖子下方留下评论。 - From Review
              • 表示两种解决方案。希望读者阅读此内容,并且他们不会尝试在现实世界中扩大规模时会导致麻烦的其他一些答案。如果审阅者不是该主题的领域专家,他们如何知道我的答案是否好?
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2019-12-12
              • 1970-01-01
              • 1970-01-01
              • 2012-04-11
              • 1970-01-01
              相关资源
              最近更新 更多