Melhor maneira de obter o InnerXml de um XElement?

147

Qual é a melhor maneira de obter o conteúdo do bodyelemento misto no código abaixo? O elemento pode conter XHTML ou texto, mas eu apenas quero o seu conteúdo no formato string. O XmlElementtipo tem a InnerXmlpropriedade que é exatamente o que eu estou procurando.

O código escrito quase faz o que eu quero, mas inclui o elemento circundante <body>... </body>, que eu não quero.

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
fonte

Respostas:

208

Eu queria ver qual dessas soluções sugeridas apresentava melhor desempenho e, portanto, realizei alguns testes comparativos. Por interesse, também comparei os métodos LINQ com o método System.Xml antigo simples sugerido por Greg. A variação foi interessante e não o que eu esperava, com os métodos mais lentos sendo três vezes mais lentos que os mais rápidos .

Os resultados ordenados pelo mais rápido para o mais lento:

  1. CreateReader - Caçador de Instâncias (0,131 segundos)
  2. System.Xml antigo simples - Greg Hurlman (0.134 segundos)
  3. Agregado com concatenação de strings - Mike Powell (0.324 segundos)
  4. StringBuilder - Vin (0,333 segundos)
  5. String.Join na matriz - Terry (0,360 segundos)
  6. String.Concat na matriz - Marcin Kosieradzki (0.364)

Método

Eu usei um único documento XML com 20 nós idênticos (chamados 'dica'):

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

Os números mostrados como segundos acima são o resultado da extração do "XML interno" dos 20 nós, 1000 vezes seguidas e da média (média) de 5 execuções. Não incluí o tempo necessário para carregar e analisar o XML em um XmlDocument(para o método System.Xml ) ou XDocument(para todos os outros).

