Consulta um XDocument para elementos por nome em qualquer profundidade

143

Eu tenho um XDocumentobjeto Desejo consultar elementos com um nome específico em qualquer profundidade usando o LINQ. Quando uso Descendants("element_name"), recebo apenas elementos que são filhos diretos do nível atual. O que estou procurando é o equivalente a "// element_name" no XPath ... devo apenas usar XPathou existe uma maneira de fazê-lo usando os métodos LINQ? Obrigado.

Rico
fonte

Respostas:

213

Descendentes devem funcionar absolutamente bem. Aqui está um exemplo:

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
fonte
1
Como você resolveria isso se um nome de elemento fosse duplicado em um documento xml? Por exemplo: Se o xml continha uma coleção de <Cars> com subelementos de <Part>, e também uma coleção de <Planes> com subelementos de <Part>, e você deseja uma lista apenas de Parts for Cars.
pfeds
12
@pfeds: Então eu usaria doc.Descendants("Cars").Descendants("Part")(ou, eventualmente, .Elements("Part")se fossem apenas crianças diretos.
Jon Skeet
8
Seis anos depois e ainda um exemplo fantástico. Na verdade, isso ainda é muito mais útil do que a explicação MSDN :-)
EvilDr
E ainda é um exemplo ruim, Dr., pois se não houver "Carros", o código acima resultaria em um NPE. Talvez o .? do novo C # finalmente o tornará válido
Dror Harari
3
@DrorHarari Não, nenhuma exceção é lançada: Experimente var foo = new XDocument().Descendants("Bar").Descendants("Baz"); porque Descendantsretorna um vazio IEnumerable<XElement>e não null.
DareDude
54

Um exemplo indicando o espaço para nome:

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
fonte
2
Mas, e se meu xml de origem não tiver um espaço para nome? Suponho que posso adicionar um no código (preciso analisar isso), mas por que isso é necessário? De qualquer forma, o root.Descendants ("myTagName") não encontra elementos enterrados em três ou quatro níveis no meu código.
EoRaptor013
2
Obrigado! Estamos usando a serialização de contrato de dados. Isso cria um cabeçalho como <MyClassEntries xmlns: i = " w3.org/2001/XMLSchema-instance " xmlns = " schemas.datacontract.org/2004/07/DataLayer.MyClass "> e fiquei surpreso por não estar recebendo quaisquer descendentes. Eu precisava adicionar o prefixo { schemas.datacontract.org/2004/07/DataLayer.MyClass }.
Kim
38

Você pode fazer desta maneira:

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

onde xmlé uma XDocument.

Esteja ciente de que a propriedade Nameretorna um objeto que possui a LocalNamee a Namespace. É por isso que você precisa usar Name.LocalNamese quiser comparar pelo nome.

Francisco Goldenstein
fonte
Estou tentando obter todo o nó EmbeddedResource do arquivo de projeto do c #, e é dessa maneira que funciona. Documento XDocument = XDocument.Load (csprojPath); IEnumerable <XElement> embeddedResourceElements = document.Descendants ("EmbeddedResource"); Não é trabalhos e eu não entendo o porquê.
Eugene Maksimov
22

Os descendentes farão exatamente o que você precisa, mas não se esqueça de incluir um nome de espaço para nome junto com o nome do elemento. Se você omitir, provavelmente receberá uma lista vazia.

Nenad Dobrilovic
fonte
11

Existem duas maneiras de conseguir isso,

  1. Linq para xml
  2. XPath

A seguir, exemplos de como usar essas abordagens,

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

Se você usa o XPath, é necessário fazer alguma manipulação com o IEnumerable:

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

Observe que

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

resulta um ponteiro nulo ou nenhum resultado.

roland roos
fonte
1
apenas para mencionar que XPathEvaluateestá no System.Xml.XPathespaço para nome.
Tahir Hassan
XPathEvaluate deve fazer o truque, mas sua consulta aceita apenas nós em uma profundidade específica (um). Se você quiser selecionar todos os elementos chamados "email", independentemente de onde eles ocorram em um documento, use o caminho "// email". Obviamente, esses caminhos são mais caros, pois a árvore inteira deve ser percorrida seja qual for o nome, mas pode ser bastante conveniente - desde que você saiba o que está fazendo.
The Dag
8

Estou usando o XPathSelectElementsmétodo de extensão que funciona da mesma maneira que o 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
fonte
1

Após a resposta de @Francisco Goldenstein, escrevi um método de extensão

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

sabemos que o exposto acima é verdadeiro. Jon nunca está errado; desejos da vida real podem ir um pouco mais longe

<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 exemplo, geralmente o problema é: como podemos obter o EchoToken no documento xml acima? Ou como desfocar o elemento com o nome attrbute.

1- Você pode encontrá-los acessando com o namespace e o nome como abaixo

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

2- Você pode encontrá-lo pelo valor do conteúdo do atributo, como este

Hamit YILDIRIM
fonte
0

Essa é minha variante da solução baseada no Linqmétodo Descendants e da XDocumentclasse

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 mais detalhes sobre o Desendantsmétodo, dê uma olhada aqui.

Mselmi Ali
fonte
-1

(O código e as instruções são para C # e podem precisar ser ligeiramente alterados para outros idiomas)

Este exemplo funciona perfeitamente se você deseja ler de um Nó Pai que possui muitos filhos, por exemplo, observe o seguinte XML;

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

Agora, com este código abaixo (lembre-se de que o arquivo XML está armazenado em recursos (consulte os links no final do snippet para obter ajuda sobre recursos)) Você pode obter cada endereço de email na tag "emails".

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 aplicativo de console e WPF ou Windows Forms, você deve adicionar o "using System.Xml.Linq;" Diretiva Using na parte superior do seu projeto, para o Console, você também precisará adicionar uma referência a este espaço para nome antes de adicionar a diretiva Using. Também para o Console, por padrão, não haverá arquivo de recursos na "pasta Propriedades", portanto você deve adicionar manualmente o arquivo de recursos. Os artigos do MSDN abaixo explicam isso em detalhes.

Adicionando e editando recursos

Como: Adicionar ou remover recursos

Ravi Ramnarine
fonte
1
Não quero ser mau aqui, mas seu exemplo não mostra netos. emailAddress é filho de emails. Gostaria de saber se existe uma maneira de usar Descendentes sem usar espaços para nome?
SoftwareSavant