Meilleur moyen d'obtenir InnerXml d'un XElement?

147

Quelle est la meilleure façon d'obtenir le contenu de l' bodyélément mixte dans le code ci-dessous? L'élément peut contenir du XHTML ou du texte, mais je veux juste que son contenu soit sous forme de chaîne. Le XmlElementtype a la InnerXmlpropriété qui est exactement ce que je recherche.

Le code tel qu'il est écrit fait presque ce que je veux, mais inclut l' élément <body>... environnant </body>, ce que je ne veux pas.

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()
                };
Mike Powell
la source

Réponses:

208

Je voulais voir laquelle de ces solutions suggérées fonctionnait le mieux, j'ai donc effectué des tests comparatifs. Par intérêt, j'ai également comparé les méthodes LINQ à l'ancienne méthode System.Xml suggérée par Greg. La variation était intéressante et pas ce à quoi je m'attendais, les méthodes les plus lentes étant plus de 3 fois plus lentes que les plus rapides .

Les résultats classés du plus rapide au plus lent:

  1. CreateReader - Instance Hunter (0,113 seconde)
  2. Plain old System.Xml - Greg Hurlman (0,134 secondes)
  3. Agrégat avec concaténation de chaînes - Mike Powell (0,324 seconde)
  4. StringBuilder - Vin (0,333 seconde)
  5. String.Join on array - Terry (0,360 seconde)
  6. String.Concat sur tableau - Marcin Kosieradzki (0.364)

Méthode

J'ai utilisé un seul document XML avec 20 nœuds identiques (appelé `` indice ''):

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

Les nombres indiqués en secondes ci-dessus sont le résultat de l'extraction du "XML interne" des 20 nœuds, 1000 fois de suite, et de la moyenne (moyenne) de 5 exécutions. Je n'ai pas inclus le temps XmlDocumentnécessaire pour charger et analyser le XML dans un (pour la méthode System.Xml ) ou XDocument(pour tous les autres).

Les algorithmes LINQ que j'ai utilisés étaient: (C # - tous prennent un XElement"parent" et renvoient la chaîne XML interne)

CreateReader:

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

return reader.ReadInnerXml();

Agréger avec concaténation de chaînes:

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 sur le tableau:

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

String.Concat sur tableau:

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

Je n'ai pas montré ici l'algorithme "Plain old System.Xml" car il appelle simplement .InnerXml sur les nœuds.


Conclusion

Si les performances sont importantes (par exemple, beaucoup de XML, analysé fréquemment), j'utiliserais la CreateReaderméthode de Daniel à chaque fois . Si vous ne faites que quelques requêtes, vous voudrez peut-être utiliser la méthode Aggregate plus concise de Mike.

Si vous utilisez XML sur des éléments volumineux avec beaucoup de nœuds (peut-être des centaines), vous commencerez probablement à voir l'avantage d'utiliser StringBuilderla méthode Aggregate, mais pas plus CreateReader. Je ne pense pas que les méthodes Joinet Concatseraient jamais plus efficaces dans ces conditions en raison de la pénalité de convertir une grande liste en un grand tableau (même évident ici avec des listes plus petites).

Luke Sampson
la source
La version de StringBuilder peut être écrite sur une seule ligne: var result = parent.Elements (). Aggregate (new StringBuilder (), (sb, xelem) => sb.AppendLine (xelem.ToString ()), sb => sb.ToString ( ))
Softlion
7
Vous avez manqué parent.CreateNavigator().InnerXml(besoin using System.Xml.XPathde la méthode d'extension).
Richard
Je n'aurais pas pensé que vous aviez besoin de l' .ToArray()intérieur .Concat, mais cela semble le rendre plus rapide
drzaus
Dans le cas où vous ne faites pas défiler vers le bas de ces réponses: envisagez simplement de supprimer le conteneur / racine de .ToString()par cette réponse . Semble encore plus rapide ...
drzaus
2
Vous devriez vraiment envelopper cela var reader = parent.CreateReader();dans une instruction using.
BrainSlugs83
70

Je pense que c'est une bien meilleure méthode (en VB, ne devrait pas être difficile à traduire):

Étant donné un XElement x:

Dim xReader = x.CreateReader
xReader.MoveToContent
xReader.ReadInnerXml
Chasseur d'instances
la source
Agréable! C'est beaucoup plus rapide que certaines des autres méthodes proposées (je les ai toutes testées - voir ma réponse pour plus de détails). Bien que tous fassent le travail, celui-ci le fait le plus rapidement - même plus rapidement que System.Xml.Node.InnerXml lui-même!
Luke Sampson
4
XmlReader est jetable, alors n'oubliez pas de l'envelopper avec using, s'il vous plaît (je modifierais la réponse moi-même si je connaissais VB).
Dmitry Fedorkov
19

Que diriez-vous d'utiliser cette méthode "d'extension" sur XElement? travaillé pour moi!

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

OU utilisez un peu de Linq

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

    return innerXml.ToString();
}

