OpenXML 标签搜索

OpenXML tag search

我正在编写一个 .NET 应用程序,它应该读取接近 200 页长的 .docx 文件(通过 DocumentFormat.OpenXML 2.5)以查找文档应包含的某些标签的所有出现。 明确地说,我不是在寻找 OpenXML 标签,而是应该由文档编写者设置到文档中的标签,作为我需要在第二阶段填写的值的占位符。 此类标签应采用以下格式:

 <!TAG!>

(其中 TAG 可以是任意字符序列)。 正如我所说,我必须找到此类标签​​的所有出现,加上(如果可能的话)找到标签出现的 'page'。 我在网上发现了一些东西,但不止一次基本方法是将文件的所有内容转储到一个字符串中,然后在不考虑 .docx 编码的情况下查看该字符串。这导致误报或根本不匹配(而测试 .docx 文件包含多个标签),其他示例可能有点超出我对 OpenXML 的了解。 查找此类标签的正则表达式模式应该是这样的:

<!(.)*?!>

可以在整个文档中找到该标签(在 table、文本、段落以及页眉和页脚中)。

我正在 Visual Studio 2013 .NET 4.5 中编码,但如果需要我可以回来。 P.S。我更喜欢不使用 Office Interop API 的代码,因为目标平台不会 运行 Office。

我能生成的最小 .docx 示例将其存储在文档中

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 wp14">
<w:body>
<w:p w:rsidR="00CA7780" w:rsidRDefault="00815E5D">
  <w:pPr>
    <w:rPr>
      <w:lang w:val="en-GB"/>
    </w:rPr>
  </w:pPr>
  <w:r>
    <w:rPr>
      <w:lang w:val="en-GB"/>
    </w:rPr>
    <w:t>TRY</w:t>
  </w:r>
</w:p>
<w:p w:rsidR="00815E5D" w:rsidRDefault="00815E5D">
  <w:pPr>
    <w:rPr>
      <w:lang w:val="en-GB"/>
    </w:rPr>
  </w:pPr>
  <w:proofErr w:type="gramStart"/>
  <w:r>
    <w:rPr>
      <w:lang w:val="en-GB"/>
    </w:rPr>
    <w:t>&lt;!TAG1</w:t>
  </w:r>
  <w:proofErr w:type="gramEnd"/>
  <w:r>
    <w:rPr>
      <w:lang w:val="en-GB"/>
    </w:rPr>
    <w:t>!&gt;</w:t>
  </w:r>
</w:p>
<w:p w:rsidR="00815E5D" w:rsidRPr="00815E5D" w:rsidRDefault="00815E5D">
  <w:pPr>
    <w:rPr>
      <w:lang w:val="en-GB"/>
    </w:rPr>
  </w:pPr>
  <w:r>
    <w:rPr>
      <w:lang w:val="en-GB"/>
    </w:rPr>
    <w:t>TRY2</w:t>
  </w:r>
  <w:bookmarkStart w:id="0" w:name="_GoBack"/>
  <w:bookmarkEnd w:id="0"/>
</w:p>
<w:sectPr w:rsidR="00815E5D" w:rsidRPr="00815E5D">
  <w:pgSz w:w="11906" w:h="16838"/>
  <w:pgMar w:top="1417" w:right="1134" w:bottom="1134" w:left="1134" w:header="708" w:footer="708" w:gutter="0"/>
  <w:cols w:space="708"/>
  <w:docGrid w:linePitch="360"/>
</w:sectPr>
</w:body>
</w:document>

此致, 麦克

