为了账号安全,请及时绑定邮箱和手机立即绑定

OpenXML标签搜索

/ 猿问

OpenXML标签搜索

C#
斯蒂芬大帝 2020-02-02 14:16:48

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


 <!TAG!>

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


<!(.)*?!>

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


我正在Visual Studio 2013 .NET 4.5中进行编码,但是如果需要的话我可以回来。PS我不希望使用Office Interop API的代码,因为目标平台将无法运行Office。


查看完整描述

3 回答

?
喵喔喔

尝试查找标签的问题在于单词并非总是以其在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>

正如评论中指出的那样,这有时是由拼写和语法检查程序引起的,但这并不是所有可能导致这种情况的原因。例如,在标签的各个部分上使用不同的样式也会导致这种情况。


处理的方法之一是找到InnerText的Paragraph和比较,对你的Regex。该InnerText属性将返回段落的纯文本,而不会妨碍基础文档中的任何格式或其他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如果需要的话,没有什么可以阻止您在上面的代码中创建多个具有不同属性的。其他元素也将丢失(例如,任何符号),因此也需要考虑这些元素。


查看完整回答
反对 2020-02-02
?
翻翻过去那场雪

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


以下代码适用于xml以及openxml节点。我使用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);

}

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


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


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


查看完整回答
反对 2020-02-02
?
千万里不及你

不确定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

    }

}


查看完整回答
反对 2020-02-02

添加回答

回复

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信