Consultar un XDocument para elementos por nombre a cualquier profundidad

143

Tengo un XDocumentobjeto Quiero consultar elementos con un nombre particular a cualquier profundidad usando LINQ. Cuando uso Descendants("element_name"), solo obtengo elementos que son hijos directos del nivel actual. Lo que estoy buscando es el equivalente de "// element_name" en XPath ... ¿debería usarlo XPath, o hay alguna forma de hacerlo utilizando los métodos LINQ? Gracias.

Rico
fuente

Respuestas:

213

Los descendientes deberían funcionar absolutamente bien. Aquí hay un ejemplo:

using System;
using System.Xml.Linq;

class Test
{
    static void Main()
    {
        string xml = @"
<root>
  <child id='1'/>
  <child id='2'>
    <grandchild id='3' />
    <grandchild id='4' />
  </child>
</root>";
        XDocument doc = XDocument.Parse(xml);

        foreach (XElement element in doc.Descendants("grandchild"))
        {
            Console.WriteLine(element);
        }
    }
}

Resultados:

<grandchild id="3" />
<grandchild id="4" />

Jon Skeet
fuente
1
¿Cómo abordaría esto si el nombre de un elemento se duplicara dentro de un documento xml? Por ejemplo: si el xml contiene una colección de <Cars> con subelementos de <Part>, y también una colección de <Planes> con subelementos de <Part>, y desea una lista de Piezas solo para automóviles.
pfeds
12
@pfeds: Entonces usaría doc.Descendants("Cars").Descendants("Part")(o posiblemente .Elements("Part")si solo fueran niños directos.)
Jon Skeet
8
Seis años después y sigue siendo un ejemplo fantástico. De hecho, esto sigue siendo mucho más útil que la explicación de MSDN :-)
EvilDr
Y sigue siendo un mal ejemplo, Dr., ya que si no hay "Autos", el código anterior daría como resultado un NPE. Quizas el .? del nuevo C # finalmente lo hará válido
Dror Harari
3
@DrorHarari No, no se produce ninguna excepción: Pruébelo var foo = new XDocument().Descendants("Bar").Descendants("Baz"); porque Descendantsdevuelve un vacío IEnumerable<XElement>y no null.
DareDude
54

Un ejemplo que indica el espacio de nombres:

String TheDocumentContent =
@"
<TheNamespace:root xmlns:TheNamespace = 'http://www.w3.org/2001/XMLSchema' >
   <TheNamespace:GrandParent>
      <TheNamespace:Parent>
         <TheNamespace:Child theName = 'Fred'  />
         <TheNamespace:Child theName = 'Gabi'  />
         <TheNamespace:Child theName = 'George'/>
         <TheNamespace:Child theName = 'Grace' />
         <TheNamespace:Child theName = 'Sam'   />
      </TheNamespace:Parent>
   </TheNamespace:GrandParent>
</TheNamespace:root>
";

XDocument TheDocument = XDocument.Parse( TheDocumentContent );

//Example 1:
var TheElements1 =
from
    AnyElement
in
    TheDocument.Descendants( "{http://www.w3.org/2001/XMLSchema}Child" )
select
    AnyElement;

ResultsTxt.AppendText( TheElements1.Count().ToString() );

//Example 2:
var TheElements2 =
from
    AnyElement
in
    TheDocument.Descendants( "{http://www.w3.org/2001/XMLSchema}Child" )
where
    AnyElement.Attribute( "theName" ).Value.StartsWith( "G" )
select
    AnyElement;

