OpenXML标记搜索

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

 

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

  

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

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

我可以生成的最小.docx示例存储此内部文档

              TRY              <!TAG1       !>             TRY2             

最诚挚的问候,迈克

尝试查找标记的问题是,单词并不总是在它们看起来在Word中的格式的基础XML中。 例如,在您的示例XML中, 标记分为多个运行,如下所示:

     <!TAG1       !>  

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

处理此问题的一种方法是找到ParagraphInnerText并将其与正则Regex进行比较。 InnerText属性将返回段落的纯文本,而基础文档中的任何格式或其他XML都不会妨碍。

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

以下代码显示了查找标记并立即替换它们,而不是像您在问题中建议的那样使用两个遍。 这只是为了让事实更简单。 它应该显示你需要的一切。

 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()) { 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(); //add the newly created run paragraph.AppendChild(newRun); } } } 

上述方法的唯一缺点是你可能拥有的任何款式都将丢失。 这些可以从现有的Run复制,但如果有多个Run具有不同的属性,则需要确定哪些需要复制到哪里。 没有什么可以阻止你在上面的代码中创建多个Run ,每个具有不同的属性,如果这是必需的。

不确定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 lookupTable = new Dictionary(); Regex reg = new Regex(@"\<\!(?.*)\!\>"); XDocument doc = XDocument.Load("document.xml"); XNamespace ns = doc.Root.GetNamespaceOfPrefix("w"); IEnumerable elements = doc.Root.Descendants(ns + "t").Where(x=> x.Value.StartsWith(" 

编辑:

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

 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> lookUpTable; #endregion #region properties public IEnumerable 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(@"", tagName),value); } } public void Save(string fileName) { document.Save(@"temp\word\document.xml"); //todo: save other parts of document here ie 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>(); Regex reg = new Regex(@"\<\!(?.*)\!\>"); document = XDocument.Load(@"temp\word\document.xml"); XNamespace ns = document.Root.GetNamespaceOfPrefix("w"); IEnumerable 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 { item }); } } } private bool NodeGotSplitUpIn2PartsDueToGrammarCheck(XElement node) { XNamespace ns = node.Document.Root.GetNamespaceOfPrefix("w"); return node.Value.StartsWith(" 

并像这样使用它:

 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 } } 

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

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

 static void Main(string[] args) { //FillInValues(FileName("test01.docx"), FileName("test01_out.docx")); string[,] tests = { { "${abc}${tha}", "ABCTHA"}, { "${abc}", "ABC"}, {"${abc}", "ABC" }, {"x${abc}", "xABC" }, {"x${abc}y", "xABCy" }, {"x${abc}${tha}z", "xABCTHAz" }, {"x${abc}u${tha}z", "xABCuTHAz" }, {"x${abc}u", "xABCu" }, {"x${abyupeekaiieic}u", "xABYUPEEKAIIEICu" }, {"x${abyupeekaiiei}", "xABYUPEEKAIIEI" }, }; 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(); SimpleStateMachine stateMachine = new SimpleStateMachine(); //stateMachine.TextReplacer =  ProcessParagraphs(paras, stateMachine); } } private static void ProcessParagraphs(IEnumerable paras, SimpleStateMachine stateMachine) { foreach (var para in paras) { foreach (var run in para.Elements()) { //Console.WriteLine("New run:"); var texts = run.Elements().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 TextsList { get; } = new List(); public StringBuilder Buffer { get; } = new StringBuilder(); ///  /// The index inside the Text element where the $ is found ///  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函数)的问题。

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