不确定 SDK 是否更好,但这可以工作并生成一个字典,其中包含标签的名称和一个您可以将新值设置为的元素:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace ConsoleApplication8
{
    class Program
    {
        static void Main(string[] args)
        {
            Dictionary<string, XElement> lookupTable = new Dictionary<string, XElement>();
            Regex reg = new Regex(@"\<\!(?<TagName>.*)\!\>");

            XDocument doc = XDocument.Load("document.xml");
            XNamespace ns = doc.Root.GetNamespaceOfPrefix("w");
            IEnumerable<XElement> elements = doc.Root.Descendants(ns + "t").Where(x=> x.Value.StartsWith("<!")).ToArray();
            foreach (var item in elements)
            {
                #region remove the grammar tag
                //before
                XElement grammar = item.Parent.PreviousNode as XElement;
                grammar.Remove();
                //after
                grammar = item.Parent.NextNode as XElement;
                grammar.Remove();
                #endregion
                #region merge the two nodes and insert the name and the XElement to the dictionary
                XElement next = (item.Parent.NextNode as XElement).Element(ns + "t");
                string totalTagName = string.Format("{0}{1}", item.Value, next.Value);
                item.Parent.NextNode.Remove();
                item.Value = totalTagName;
                lookupTable.Add(reg.Match(totalTagName).Groups["TagName"].Value, item);
                #endregion
            }
            foreach (var item in lookupTable)
            {
                Console.WriteLine("The document contains a tag {0}" , item.Key);
                Console.WriteLine(item.Value.ToString());
            }


        }
    }
}

编辑:

您可以制作的可能结构的更完整示例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using System.IO.Compression; //you will have to add a reference to System.IO.Compression.FileSystem(.dll)
using System.IO;
using System.Text.RegularExpressions;

namespace ConsoleApplication28
{
    public class MyWordDocument
    {
        #region fields

        private string fileName;
        private XDocument document;
        //todo: create fields for all document xml files that can contain the placeholders

        private Dictionary<string, List<XElement>> lookUpTable;

        #endregion

        #region properties

        public IEnumerable<string> Tags { get { return lookUpTable.Keys; } }

        #endregion

        #region construction

        public MyWordDocument(string fileName)
        {
            this.fileName = fileName;
            ExtractDocument();
            CreateLookUp();
        }

        #endregion
        #region methods

        public void ReplaceTagWithValue(string tagName, string value)
        {
            foreach (var item in lookUpTable[tagName])
            {
                item.Value = item.Value.Replace(string.Format(@"<!{0}!>", tagName),value);
            }
        }

        public void Save(string fileName)
        {
            document.Save(@"temp\word\document.xml");
            //todo: save other parts of document here i.e. footer header or other stuff

            ZipFile.CreateFromDirectory("temp", fileName);
        }

        private void CreateLookUp()
        {
            //todo: make this work for all cases and for all files that can contain the placeholders
            //tip: open the raw document in word and replace the tags,
            //     save the file to different location and extract the xmlfiles of both versions and compare to see what you have to do
            lookUpTable = new Dictionary<string, List<XElement>>();
            Regex reg = new Regex(@"\<\!(?<TagName>.*)\!\>");
            document = XDocument.Load(@"temp\word\document.xml");
            XNamespace ns = document.Root.GetNamespaceOfPrefix("w");
            IEnumerable<XElement> elements = document.Root.Descendants(ns + "t").Where(NodeGotSplitUpIn2PartsDueToGrammarCheck).ToArray();
            foreach (var item in elements)
            {
                XElement grammar = item.Parent.PreviousNode as XElement;
                grammar.Remove();
                grammar = item.Parent.NextNode as XElement;
                grammar.Remove();
                XElement next = (item.Parent.NextNode as XElement).Element(ns + "t");
                string totalTagName = string.Format("{0}{1}", item.Value, next.Value);
                item.Parent.NextNode.Remove();
                item.Value = totalTagName;
                string tagName = reg.Match(totalTagName).Groups["TagName"].Value;
                if (lookUpTable.ContainsKey(tagName))
                {
                    lookUpTable[tagName].Add(item);
                }
                else
                {
                    lookUpTable.Add(tagName, new List<XElement> { item });
                }
            }
        }

        private bool NodeGotSplitUpIn2PartsDueToGrammarCheck(XElement node)
        {
            XNamespace ns = node.Document.Root.GetNamespaceOfPrefix("w");
            return node.Value.StartsWith("<!") && ((XElement)node.Parent.PreviousNode).Name == ns + "proofErr";
        }