foreach ( XElement CurrentElement in TheElements2 )
{
    ResultsTxt.AppendText( "\r\n" + CurrentElement.Attribute( "theName" ).Value );
}
Jelgab
fuente
2
Pero, ¿qué pasa si mi fuente xml no tiene un espacio de nombres? Supongo que puedo agregar uno en el código (tengo que ver eso), pero ¿por qué es necesario? En cualquier caso, root.Descendants ("myTagName") no encuentra elementos enterrados a tres o cuatro niveles de profundidad en mi código.
EoRaptor013
2
¡Gracias! Estamos utilizando la serialización de contrato de datos. Esto crea un encabezado como <MyClassEntries xmlns: i = " w3.org/2001/XMLSchema-instance " xmlns = " schemas.datacontract.org/2004/07/DataLayer.MyClass "> y me quedé perplejo por qué no estaba recibiendo cualquier descendiente Necesitaba agregar el prefijo { schemas.datacontract.org/2004/07/DataLayer.MyClass }.
Kim
38

Puedes hacerlo de esta manera:

xml.Descendants().Where(p => p.Name.LocalName == "Name of the node to find")

donde xmlesta a XDocument.

Tenga en cuenta que la propiedad Namedevuelve un objeto que tiene ay LocalNamea Namespace. Es por eso que debe usar Name.LocalNamesi desea comparar por nombre.

Francisco Goldenstein
fuente
Estoy tratando de obtener todos los nodos EmbeddedResource del archivo de proyecto C #, y esta es la única forma en que funciona. Documento XDocument = XDocument.Load (csprojPath); IEnumerable <XElement> embeddedResourceElements = document.Descendants ("EmbeddedResource"); No funciona y no entiendo por qué.
Eugene Maksimov
22

Los descendientes harán exactamente lo que necesita, pero asegúrese de haber incluido un nombre de espacio de nombres junto con el nombre del elemento. Si lo omite, probablemente obtendrá una lista vacía.

Nenad Dobrilovic
fuente
11

Hay dos maneras de lograr esto,

  1. Linq-to-xml
  2. XPath

Los siguientes son ejemplos del uso de estos enfoques,

List<XElement> result = doc.Root.Element("emails").Elements("emailAddress").ToList();

Si usa XPath, necesita hacer alguna manipulación con IEnumerable:

IEnumerable<XElement> mails = ((IEnumerable)doc.XPathEvaluate("/emails/emailAddress")).Cast<XElement>();

Tenga en cuenta que

var res = doc.XPathEvaluate("/emails/emailAddress");

da como resultado un puntero nulo o ningún resultado.

roland roos
fuente
1
solo por mencionar que XPathEvaluateestá en el System.Xml.XPathespacio de nombres.
Tahir Hassan
XPathEvaluate debería hacer el truco, pero su consulta solo toma nodos a una profundidad particular (uno). Si desea seleccionar todos los elementos llamados "correo electrónico", independientemente de en qué parte del documento se encuentren, utilice la ruta "// correo electrónico". Obviamente, estos caminos son más caros, ya que se debe recorrer todo el árbol como se llame, pero puede ser bastante conveniente, siempre que sepa lo que está haciendo.
The Dag
8

Estoy usando el XPathSelectElementsmétodo de extensión que funciona de la misma manera que el XmlDocument.SelectNodesmétodo:

using System;
using System.Xml.Linq;
using System.Xml.XPath; // for XPathSelectElements

namespace testconsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            XDocument xdoc = XDocument.Parse(
                @"<root>
                    <child>
                        <name>john</name>
                    </child>
                    <child>
                        <name>fred</name>
                    </child>
                    <child>
                        <name>mark</name>
                    </child>
                 </root>");

            foreach (var childElem in xdoc.XPathSelectElements("//child"))
            {
                string childName = childElem.Element("name").Value;
                Console.WriteLine(childName);
            }
        }
    }
}
Tahir Hassan
fuente
1

Siguiendo la respuesta de @Francisco Goldenstein, escribí un método de extensión

using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;

namespace Mediatel.Framework
{
    public static class XDocumentHelper
    {
        public static IEnumerable<XElement> DescendantElements(this XDocument xDocument, string nodeName)
        {
            return xDocument.Descendants().Where(p => p.Name.LocalName == nodeName);
        }
    }
}
Tiago Freitas Leal
fuente
0

Sabemos que lo anterior es cierto. Jon nunca se equivoca; los deseos de la vida real pueden ir un poco más allá

