.net - ¿La mejor manera de obtener InnerXml de un XElement?
(15)
¿Cuál es la mejor manera de obtener el contenido del elemento de body
mixto en el código a continuación? El elemento puede contener XHTML o texto, pero solo quiero su contenido en forma de cadena. El tipo XmlElement
tiene la propiedad InnerXml
, que es exactamente lo que busco.
El código tal como está escrito casi hace lo que quiero, pero incluye el elemento <body>
... </body>
que lo rodea, que no quiero.
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()
};
¿Es posible usar los objetos de espacio de nombres System.Xml para hacer el trabajo aquí en lugar de usar LINQ? Como ya mencionó, XmlNode.InnerXml es exactamente lo que necesita.
¿Qué tal usar este método de "extensión" en XElement? trabajó para mi !
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 use un poco 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 : El código anterior tiene que usar element.Nodes()
en lugar de element.Elements()
. Algo muy importante para recordar la diferencia entre los dos. element.Nodes()
le ofrece todo como XText
, XAttribute
, etc., pero XElement
solo es un Element.
¿ya sabes? Lo mejor que puede hacer es volver a CDATA :( Estoy buscando soluciones aquí, pero creo que CDATA es, con mucho, el más simple y económico, no el más conveniente para desarrollar con
// el uso de Regex podría ser más rápido para simplemente recortar la etiqueta de elemento de inicio y 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);
@ Greg: Parece que has editado tu respuesta para que sea una respuesta completamente diferente. A lo que mi respuesta es sí, podría hacerlo usando System.Xml pero esperaba mojarme los pies con LINQ to XML.
Dejaré mi respuesta original a continuación en caso de que alguien más se pregunte por qué no puedo usar la propiedad .Value de XElement para obtener lo que necesito:
@Greg: la propiedad Value concatena todo el contenido de texto de cualquier nodo secundario. Entonces, si el elemento del cuerpo contiene solo texto, funciona, pero si contiene XHTML obtengo todo el texto concatenado, pero ninguna de las etiquetas.
Con todo el crédito debido a aquellos que descubrieron y probaron el mejor enfoque (¡gracias!), Aquí está envuelto en un método de extensión:
public static string InnerXml(this XNode node) {
using (var reader = node.CreateReader()) {
reader.MoveToContent();
return reader.ReadInnerXml();
}
}
Creo que este es un método mucho mejor (en VB, no debería ser difícil de traducir):
Dado un XElement x:
Dim xReader = x.CreateReader
xReader.MoveToContent
xReader.ReadInnerXml
Mantenlo simple y eficiente:
String.Concat(node.Nodes().Select(x => x.ToString()).ToArray())
- El agregado es ineficaz para la memoria y el rendimiento al concatenar cadenas
- Usar Join ("", sth) es usar una matriz de cadenas dos veces más grande que Concat ... Y se ve bastante extraño en el código.
- Usar + = parece muy extraño, pero aparentemente no es mucho peor que usar ''+''; probablemente se optimizaría con el mismo código, porque el resultado de la asignación no se usa y el compilador puede eliminarlo de forma segura.
- StringBuilder es tan imperativo, y todos saben que el "estado" innecesario apesta.
Personalmente, terminé escribiendo un método de extensión InnerXml
usando el método Agregado:
public static string InnerXml(this XElement thiz)
{
return thiz.Nodes().Aggregate( string.Empty, ( element, node ) => element += node.ToString() );
}
Mi código de cliente es tan breve como lo sería con el antiguo espacio de nombres System.Xml:
var innerXml = myXElement.InnerXml();
Preguntándome si (observe que me deshice de b + = y solo tengo b +)
t.Element( "body" ).Nodes()
.Aggregate( "", ( b, node ) => b + node.ToString() );
podría ser un poco menos eficiente que
string.Join( "", t.Element.Nodes()
.Select( n => n.ToString() ).ToArray() );
No estoy 100% seguro ... pero mirando Aggregate () y string.Join () en Reflector ... Creo que lo leí como Aggregate solo agregando un valor de retorno, así que esencialmente obtienes:
cadena = cadena + cadena
versus string.Join, tiene alguna mención allí de FastStringAllocation o algo así, lo que me hace pensar que la gente de Microsoft podría haber puesto un aumento de rendimiento adicional allí. Por supuesto, mi .ToArray () llama a mi negado eso, pero solo quería ofrecer otra sugerencia.
Quería ver cuál de estas soluciones sugeridas funcionaba mejor, así que realicé algunas pruebas comparativas. Por interés, también comparé los métodos LINQ con el antiguo método System.Xml sugerido por Greg. La variación fue interesante y no era lo que esperaba, ya que los métodos más lentos son más de 3 veces más lentos que los más rápidos .
Los resultados ordenados por el más rápido al más lento:
- CreateReader - Instance Hunter (0.113 segundos)
- Sistema antiguo simple.Xml - Greg Hurlman (0.134 segundos)
- Agregado con concatenación de cadenas - Mike Powell (0.324 segundos)
- StringBuilder - Vin (0.333 segundos)
- String.Join en matriz - Terry (0.360 segundos)
- String.Concat on array - Marcin Kosieradzki (0.364)
Método
Usé un solo documento XML con 20 nodos idénticos (llamado ''sugerencia''):
<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>
Los números que se muestran como segundos arriba son el resultado de extraer el "XML interno" de los 20 nodos, 1000 veces seguidas, y tomar el promedio (media) de 5 ejecuciones. No XmlDocument
el tiempo que tardó en cargar y analizar el XML en un XmlDocument
(para el método System.Xml ) o XDocument
(para todos los demás).
Los algoritmos LINQ que utilicé fueron: (C #: todos toman un "padre" XElement
y devuelven la cadena XML interna)
CreateReader:
var reader = parent.CreateReader();
reader.MoveToContent();
return reader.ReadInnerXml();
Agregado con concatenación de cadenas:
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 en matriz:
return String.Join("", parent.Nodes().Select(x => x.ToString()).ToArray());
String.Concat en la matriz:
return String.Concat(parent.Nodes().Select(x => x.ToString()).ToArray());
No he mostrado el algoritmo "Plain old System.Xml" aquí, ya que solo está llamando a .InnerXml en los nodos.
Conclusión
Si el rendimiento es importante (p. Ej., Mucho XML, analizado con frecuencia), utilizaría el método CreateReader
de Daniel cada vez . Si solo está haciendo algunas consultas, es posible que desee utilizar el método Agregado más conciso de Mike.
Si está utilizando XML en elementos grandes con muchos nodos (quizás 100), probablemente comenzará a ver el beneficio de usar StringBuilder
sobre el método Aggregate, pero no sobre CreateReader
. No creo que los métodos Join
y Concat
sean más eficientes en estas condiciones debido a la penalización de convertir una lista grande en una matriz grande (incluso obvio aquí con listas más pequeñas).
Terminé usando esto:
Body = t.Element("body").Nodes().Aggregate("", (b, node) => b += node.ToString());
doc.ToString () o doc.ToString (SaveOptions) hace el trabajo. Ver http://msdn.microsoft.com/en-us/library/system.xml.linq.xelement.tostring(v=vs.110).aspx
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();
}
var innerXmlAsText= XElement.Parse(xmlContent)
.Descendants()
.Where(n => n.Name.LocalName == "template")
.Elements()
.Single()
.ToString();
Hará el trabajo por ti