        private void ExtractDocument()
        {
            if (!Directory.Exists("temp"))
            {
                Directory.CreateDirectory("temp");
            }
            else
            {
                Directory.Delete("temp",true);
                Directory.CreateDirectory("temp");
            }
            ZipFile.ExtractToDirectory(fileName, "temp");
        }

        #endregion
    }
}

并像这样使用它:

class Program
{
    static void Main(string[] args)
    {
        MyWordDocument doc = new MyWordDocument("somedoc.docx"); //todo: fix path

        foreach (string name in doc.Tags) //name would be the extracted name from the placeholder
        {
            doc.ReplaceTagWithValue(name, "Example");
        }

        doc.Save("output.docx"); //todo: fix path
    }
}

尝试查找标签的问题在于,单词并不总是以它们在 Word 中显示的格式存在于基础 XML 中。例如,在您的示例 XML 中,<!TAG1!> 标记被拆分为多个运行,如下所示:

<w:r>
    <w:rPr>
        <w:lang w:val="en-GB"/>
    </w:rPr>
    <w:t>&lt;!TAG1</w:t>
</w:r>
<w:proofErr w:type="gramEnd"/>
    <w:r>
    <w:rPr>
        <w:lang w:val="en-GB"/>
    </w:rPr>
    <w:t>!&gt;</w:t>
</w:r>

正如评论中所指出的,这有时是由拼写和语法检查器引起的,但这并不是导致它的全部原因。例如,标签的某些部分具有不同的样式也可能导致它。

处理此问题的一种方法是找到 ParagraphInnerText 并将其与您的 Regex 进行比较。 InnerText 属性 将 return 段落的纯文本,没有任何格式或基础文档中的其他 XML 妨碍。

有了标签后,下一个问题就是替换文本。由于上述原因,您不能只用一些新文本替换 InnerText,因为不清楚文本的哪些部分属于 Run。最简单的方法是删除任何现有的 Run 并添加一个新的 Run 和包含新文本的 Text 属性。

以下代码显示找到标签并立即替换它们,而不是像您在问题中建议的那样使用两次传递。老实说,这只是为了让这个例子更简单。它应该显示您需要的一切。

private static void ReplaceTags(string filename)
{
    Regex regex = new Regex("<!(.)*?!>", RegexOptions.Compiled);

    using (WordprocessingDocument wordDocument = WordprocessingDocument.Open(filename, true))
    {
        //grab the header parts and replace tags there
        foreach (HeaderPart headerPart in wordDocument.MainDocumentPart.HeaderParts)
        {
            ReplaceParagraphParts(headerPart.Header, regex);
        }
        //now do the document
        ReplaceParagraphParts(wordDocument.MainDocumentPart.Document, regex);
        //now replace the footer parts
        foreach (FooterPart footerPart in wordDocument.MainDocumentPart.FooterParts)
        {
            ReplaceParagraphParts(footerPart.Footer, regex);
        }
    }
}

private static void ReplaceParagraphParts(OpenXmlElement element, Regex regex)
{
    foreach (var paragraph in element.Descendants<Paragraph>())
    {
        Match match = regex.Match(paragraph.InnerText);
        if (match.Success)
        {
            //create a new run and set its value to the correct text
            //this must be done before the child runs are removed otherwise
            //paragraph.InnerText will be empty
            Run newRun = new Run();
            newRun.AppendChild(new Text(paragraph.InnerText.Replace(match.Value, "some new value")));
            //remove any child runs
            paragraph.RemoveAllChildren<Run>();
            //add the newly created run
            paragraph.AppendChild(newRun);
        }
    }
}

上述方法的一个缺点是您可能拥有的任何样式都将丢失。这些可以从现有的 Run 中复制,但如果有多个 Run 具有不同的属性,您将需要确定哪些需要复制到哪里。如果需要的话,没有什么可以阻止您在上面的代码中创建多个 Run,每个都具有不同的属性。其他元素也会丢失(例如任何符号),因此也需要考虑这些元素。

