Ricerca tag OpenXML

Sto scrivendo un’applicazione .NET che dovrebbe leggere un file .docx vicino a 200 pagine (tramite DocumentFormat.OpenXML 2.5) per trovare tutte le occorrenze di determinati tag che il documento dovrebbe contenere. Per essere chiari, non sto cercando tag OpenXML, ma piuttosto tag che dovrebbero essere impostati nel documento dallo scrittore di documenti come segnaposto per i valori che devo riempire in una seconda fase. Tali tag dovrebbero essere nel seguente formato:

 

(dove TAG può essere una sequenza arbitraria di caratteri). Come ho detto, devo trovare tutte le occorrenze di tali tag più (se ansible) individuando la ‘pagina’ in cui è stata trovata l’occorrenza del tag. Ho trovato qualcosa che mi guardava intorno nel web ma più di una volta l’approccio di base era quello di scaricare tutto il contenuto del file in una stringa e quindi cercare all’interno di tale stringa indipendentemente dalla codifica .docx. Ciò ha causato falsi positivi o nessuna corrispondenza (mentre il file .docx del test contiene diversi tag), altri esempi erano probabilmente un po ‘più della mia conoscenza di OpenXML. Il modello regex per trovare tali tag dovrebbe essere qualcosa di questo tipo:

  

Il tag può essere trovato su tutto il documento (all’interno di tabella, testo, paragrafo, come anche intestazione e piè di pagina).

Sto codificando in Visual Studio 2013 .NET 4.5 ma posso tornare indietro se necessario. PS Preferisco il codice senza l’utilizzo delle API di Office Interop poiché la piattaforma di destinazione non eseguirà Office.

Il più piccolo esempio di .docx che posso produrre memorizza questo documento all’interno

              TRY              <!TAG1       !>             TRY2             

I migliori saluti, Mike

Il problema con il tentativo di trovare tag è che le parole non sono sempre nell’XML sottostante nel formato in cui appaiono in Word. Ad esempio, nel tuo esempio XML il < !TAG1!> È suddiviso su più esecuzioni come questo:

     <!TAG1       !>  

Come sottolineato nei commenti, questo a volte è causato dal correttore ortografico e grammaticale, ma non è tutto ciò che può causarlo. Ad esempio, avere stili diversi su parti del tag potrebbe causarlo.

Un modo per InnerText è trovare il InnerText di un Paragraph e confrontarlo con il tuo Regex . La proprietà InnerText restituirà il testo normale del paragrafo senza alcuna formattazione o altro codice XML all’interno del documento sottostante.

Una volta che hai i tuoi tag, sostituire il testo è il prossimo problema. A causa dei suddetti motivi, non puoi semplicemente sostituire InnerText con del nuovo testo in quanto non sarebbe chiaro a quali parti del testo apparterrebbe in quale Run . Il modo più semplice per risolvere questo è rimuovere qualsiasi Run esistente e aggiungere una nuova Run con una proprietà Text contenente il nuovo testo.

Il codice seguente mostra di trovare i tag e di sostituirli immediatamente piuttosto che utilizzare due passaggi come suggerisci nella tua domanda. Questo era solo per rendere l’esempio più semplice per essere onesti. Dovrebbe mostrare tutto ciò di cui hai bisogno.

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

L’unico svantaggio di questo approccio è che tutti gli stili che potresti avere saranno persi. Questi potrebbero essere copiati dai Run esistenti, ma se ci sono più Run con proprietà differenti dovrai calcolare quali sono necessari per copiare dove. Non c’è niente che ti impedisca di creare più Run nel codice sopra ciascuno con proprietà diverse se è ciò che è richiesto.

Non sono sicuro che l’SDK sia migliore, ma funziona e produce un dizionario che contiene il nome del tag e un elemento che puoi impostare come nuovo:

 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("< !")).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()); } } } } 

Modificare:

Un esempio più completo della ansible struttura che puoi realizzare:

 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(@"< !{0}!>", 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("< !") && ((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 } } 

e usalo in questo modo:

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

Ho lo stesso bisogno di fare con l’eccezione che voglio usare ${...} voci invece di < !...!> . Puoi personalizzare il codice qui sotto per utilizzare i tuoi tag ma richiederebbe più stati.

Il seguente codice funziona per i nodes xml e openxml. Ho provato il codice usando xml, perché quando si tratta di documenti di parole è difficile controllare come la parola organizza i paragrafi, le esecuzioni e gli elementi di testo. Immagino che non sia imansible, ma in questo modo ho più controllo:

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

So che la mia risposta è in ritardo ma potrebbe essere utile agli altri. Assicurati anche di provarlo. Domani effettuerò ulteriori test con documenti reali. Se trovo qualche bug risolverò il codice qui, ma finora tutto bene.

Aggiornamento: il codice non funziona quando i segnaposto ${...} sono posizionati in una tabella. Questo è un problema con il codice che analizza il documento (la funzione FillInValues).

Aggiornamento: ho cambiato il codice per analizzare tutti i paragrafi.