<ota:OTA_AirAvailRQ
    xmlns:ota="http://www.opentravel.org/OTA/2003/05" EchoToken="740" Target=" Test" TimeStamp="2012-07-19T14:42:55.198Z" Version="1.1">
    <ota:OriginDestinationInformation>
        <ota:DepartureDateTime>2012-07-20T00:00:00Z</ota:DepartureDateTime>
    </ota:OriginDestinationInformation>
</ota:OTA_AirAvailRQ>

Por ejemplo, por lo general el problema es, ¿cómo podemos obtener EchoToken en el documento xml anterior? O cómo desenfocar el elemento con el nombre atribuir.

1- Puede encontrarlos accediendo con el espacio de nombres y el nombre como se muestra a continuación

doc.Descendants().Where(p => p.Name.LocalName == "OTA_AirAvailRQ").Attributes("EchoToken").FirstOrDefault().Value

2- Puedes encontrarlo por el valor del contenido del atributo, como este

Hamit YILDIRIM
fuente
0

Esta es mi variante de la solución basada en el Linqmétodo de descendientes de la XDocumentclase

using System;
using System.Linq;
using System.Xml.Linq;

class Test
{
    static void Main()
    {
        XDocument xml = XDocument.Parse(@"
        <root>
          <child id='1'/>
          <child id='2'>
            <subChild id='3'>
                <extChild id='5' />
                <extChild id='6' />
            </subChild>
            <subChild id='4'>
                <extChild id='7' />
            </subChild>
          </child>
        </root>");

        xml.Descendants().Where(p => p.Name.LocalName == "extChild")
                         .ToList()
                         .ForEach(e => Console.WriteLine(e));

        Console.ReadLine();
    }
}

Resultados:

Para más detalles sobre el Desendantsmétodo, eche un vistazo aquí.

Mselmi Ali
fuente
-1

(El código y las instrucciones son para C # y es posible que deba modificarse ligeramente para otros idiomas)

Este ejemplo funciona perfecto si desea leer de un nodo primario que tiene muchos hijos, por ejemplo, mire el siguiente XML;

<?xml version="1.0" encoding="UTF-8"?> 
<emails>
    <emailAddress>[email protected]</emailAddress>
    <emailAddress>[email protected]</emailAddress>
    <emailAddress>rgreen@set_ig.ca</emailAddress> 
</emails>

Ahora con este código a continuación (teniendo en cuenta que el archivo XML se almacena en recursos (consulte los enlaces al final del fragmento para obtener ayuda sobre los recursos)) Puede obtener cada dirección de correo electrónico dentro de la etiqueta "correos electrónicos".

XDocument doc = XDocument.Parse(Properties.Resources.EmailAddresses);

var emailAddresses = (from emails in doc.Descendants("emailAddress")
                      select emails.Value);

foreach (var email in emailAddresses)
{
    //Comment out if using WPF or Windows Form project
    Console.WriteLine(email.ToString());

   //Remove comment if using WPF or Windows Form project
   //MessageBox.Show(email.ToString());
}

Resultados

  1. [email protected]
  2. [email protected]
  3. rgreen@set_ig.ca

Nota: Para la aplicación de consola y WPF o Windows Forms, debe agregar "using System.Xml.Linq;" Directiva de uso en la parte superior de su proyecto, para la consola también deberá agregar una referencia a este espacio de nombres antes de agregar la directiva de uso. Además, para la consola no habrá un archivo de recursos de forma predeterminada en la "Carpeta de propiedades", por lo que debe agregar manualmente el archivo de recursos. Los siguientes artículos de MSDN explican esto en detalle.

Agregar y editar recursos

Cómo: Agregar o quitar recursos

Ravi Ramnarine
fuente
1
No quiero ser malo aquí, pero su ejemplo no muestra nietos. emailAddress es un hijo de correos electrónicos. Me pregunto si hay una manera de usar Descendientes sin usar espacios de nombres.
SoftwareSavant