除了我想使用 ${...} 条目而不是 <!...!> 之外,我和您有同样的需求。您可以自定义下面的代码以使用您的标签,但它需要更多状态。

以下代码适用于 xml 以及打开的 xml 节点。我使用 xml 测试了代码,因为当涉及到 word 文档时,很难控制 word 如何排列段落、运行和文本元素。我想这不是不可能,但这样我有更多的控制权:

static void Main(string[] args)
{
  //FillInValues(FileName("test01.docx"), FileName("test01_out.docx"));

  string[,] tests =
  {
    { "<r><t>${abc</t><t>}$</t><t>{tha}</t></r>", "<r><t>ABC</t><t>THA</t><t></t></r>"},
    { "<r><t>$</t><t>{</t><t>abc</t><t>}</t></r>", "<r><t>ABC</t><t></t></r>"},
    {"<r><t>${abc}</t></r>", "<r><t>ABC</t></r>" },
    {"<r><t>x${abc}</t></r>", "<r><t>xABC</t></r>" },
    {"<r><t>x${abc}y</t></r>", "<r><t>xABCy</t></r>" },
    {"<r><t>x${abc}${tha}z</t></r>", "<r><t>xABCTHAz</t></r>" },
    {"<r><t>x${abc}u${tha}z</t></r>", "<r><t>xABCuTHAz</t></r>" },
    {"<r><t>x${ab</t><t>c}u</t></r>", "<r><t>xABC</t><t>u</t></r>" },
    {"<r><t>x${ab</t><t>yupeekaiiei</t><t>c}u</t></r>", "<r><t>xABYUPEEKAIIEIC</t><t>u</t></r>" },
    {"<r><t>x${ab</t><t>yupeekaiiei</t><t>}</t></r>", "<r><t>xABYUPEEKAIIEI</t><t></t></r>" },

  };


  for (int i = 0; i < tests.GetLength(0); i++)
  {
    string value = tests[i, 0];
    string expectedValue = tests[i, 1];
    string actualValue = Test(value);
    Console.WriteLine($"{value} => {actualValue} == {expectedValue} = {actualValue == expectedValue}");

  }

  Console.WriteLine("Done!");
  Console.ReadLine();
}


public interface ITextReplacer
{
  string ReplaceValue(string value);
}

public class DefaultTextReplacer : ITextReplacer
{
  public string ReplaceValue(string value) { return $"{value.ToUpper()}"; }
}

public interface ITextElement
{
  string Value { get; set; }
  void RemoveFromParent();
}


public class XElementWrapper : ITextElement
{
  private XElement _element;

  public XElementWrapper(XElement element) { _element = element; }

  string ITextElement.Value
  {
    get { return _element.Value; }
    set { _element.Value = value; }
  }

  public XElement Element
  {
    get { return _element; }
    set { _element = value; }
  }

  public void RemoveFromParent()
  {
    _element.Remove();
  }


}

public class OpenXmlTextWrapper : ITextElement
{
  private Text _text;
  public OpenXmlTextWrapper(Text text) { _text = text; }

  public string Value
  {
    get { return _text.Text; }
    set { _text.Text = value; }
  }

  public Text Text
  {
    get { return _text; }
    set { _text = value; }
  }

  public void RemoveFromParent() { _text.Remove(); }
}


private static void FillInValues(string sourceFileName, string destFileName)
{
  File.Copy(sourceFileName, destFileName, true);

  using (WordprocessingDocument doc =
    WordprocessingDocument.Open(destFileName, true))
  {
    var body = doc.MainDocumentPart.Document.Body;
    var paras = body.Descendants<Paragraph>();

    SimpleStateMachine stateMachine = new SimpleStateMachine();

    //stateMachine.TextReplacer = <your implementation object >
    ProcessParagraphs(paras, stateMachine);
  }
}