Remarque : le code ci-dessus doit utiliser element.Nodes()par opposition à element.Elements(). Chose très importante pour se rappeler la différence entre les deux. element.Nodes()vous donne tout comme XText, XAttributeetc., mais XElementseulement un élément.

Vin
la source
15

Avec tout le mérite de ceux qui ont découvert et prouvé la meilleure approche (merci!), La voici résumée dans une méthode d'extension:

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

Restez simple et efficace:

String.Concat(node.Nodes().Select(x => x.ToString()).ToArray())
  • L'agrégat est la mémoire et les performances inefficaces lors de la concaténation de chaînes
  • Utiliser Join ("", sth) utilise un tableau de chaînes deux fois plus grand que Concat ... Et cela semble assez étrange dans le code.
  • Utiliser + = semble très étrange, mais n'est apparemment pas bien pire que d'utiliser '+' - serait probablement optimisé pour le même code, car le résultat de l'affectation de cas n'est pas utilisé et pourrait être supprimé en toute sécurité par le compilateur.
  • StringBuilder est tellement impératif - et tout le monde sait qu'un «état» inutile est nul.
Marcin Kosieradzki
la source
7

J'ai fini par utiliser ceci:

Body = t.Element("body").Nodes().Aggregate("", (b, node) => b += node.ToString());
Mike Powell
la source
Cela fera beaucoup de concaténation de chaînes - je préférerais moi-même l'utilisation de StringBuilder par Vin. Le manuel foreach n'est pas un point négatif.
Marc Gravell
Cette méthode m'a vraiment sauvé aujourd'hui, en essayant d'écrire un XElement avec le nouveau constructeur et aucune des autres méthodes ne s'y prêtait facilement, alors que celle-ci le faisait. Merci!
delliottg
3

Personnellement, j'ai fini par écrire une InnerXmlméthode d'extension en utilisant la méthode Aggregate:

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

Mon code client est alors aussi laconique qu'il le serait avec l'ancien espace de noms System.Xml:

var innerXml = myXElement.InnerXml();
Martin RL
la source
2

@Greg: Il semble que vous ayez modifié votre réponse pour être une réponse complètement différente. Ce à quoi ma réponse est oui, je pourrais le faire en utilisant System.Xml mais j'espérais me mouiller les pieds avec LINQ to XML.

Je laisserai ma réponse originale ci-dessous au cas où quelqu'un d'autre se demanderait pourquoi je ne peux pas simplement utiliser la propriété .Value de XElement pour obtenir ce dont j'ai besoin:

@Greg: la propriété Value concatène tout le contenu texte de tous les nœuds enfants. Donc, si l'élément body ne contient que du texte, cela fonctionne, mais s'il contient du XHTML, je récupère tout le texte concaténé mais aucune des balises.

Mike Powell
la source
J'ai rencontré exactement le même problème et j'ai pensé que c'était un bug: j'avais du contenu `` mixte '' (c'est-à-dire <root>random text <sub1>child</sub1> <sub2>child</sub2></root>) qui est devenu random text childchildviaXElement.Parse(...).Value
drzaus
1

// l'utilisation de Regex peut être plus rapide pour simplement couper la balise d'élément de début et de fin

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);
user950851
la source
1
soigné. encore plus rapide à utiliser simplement IndexOf:var xml = root.ToString(); var begin = xml.IndexOf('>')+1; var end = xml.LastIndexOf('<'); return xml.Substring(begin, end-begin);
drzaus
0

Est-il possible d'utiliser les objets d'espace de noms System.Xml pour effectuer le travail ici au lieu d'utiliser LINQ? Comme vous l'avez déjà mentionné, XmlNode.InnerXml est exactement ce dont vous avez besoin.

Greg Hurlman
la source
0

Je me demande si (remarquez que je me suis débarrassé du b + = et que j'ai juste b +)

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

pourrait être légèrement moins efficace que

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

Pas sûr à 100% ... mais en regardant Aggregate () et string.Join () in Reflector ... Je pense je l'ai lu comme Aggregate en ajoutant simplement une valeur de retour, donc vous obtenez essentiellement:

chaîne = chaîne + chaîne

versus string.Join, il y a une mention de FastStringAllocation ou quelque chose du genre, ce qui me fait penser que les gens de Microsoft ont peut-être amélioré les performances. Bien sûr, mon .ToArray () appelle mon nier cela, mais je voulais juste proposer une autre suggestion.


la source
0

tu sais? la meilleure chose à faire est de revenir à CDATA: (je cherche des solutions ici mais je pense que CDATA est de loin le plus simple et le moins cher, pas le plus pratique à développer avec tho

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

Fera le travail pour vous

Vinod Srivastav
la source
-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();
}
Shivraj
la source
Et aussi si l'élément a des attributs ou même seulement un espace de trop, la logique échoue.
Christoph