Il modo migliore per ottenere InnerXml di un XElement?


147

Qual è il modo migliore per ottenere il contenuto bodydell'elemento misto nel codice seguente? L'elemento potrebbe contenere XHTML o testo, ma voglio solo il suo contenuto in forma di stringa. Il XmlElementtipo ha la InnerXmlproprietà che è esattamente quello che sto cercando.

Il codice come scritto fa quasi quello che voglio, ma include l' elemento circostante <body>... </body>che non voglio.

XDocument doc = XDocument.Load(new StreamReader(s));
var templates = from t in doc.Descendants("template")
                where t.Attribute("name").Value == templateName
                select new
                {
                   Subject = t.Element("subject").Value,
                   Body = t.Element("body").ToString()
                };

Risposte:


208

Volevo vedere quale di queste soluzioni suggerite funzionasse meglio, quindi ho eseguito alcuni test comparativi. Per interesse, ho anche confrontato i metodi LINQ con il semplice vecchio metodo System.Xml suggerito da Greg. La variazione è stata interessante e non quella che mi aspettavo, con i metodi più lenti più di 3 volte più lenti del più veloce .

I risultati ordinati dal più veloce al più lento:

  1. CreateReader - Instance Hunter (0.113 secondi)
  2. Semplicemente vecchio System.Xml - Greg Hurlman (0.134 secondi)
  3. Aggregato con concatenazione di stringhe - Mike Powell (0,324 secondi)
  4. StringBuilder - Vin (0.333 secondi)
  5. String.Join on array - Terry (0.360 secondi)
  6. String.Concat on array - Marcin Kosieradzki (0.364)

Metodo

Ho usato un singolo documento XML con 20 nodi identici (chiamato 'suggerimento'):

<hint>
  <strong>Thinking of using a fake address?</strong>
  <br />
  Please don't. If we can't verify your address we might just
  have to reject your application.
</hint>

I numeri mostrati come secondi sopra sono il risultato dell'estrazione dell '"XML interno" dei 20 nodi, 1000 volte di fila, e prendendo la media (media) di 5 corse. Non ho incluso il tempo impiegato per caricare e analizzare l'XML in un XmlDocument(per il metodo System.Xml ) o XDocument(per tutti gli altri).