private static void ProcessParagraphs(IEnumerable<Paragraph> paras, SimpleStateMachine stateMachine)
{
  foreach (var para in paras)
  {
    foreach (var run in para.Elements<Run>())
    {
      //Console.WriteLine("New run:");

      var texts = run.Elements<Text>().ToArray();

      for (int k = 0; k < texts.Length; k++)
      {
        OpenXmlTextWrapper wrapper = new OpenXmlTextWrapper(texts[k]);
        stateMachine.HandleText(wrapper);
      }
    }
  }
}

public class SimpleStateMachine
{
  // 0 - outside - initial state
  // 1 - $ matched
  // 2 - ${ matched
  // 3 - } - final state

  // 0 -> 1 $
  // 0 -> 0 anything other than $
  // 1 -> 2 {
  // 1 -> 0 anything other than {
  // 2 -> 3 }
  // 2 -> 2 anything other than }
  // 3 -> 0

  public ITextReplacer TextReplacer { get; set; } = new DefaultTextReplacer();
  public int State { get; set; } = 0;
  public List<ITextElement> TextsList { get; } = new List<ITextElement>();
  public StringBuilder Buffer { get; } = new StringBuilder();


  /// <summary>
  /// The index inside the Text element where the $ is found
  /// </summary>
  public int Position { get; set; }

  public void Reset()
  {
    State = 0;
    TextsList.Clear();
    Buffer.Clear();
  }

  public void Add(ITextElement text)
  {
    if (TextsList.Count == 0 || TextsList.Last() != text)
    {
      TextsList.Add(text);
    }
  }

  public void HandleText(ITextElement text)
  {
    // Scan the characters

    for (int i = 0; i < text.Value.Length; i++)
    {
      char c = text.Value[i];

      switch (State)
      {
        case 0:
          if (c == '$')
          {
            State = 1;
            Position = i;
            Add(text);
          }
          break;
        case 1:
          if (c == '{')
          {
            State = 2;
            Add(text);
          }
          else
          {
            Reset();
          }
          break;
        case 2:
          if (c == '}')
          {
            Add(text);

            Console.WriteLine("Found: " + Buffer);
            // We are on the final State
            // I will use the first text in the stack and discard the others


            // Here I am going to distinguish between whether I have only one item or more
            if (TextsList.Count == 1)
            {
              // Happy path - we have only one item - set the replacement value and then continue scanning
              string prefix = TextsList[0].Value.Substring(0, Position) + TextReplacer.ReplaceValue(Buffer.ToString());
              // Set the current index to point to the end of the prefix.The program will continue to with the next items
              TextsList[0].Value = prefix + TextsList[0].Value.Substring(i + 1);
              i = prefix.Length - 1;
              Reset();
            }
            else
            {
              // We have more than one item - discard the inbetweeners

              for (int j = 1; j < TextsList.Count - 1; j++)
              {
                TextsList[j].RemoveFromParent();
              }

              // I will set the value under the first Text item where the $ was found
              TextsList[0].Value = TextsList[0].Value.Substring(0, Position) + TextReplacer.ReplaceValue(Buffer.ToString());
              // Set the text for the current item to the remaining chars
              text.Value = text.Value.Substring(i + 1);
              i = -1;
              Reset();
            }
          }
          else
          {
            Buffer.Append(c);
            Add(text);
          }
          break;
      }
    }
  }
}

public static string Test(string xml)
{
  XElement root = XElement.Parse(xml);
  SimpleStateMachine stateMachine = new SimpleStateMachine();


  foreach (XElement element in root.Descendants()
    .Where(desc => !desc.Elements().Any()))
  {
    XElementWrapper wrapper = new XElementWrapper(element);
    stateMachine.HandleText(wrapper);
  }

  return root.ToString(SaveOptions.DisableFormatting);
}

我知道我的回答晚了,但它可能对其他人有用。还要确保对其进行测试。明天我将使用真实文档进行更多测试。如果我发现任何错误,我会在此处修复代码,但到目前为止还不错。

更新:当 ${...} 占位符放在 table 中时,代码不起作用。这是扫描文档的代码(FillInValues 函数)的问题。

更新:我更改了代码以扫描所有段落。