Os algoritmos LINQ que usei foram: (C # - todos pegam um XElement"pai" e retornam a cadeia XML interna)

CreateReader:

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

return reader.ReadInnerXml();

Agregue com concatenação de cadeias:

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 na matriz:

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

String.Concat na matriz:

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

Não mostrei o algoritmo "Plain System.Xml antigo" aqui, pois ele está chamando .InnerXml nos nós.


Conclusão

Se o desempenho for importante (por exemplo, muito XML, analisado com freqüência), eu usaria o CreateReadermétodo de Daniel toda vez . Se você está apenas fazendo algumas consultas, convém usar o método Aggregate, mais conciso, de Mike.

Se você estiver usando XML em elementos grandes com muitos nós (talvez 100), você provavelmente começará a ver o benefício de usar StringBuildero método Aggregate, mas não o acabou CreateReader. Eu não acho que os métodos Joine Concatseriam mais eficientes nessas condições devido à penalidade de converter uma lista grande em uma grande variedade (até óbvio aqui com listas menores).

Luke Sampson
fonte
A versão StringBuilder pode ser escrita em uma linha: var result = parent.Elements (). Aggregate (new StringBuilder (), (sb, xelem) => sb.AppendLine (xelem.ToString ()), sb => sb.ToString ( ))
Softlion 23/09
7
Você perdeu parent.CreateNavigator().InnerXml(precisa using System.Xml.XPathdo método de extensão).
23412 Richard Richard
Eu não teria pensado que você precisa do .ToArray()interior .Concat, mas parece para torná-lo mais rápido
drzaus
Caso você não role para o final destas respostas: considere apenas retirar o contêiner / raiz de .ToString()acordo com esta resposta . Parece ainda mais rápido ...
drzaus
2
Você realmente deve agrupar isso var reader = parent.CreateReader();em uma instrução using.
BrainSlugs83
70

Eu acho que esse é um método muito melhor (no VB, não deve ser difícil de traduzir):

Dado um XElement x:

Dim xReader = x.CreateReader
xReader.MoveToContent
xReader.ReadInnerXml
Caçador de Instâncias
fonte
Agradável! Isso é muito mais rápido do que alguns dos outros métodos propostos (eu testei todos - veja minha resposta para obter detalhes). Embora todos eles façam o trabalho, este faz o mais rápido - até parece mais rápido que o próprio System.Xml.Node.InnerXml!
9339 Luke Sampson
4
O XmlReader é descartável; portanto, não se esqueça de envolvê-lo com o uso, por favor (eu mesmo editaria a resposta se conhecesse o VB).
Dmitry Fedorkov
19

Que tal usar esse método de "extensão" no XElement? trabalhou para mim!

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

Nota : O código acima deve ser usado element.Nodes()em oposição a element.Elements(). Coisa muito importante para lembrar a diferença entre os dois. element.Nodes()dá tudo XText, XAttributeetc, mas XElementapenas um elemento.

Vin
fonte
15

Com todo o crédito devido a quem descobriu e provou a melhor abordagem (obrigado!), Aqui está envolvido em um método de extensão:

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

Mantenha-o simples e eficiente:

String.Concat(node.Nodes().Select(x => x.ToString()).ToArray())
  • Agregado é memória e desempenho ineficientes ao concatenar cadeias
  • Usar Join ("", sth) está usando uma matriz de cadeias duas vezes maior que Concat ... E parece bastante estranho no código.
  • Usar + = parece muito estranho, mas aparentemente não é muito pior do que usar '+' - provavelmente seria otimizado para o mesmo código, pois o resultado da atribuição não é utilizado e pode ser removido com segurança pelo compilador.
  • O StringBuilder é tão imperativo - e todo mundo sabe que esse "estado" desnecessário é péssimo.
Marcin Kosieradzki
fonte
7

Acabei usando isso:

Body = t.Element("body").Nodes().Aggregate("", (b, node) => b += node.ToString());
Mike Powell
fonte
Isso fará muita concatenação de strings - eu prefiro o uso de StringBuilder por Vin. O foreach manual não é negativo.
Marc Gravell
Esse método realmente me salvou hoje, tentando escrever um XElement com o novo construtor e nenhum dos outros métodos se prestava a ele com facilidade, enquanto este o fazia. Obrigado!
delliottg
3

Pessoalmente, acabei escrevendo um InnerXmlmétodo de extensão usando o método Aggregate:

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

Meu código de cliente é tão conciso quanto no antigo espaço para nome System.Xml:

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

@ Greg: Parece que você editou sua resposta para ser uma resposta completamente diferente. Em que minha resposta é sim, eu poderia fazer isso usando o System.Xml, mas esperava molhar os pés com o LINQ to XML.

Deixarei minha resposta original abaixo, caso alguém se pergunte por que não posso simplesmente usar a propriedade .Value do XElement para obter o que eu preciso:

@Greg: a propriedade Value concatena todo o conteúdo do texto de qualquer nó filho. Portanto, se o elemento body contiver apenas texto, ele funcionará, mas se ele contiver XHTML, reunirei todo o texto, mas nenhuma das tags.

Mike Powell
fonte
Encontrei exatamente o mesmo problema e pensei que era um bug: eu tinha conteúdo 'misto' (ie <root>random text <sub1>child</sub1> <sub2>child</sub2></root>) que se tornou random text childchildviaXElement.Parse(...).Value
drzaus
1

// o uso do Regex pode ser mais rápido para simplesmente aparar a tag do elemento de início e fim

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
fonte
1
arrumado. ainda mais rápido para usar IndexOf:var xml = root.ToString(); var begin = xml.IndexOf('>')+1; var end = xml.LastIndexOf('<'); return xml.Substring(begin, end-begin);
drzaus
0

É possível usar os objetos de espaço para nome System.Xml para concluir o trabalho aqui em vez de usar o LINQ? Como você já mencionou, XmlNode.InnerXml é exatamente o que você precisa.

Greg Hurlman
fonte
0

Querendo saber se (note que eu me livrei do b + = e apenas tenho b +)

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

pode ser um pouco menos eficiente do que

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

Não 100% de certeza ... mas olhando para Aggregate () e String.Join () no refletor ... Eu acho que que li como Agregado apenas acrescentando um valor retornado, então, basicamente, você obtém:

string = string + string

Junte-se a isso, há alguma menção a FastStringAllocation ou algo assim, o que me faz pensar que o pessoal da Microsoft pode ter colocado um aumento extra no desempenho. É claro que meu .ToArray () chama isso de negar isso, mas eu só queria oferecer outra sugestão.


fonte
0

você sabe? a melhor coisa a fazer é voltar ao CDATA :( estou procurando soluções aqui, mas acho que o CDATA é de longe o mais simples e mais barato, não o mais conveniente para desenvolver com esse

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

Fará o trabalho para você

Vinod Srivastav
fonte
-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
fonte
E também, se o elemento tiver algum atributo ou até um espaço a mais, a lógica falhará.
Christoph