【问题标题】:Replace bookmark text in Word file using Open XML SDK使用 Open XML SDK 替换 Word 文件中的书签文本
【发布时间】:2010-07-22 11:27:01
【问题描述】:

我认为 v2.0 更好...他们有一些不错的“如何:...”examples 但书签的行为似乎不像表格那样明显...书签由两个 XML 元素BookmarkStartBookmarkEnd。我们有一些带有文本作为书签的模板,我们只是想用一些其他文本替换书签......没有奇怪的格式发生,但我如何选择/替换书签文本?

【问题讨论】:

    标签: c# ms-word openxml openxml-sdk


    【解决方案1】:

    以下是我以你们为灵感的方法:

      IDictionary<String, BookmarkStart> bookmarkMap = 
          new Dictionary<String, BookmarkStart>();
    
      foreach (BookmarkStart bookmarkStart in file.MainDocumentPart.RootElement.Descendants<BookmarkStart>())
      {
          bookmarkMap[bookmarkStart.Name] = bookmarkStart;
      }
    
      foreach (BookmarkStart bookmarkStart in bookmarkMap.Values)
      {
          Run bookmarkText = bookmarkStart.NextSibling<Run>();
          if (bookmarkText != null)
          {
              bookmarkText.GetFirstChild<Text>().Text = "blah";
          }
      }
    

    【讨论】:

    • 您在此处遵循一种非常简单的模式,但并非在所有情况下都适用。在许多情况下,书签替换会变得更加复杂,这不适用于此算法。
    • 这对我不起作用,它不会给我任何错误,我确认它阅读了书签,但没有用文本替换它们。
    • 这很好用,但如果您不能限制为纯文本、单个单词的书签运行,它会非常脆弱。一个示例:“BOOKMARK”将起作用,“BOOKMARK1”将找不到,因为它被分成“BOOKMARK”和“1”(在 Windows 10 桌面版上使用 Microsoft Word 2016 测试),最终被部分替换。
    【解决方案2】:

    用单个内容(可能是多个文本块)替换书签。

    public static void InsertIntoBookmark(BookmarkStart bookmarkStart, string text)
    {
        OpenXmlElement elem = bookmarkStart.NextSibling();
    
        while (elem != null && !(elem is BookmarkEnd))
        {
            OpenXmlElement nextElem = elem.NextSibling();
            elem.Remove();
            elem = nextElem;
        }
    
        bookmarkStart.Parent.InsertAfter<Run>(new Run(new Text(text)), bookmarkStart);
    }
    

    首先,移除 start 和 end 之间的现有内容。然后直接在开始之后(结束之前)添加一个新的运行。

    但是,不确定书签在打开时是否在另一个部分或不同的表格单元格等中关闭。..

    对我来说现在已经足够了。

    【讨论】:

    • 注意,我已经翻译了这个答案(在 Google 的 很多 的帮助下)。请检查其准确性。以后请用英文发帖。
    • 这对我有用,只需确保添加以下行以将更改保存回您的文档 file.MainDocumentPart.Document.Save();文件.关闭(); file 是您使用 WordprocessingDocument.Open("path", true) 打开的文件
    【解决方案3】:

    经过很多小时,我写了这个方法:

        Public static void ReplaceBookmarkParagraphs(WordprocessingDocument doc, string bookmark, string text)
        {
            //Find all Paragraph with 'BookmarkStart' 
            var t = (from el in doc.MainDocumentPart.RootElement.Descendants<BookmarkStart>()
                     where (el.Name == bookmark) &&
                     (el.NextSibling<Run>() != null)
                     select el).First();
            //Take ID value
            var val = t.Id.Value;
            //Find the next sibling 'text'
            OpenXmlElement next = t.NextSibling<Run>();
            //Set text value
            next.GetFirstChild<Text>().Text = text;
    
            //Delete all bookmarkEnd node, until the same ID
            deleteElement(next.GetFirstChild<Text>().Parent, next.GetFirstChild<Text>().NextSibling(), val, true);
        }
    

    之后,我调用:

    Public static bool deleteElement(OpenXmlElement parentElement, OpenXmlElement elem, string id, bool seekParent)
    {
        bool found = false;
    
        //Loop until I find BookmarkEnd or null element
        while (!found && elem != null && (!(elem is BookmarkEnd) || (((BookmarkEnd)elem).Id.Value != id)))
        {
            if (elem.ChildElements != null && elem.ChildElements.Count > 0)
            {
                found = deleteElement(elem, elem.FirstChild, id, false);
            }
    
            if (!found)
            {
                OpenXmlElement nextElem = elem.NextSibling();
                elem.Remove();
                elem = nextElem;
            }
        }
    
        if (!found)
        {
            if (elem == null)
            {
                if (!(parentElement is Body) && seekParent)
                {
                    //Try to find bookmarkEnd in Sibling nodes
                    found = deleteElement(parentElement.Parent, parentElement.NextSibling(), id, true);
                }
            }
            else
            {
                if (elem is BookmarkEnd && ((BookmarkEnd)elem).Id.Value == id)
                {
                    found = true;
                }
            }
        }
    
        return found;
    }
    

    如果您没有空书签,此代码运行良好。 我希望它可以帮助某人。

    【讨论】:

      【解决方案4】:

      我在 10 分钟前才发现这一点,所以请原谅代码的骇人听闻的性质。

      首先我编写了一个辅助递归辅助函数来查找所有书签:

      private static Dictionary<string, BookmarkEnd> FindBookmarks(OpenXmlElement documentPart, Dictionary<string, BookmarkEnd> results = null, Dictionary<string, string> unmatched = null )
      {
          results = results ?? new Dictionary<string, BookmarkEnd>();
          unmatched = unmatched ?? new Dictionary<string,string>();
      
          foreach (var child in documentPart.Elements())
          {
              if (child is BookmarkStart)
              {
                  var bStart = child as BookmarkStart;
                  unmatched.Add(bStart.Id, bStart.Name);
              }
      
              if (child is BookmarkEnd)
              {
                  var bEnd = child as BookmarkEnd;
                  foreach (var orphanName in unmatched)
                  {
                      if (bEnd.Id == orphanName.Key)
                          results.Add(orphanName.Value, bEnd);
                  }
              }
      
              FindBookmarks(child, results, unmatched);
          }
      
          return results;
      }
      

      这会返回给我一个字典,我可以使用它来分隔我的替换列表并在书签后添加文本:

      var bookMarks = FindBookmarks(doc.MainDocumentPart.Document);
      
      foreach( var end in bookMarks )
      {
          var textElement = new Text("asdfasdf");
          var runElement = new Run(textElement);
      
          end.Value.InsertAfterSelf(runElement);
      }
      

      据我所知,插入和替换书签看起来更难。当我使用 InsertAt 而不是 InsertIntoSelf 时,我得到:“非复合元素没有子元素。” YMMV

      【讨论】:

      • 我想我想做的是使用开始/结束书签标签让我选择一部分文本(运行?)并修改它。不过,书签的存储位置似乎很随机,我的都在doc.MainDocumentPart.Document.Body.Descendants
      • @John 他们在文档中添加它们的位置的树内。一点也不随意。一切都将在 Body.Descendants 中。 Body.Elements 仅获得第一级子级。等等,也许我应该只是搜索后代......
      【解决方案5】:

      我从答案中获取了代码,但在特殊情况下遇到了几个问题:

      1. 您可能希望忽略隐藏的书签。如果名称以 _(下划线)开头,则隐藏书签
      2. 如果书签用于更多的 TableCell,您将在具有属性 ColumnFirst 的行的第一个单元格的 BookmarkStart 中找到它,该属性引用书签开始的单元格的从 0 开始的列索引。 ColumnLast 指的是书签结束的单元格,对于我的特殊情况,它始终是 ColumnFirst == ColumnLast (书签仅标记一列)。在这种情况下,您也不会找到 BookmarkEnd。
      3. Bookmarks 可以为空,因此 BookmarkStart 直接跟在 BookmarkEnd 之后,在这种情况下,您只需调用 bookmarkStart.Parent.InsertAfter(new Run(new Text("Hello World")), bookmarkStart)
      4. 此外,书签可以包含许多文本元素,因此您可能需要删除所有其他元素,否则书签的部分可能会被替换,而其他以下部分将保留。
      5. 而且我不确定我的最后一次 hack 是否有必要,因为我不知道 OpenXML 的所有限制,但是在发现前 4 个之后,我也不再相信 Run 会有兄弟姐妹, 有一个 Text 的孩子。因此,我只查看我所有的兄弟姐妹(直到 BookmarEnd 与 BookmarkStart 具有相同的 ID)并检查所有孩子,直到找到任何文本。 - 如果有必要,也许有更多 OpenXML 经验的人可以回答?

      你可以查看我的具体实现here)

      希望这对遇到同样问题的一些人有所帮助。

      【讨论】:

      • 请注意,您应该在此处或此站点上发布有用的答案点,否则您的帖子可能会被删除为"Not an Answer"。如果您愿意,您仍然可以包含该链接,但仅作为“参考”。答案应该是独立的,不需要链接。
      【解决方案6】:

      这里的大多数解决方案都假设运行前开始和运行后结​​束的常规书签模式,这并不总是正确的,例如如果书签从一个段落或表格开始并在另一个段落的某个地方结束(就像其他人指出的那样)。如何使用文档顺序来处理书签未放置在常规结构中的情况 - 文档顺序仍然会找到所有相关的文本节点,然后可以替换它们之间的所有相关文本节点。只需执行 root.DescendantNodes().Where(xtext or bookmarkstart or bookmark end) ,它将按文档顺序遍历,然后可以替换在看到书签开始节点之后但在看到结束节点之前出现的文本节点。

      【讨论】:

        【解决方案7】:

        这是我如何做到这一点和 VB 在 bookmarkStart 和 BookmarkEnd 之间添加/替换文本。

        <w:bookmarkStart w:name="forbund_kort" w:id="0" /> 
                - <w:r>
                  <w:t>forbund_kort</w:t> 
                  </w:r>
        <w:bookmarkEnd w:id="0" />
        
        
        Imports DocumentFormat.OpenXml.Packaging
        Imports DocumentFormat.OpenXml.Wordprocessing
        
            Public Class PPWordDocx
        
                Public Sub ChangeBookmarks(ByVal path As String)
                    Try
                        Dim doc As WordprocessingDocument = WordprocessingDocument.Open(path, True)
                         'Read the entire document contents using the GetStream method:
        
                        Dim bookmarkMap As IDictionary(Of String, BookmarkStart) = New Dictionary(Of String, BookmarkStart)()
                        Dim bs As BookmarkStart
                        For Each bs In doc.MainDocumentPart.RootElement.Descendants(Of BookmarkStart)()
                            bookmarkMap(bs.Name) = bs
                        Next
                        For Each bs In bookmarkMap.Values
                            Dim bsText As DocumentFormat.OpenXml.OpenXmlElement = bs.NextSibling
                            If Not bsText Is Nothing Then
                                If TypeOf bsText Is BookmarkEnd Then
                                    'Add Text element after start bookmark
                                    bs.Parent.InsertAfter(New Run(New Text(bs.Name)), bs)
                                Else
                                    'Change Bookmark Text
                                    If TypeOf bsText Is Run Then
                                        If bsText.GetFirstChild(Of Text)() Is Nothing Then
                                            bsText.InsertAt(New Text(bs.Name), 0)
                                        End If
                                        bsText.GetFirstChild(Of Text)().Text = bs.Name
                                    End If
                                End If
        
                            End If
                        Next
                        doc.MainDocumentPart.RootElement.Save()
                        doc.Close()
                    Catch ex As Exception
                        Throw ex
                    End Try
                End Sub
        
            End Class
        

        【讨论】:

          【解决方案8】:

          我需要用表格替换书签的文本(书签名称为“表格”)。这是我的方法:

          public void ReplaceBookmark( DatasetToTable( ds ) )
          {
              MainDocumentPart mainPart = myDoc.MainDocumentPart;
              Body body = mainPart.Document.GetFirstChild<Body>();
              var bookmark = body.Descendants<BookmarkStart>()
                                  .Where( o => o.Name == "Table" )
                                  .FirstOrDefault();
              var parent = bookmark.Parent; //bookmark's parent element
              if (ds!=null)
              {
                  parent.InsertAfterSelf( DatasetToTable( ds ) );
                  parent.Remove();
              }
              mainPart.Document.Save();
          }
          
          
          public Table DatasetToTable( DataSet ds )
          {
              Table table = new Table();
              //creating table;
              return table;
          }
          

          希望对你有帮助

          【讨论】:

            【解决方案9】:

            这是我在 VB.NET 中的做法:

            For Each curBookMark In contractBookMarkStarts
            
                  ''# Get the "Run" immediately following the bookmark and then
                  ''# get the Run's "Text" field
                  runAfterBookmark = curBookMark.NextSibling(Of Wordprocessing.Run)()
                  textInRun = runAfterBookmark.LastChild
            
                  ''# Decode the bookmark to a contract attribute
                  lines = DecodeContractDataToContractDocFields(curBookMark.Name, curContract).Split(vbCrLf)
            
                  ''# If there are multiple lines returned then some work needs to be done to create
                  ''# the necessary Run/Text fields to hold lines 2 thru n.  If just one line then set the
                  ''# Text field to the attribute from the contract
                  For ptr = 0 To lines.Count - 1
                      line = lines(ptr)
                      If ptr = 0 Then
                          textInRun.Text = line.Trim()
                      Else
                          ''# Add a <br> run/text component then add next line
                          newRunForLf = New Run(runAfterBookmark.OuterXml)
                          newRunForLf.LastChild.Remove()
                          newBreak = New Break()
                          newRunForLf.Append(newBreak)
            
                          newRunForText = New Run(runAfterBookmark.OuterXml)
                          DirectCast(newRunForText.LastChild, Text).Text = line.Trim
            
                          curBookMark.Parent.Append(newRunForLf)
                          curBookMark.Parent.Append(newRunForText)
                      End If
                  Next
            Next
            

            【讨论】:

              【解决方案10】:

              接受的答案和其他一些答案对书签在文档结构中的位置进行了假设。这是我的 C# 代码,它可以处理替换跨越多个段落的书签正确替换不在段落边界开始和结束的书签。仍然不完美,但更接近......希望它有用。如果您发现更多改进方法,请编辑!

                  private static void ReplaceBookmarkParagraphs(MainDocumentPart doc, string bookmark, IEnumerable<OpenXmlElement> paras) {
                      var start = doc.Document.Descendants<BookmarkStart>().Where(x => x.Name == bookmark).First();
                      var end = doc.Document.Descendants<BookmarkEnd>().Where(x => x.Id.Value == start.Id.Value).First();
                      OpenXmlElement current = start;
                      var done = false;
              
                      while ( !done && current != null ) {
                          OpenXmlElement next;
                          next = current.NextSibling();
              
                          if ( next == null ) {
                              var parentNext = current.Parent.NextSibling();
                              while ( !parentNext.HasChildren ) {
                                  var toRemove = parentNext;
                                  parentNext = parentNext.NextSibling();
                                  toRemove.Remove();
                              }
                              next = current.Parent.NextSibling().FirstChild;
              
                              current.Parent.Remove();
                          }
              
                          if ( next is BookmarkEnd ) {
                              BookmarkEnd maybeEnd = (BookmarkEnd)next;
                              if ( maybeEnd.Id.Value == start.Id.Value ) {
                                  done = true;
                              }
                          }
                          if ( current != start ) {
                              current.Remove();
                          }
              
                          current = next;
                      }
              
                      foreach ( var p in paras ) {
                          end.Parent.InsertBeforeSelf(p);
                      }
                  }
              

              【讨论】:

                【解决方案11】:

                这是我最终得到的结果 - 不是 100% 完美,但适用于简单的书签和要插入的简单文本:

                private void FillBookmarksUsingOpenXml(string sourceDoc, string destDoc, Dictionary<string, string> bookmarkData)
                    {
                        string wordmlNamespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
                        // Make a copy of the template file.
                        File.Copy(sourceDoc, destDoc, true);
                
                        //Open the document as an Open XML package and extract the main document part.
                        using (WordprocessingDocument wordPackage = WordprocessingDocument.Open(destDoc, true))
                        {
                            MainDocumentPart part = wordPackage.MainDocumentPart;
                
                            //Setup the namespace manager so you can perform XPath queries 
                            //to search for bookmarks in the part.
                            NameTable nt = new NameTable();
                            XmlNamespaceManager nsManager = new XmlNamespaceManager(nt);
                            nsManager.AddNamespace("w", wordmlNamespace);
                
                            //Load the part's XML into an XmlDocument instance.
                            XmlDocument xmlDoc = new XmlDocument(nt);
                            xmlDoc.Load(part.GetStream());
                
                            //Iterate through the bookmarks.
                            foreach (KeyValuePair<string, string> bookmarkDataVal in bookmarkData)
                            {
                                var bookmarks = from bm in part.Document.Body.Descendants<BookmarkStart>()
                                          select bm;
                
                                foreach (var bookmark in bookmarks)
                                {
                                    if (bookmark.Name == bookmarkDataVal.Key)
                                    {
                                        Run bookmarkText = bookmark.NextSibling<Run>();
                                        if (bookmarkText != null)  // if the bookmark has text replace it
                                        {
                                            bookmarkText.GetFirstChild<Text>().Text = bookmarkDataVal.Value;
                                        }
                                        else  // otherwise append new text immediately after it
                                        {
                                            var parent = bookmark.Parent;   // bookmark's parent element
                
                                            Text text = new Text(bookmarkDataVal.Value);
                                            Run run = new Run(new RunProperties());
                                            run.Append(text);
                                            // insert after bookmark parent
                                            parent.Append(run);
                                        }
                
                                        //bk.Remove();    // we don't want the bookmark anymore
                                    }
                                }
                            }
                
                            //Write the changes back to the document part.
                            xmlDoc.Save(wordPackage.MainDocumentPart.GetStream(FileMode.Create));
                        }
                    }
                

                【讨论】:

                • 您在更改/匹配字体样式/大小时遇到​​过问题吗?我可以插入文本,但最初在模板中的文本大小/样式会被某种“默认”样式覆盖。有什么想法吗?
                猜你喜欢
                • 1970-01-01
                • 2013-08-21
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2015-07-13
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多