Gli algoritmi LINQ che ho usato erano: (C # - tutti prendono un XElement"genitore" e restituiscono la stringa XML interna)

CreateReader:

var reader = parent.CreateReader();
reader.MoveToContent();

return reader.ReadInnerXml();

Aggregato con concatenazione di stringhe:

return parent.Nodes().Aggregate("", (b, node) => b += node.ToString());

StringBuilder:

StringBuilder sb = new StringBuilder();

foreach(var node in parent.Nodes()) {
    sb.Append(node.ToString());
}

return sb.ToString();

String.Join su array:

return String.Join("", parent.Nodes().Select(x => x.ToString()).ToArray());

String.Concat su array:

return String.Concat(parent.Nodes().Select(x => x.ToString()).ToArray());

Non ho mostrato l'algoritmo "Plain old System.Xml" qui come sta solo chiamando .InnerXml sui nodi.


Conclusione

Se le prestazioni sono importanti (ad esempio un sacco di XML, analizzate frequentemente), utilizzerei il CreateReadermetodo di Daniel ogni volta . Se stai solo facendo alcune domande, potresti voler utilizzare il metodo aggregato più conciso di Mike.

Se stai usando XML su elementi di grandi dimensioni con molti nodi (forse 100), probabilmente inizieresti a vedere i vantaggi dell'utilizzo StringBuilderdel metodo Aggregate, ma non di quello CreateReader. Non credo che i metodi Joine Concatsarebbero mai più efficienti in queste condizioni a causa della penalità di convertire un elenco di grandi dimensioni in un array di grandi dimensioni (anche qui ovvio con elenchi più piccoli).


La versione StringBuilder può essere scritta su una riga: var risultato = parent.Elements (). Aggregate (new StringBuilder (), (sb, xelem) => sb.AppendLine (xelem.ToString ()), sb => sb.ToString ( ))
Softlion,

7
Hai perso parent.CreateNavigator().InnerXml(è necessario using System.Xml.XPathil metodo di estensione).
Richard,

Non avrei mai pensato che è necessario il .ToArray()dentro .Concat, ma sembra per renderlo più veloce
drzaus

Nel caso in cui non scorre sul fondo di queste risposte: prendere in considerazione solo strippaggio il contenitore / radice da .ToString()per questa risposta . Sembra ancora più veloce ...
drzaus,

2
Dovresti davvero racchiuderlo var reader = parent.CreateReader();in un'istruzione using.
BrainSlugs83

70

Penso che questo sia un metodo molto migliore (in VB, non dovrebbe essere difficile da tradurre):

Dato un XElement x:

Dim xReader = x.CreateReader
xReader.MoveToContent
xReader.ReadInnerXml

Bello! Questo è molto più veloce di alcuni degli altri metodi proposti (li ho testati tutti - vedi la mia risposta per i dettagli). Sebbene tutti facciano il lavoro, questo lo fa nel modo più veloce - anche visto più velocemente di System.Xml.Node.InnerXml stesso!
Luke Sampson,

4
XmlReader è usa e getta, quindi non dimenticare di avvolgerlo con l'utilizzo, per favore (modificherei la risposta da solo se conoscessi VB).
Dmitry Fedorkov,

19

Che ne dici di usare questo metodo "extension" su XElement? ha funzionato per me!

public static string InnerXml(this XElement element)
{
    StringBuilder innerXml = new StringBuilder();

    foreach (XNode node in element.Nodes())
    {
        // append node's xml string to innerXml
        innerXml.Append(node.ToString());
    }

    return innerXml.ToString();
}

O usa un po 'di Linq

public static string InnerXml(this XElement element)
{
    StringBuilder innerXml = new StringBuilder();
    doc.Nodes().ToList().ForEach( node => innerXml.Append(node.ToString()));

    return innerXml.ToString();
}

Nota : il codice sopra deve usare element.Nodes()al contrario di element.Elements(). Cosa molto importante da ricordare la differenza tra i due. element.Nodes()ti dà tutto come XText, XAttributeecc., ma XElementsolo un elemento.


15

Con tutto il merito di coloro che hanno scoperto e dimostrato l'approccio migliore (grazie!), Qui è racchiuso in un metodo di estensione:

public static string InnerXml(this XNode node) {
    using (var reader = node.CreateReader()) {
        reader.MoveToContent();
        return reader.ReadInnerXml();
    }
}

10

Mantenerlo semplice ed efficiente:

String.Concat(node.Nodes().Select(x => x.ToString()).ToArray())
  • L'aggregazione è la memoria e le prestazioni inefficienti quando si concatenano le stringhe
  • Usando Join ("", sth) si usa un array di stringhe due volte più grande di Concat ... E sembra abbastanza strano nel codice.
  • Usare + = sembra molto strano, ma a quanto pare non è molto peggio che usare '+' - probabilmente sarebbe ottimizzato sullo stesso codice, poiché il risultato dell'assegnazione non è usato e potrebbe essere rimosso in modo sicuro dal compilatore.
  • StringBuilder è così indispensabile - e tutti sanno che lo "stato" non necessario fa schifo.

7

Ho finito per usare questo:

Body = t.Element("body").Nodes().Aggregate("", (b, node) => b += node.ToString());

Ciò comporterebbe molta concatenazione di stringhe: preferirei l'uso di StringBuilder da parte di Vin. Il manuale foreach non è negativo.
Marc Gravell

Questo metodo mi ha davvero salvato oggi, cercando di scrivere un XElement con il nuovo costruttore e nessuno degli altri metodi si è prestato ad esso facilmente, mentre questo ha fatto. Grazie!
delliottg,

3

Personalmente, ho finito per scrivere un InnerXmlmetodo di estensione usando il metodo Aggregate:

public static string InnerXml(this XElement thiz)
{
   return thiz.Nodes().Aggregate( string.Empty, ( element, node ) => element += node.ToString() );
}

Il mio codice client è quindi altrettanto conciso come sarebbe con il vecchio spazio dei nomi System.Xml:

var innerXml = myXElement.InnerXml();

2

@Greg: sembra che tu abbia modificato la tua risposta in modo che sia completamente diversa. Alla quale la mia risposta è sì, potrei farlo usando System.Xml ma speravo di bagnarmi i piedi con LINQ to XML.

Lascerò la mia risposta originale di seguito nel caso in cui qualcun altro si chieda perché non posso semplicemente usare la proprietà .Value di XElement per ottenere ciò di cui ho bisogno:

@Greg: la proprietà Value concatena tutto il contenuto del testo di tutti i nodi figlio. Quindi se l'elemento body contiene solo testo funziona, ma se contiene XHTML ottengo tutto il testo concatenato insieme ma nessuno dei tag.


Ho riscontrato questo stesso identico problema e ho pensato che fosse un bug: avevo contenuto "misto" (cioè <root>random text <sub1>child</sub1> <sub2>child</sub2></root>) che è diventato random text childchildviaXElement.Parse(...).Value
drzaus il

1

// l'uso di Regex potrebbe essere più veloce per tagliare semplicemente il tag dell'elemento iniziale e finale

var content = element.ToString();
var matchBegin = Regex.Match(content, @"<.+?>");
content = content.Substring(matchBegin.Index + matchBegin.Length);          
var matchEnd = Regex.Match(content, @"</.+?>", RegexOptions.RightToLeft);
content = content.Substring(0, matchEnd.Index);

1
pulito. ancora più veloce da usare IndexOf:var xml = root.ToString(); var begin = xml.IndexOf('>')+1; var end = xml.LastIndexOf('<'); return xml.Substring(begin, end-begin);
drzaus


0

È possibile utilizzare gli oggetti dello spazio dei nomi System.Xml per eseguire il lavoro qui invece di utilizzare LINQ? Come hai già detto, XmlNode.InnerXml è esattamente ciò di cui hai bisogno.


0

Mi chiedo se (nota che mi sono liberato di b + = e ho solo b +)

t.Element( "body" ).Nodes()
 .Aggregate( "", ( b, node ) => b + node.ToString() );

potrebbe essere leggermente meno efficiente di

string.Join( "", t.Element.Nodes()
                  .Select( n => n.ToString() ).ToArray() );

Non sicuro al 100% ... ma dando un'occhiata a Aggregate () e string.Join () in Reflector ... Penso di averlo letto come Aggregate semplicemente aggiungendo un valore di ritorno, quindi essenzialmente ottieni:

stringa = stringa + stringa

versus string. Unisciti, ha qualche menzione di FastStringAllocation o qualcosa del genere, il che mi fa pensare che la gente di Microsoft potrebbe aver apportato un ulteriore aumento delle prestazioni. Ovviamente il mio .ToArray () lo chiama negare, ma volevo solo offrire un altro suggerimento.


0

sai? la cosa migliore da fare è tornare a CDATA :( sto guardando le soluzioni qui, ma penso che CDATA sia di gran lunga il più semplice ed economico, non il più conveniente da sviluppare con tho


0
var innerXmlAsText= XElement.Parse(xmlContent)
                    .Descendants()
                    .Where(n => n.Name.LocalName == "template")
                    .Elements()
                    .Single()
                    .ToString();

Farà il lavoro per te


-2
public static string InnerXml(this XElement xElement)
{
    //remove start tag
    string innerXml = xElement.ToString().Trim().Replace(string.Format("<{0}>", xElement.Name), "");
    ////remove end tag
    innerXml = innerXml.Trim().Replace(string.Format("</{0}>", xElement.Name), "");
    return innerXml.Trim();
}

E anche se l'elemento ha degli attributi o anche solo uno spazio troppo, la logica fallisce.
Christoph
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.