在任何深度上按名称查询XDocument中的元素

155

我有一个XDocument对象。我想使用LINQ查询任何深度上具有特定名称的元素。

当我使用Descendants("element_name")时,我只能得到当前级别的直接子元素。我正在寻找与XPath中的"//element_name"等效的方法...我应该只使用XPath,还是有一种方法可以使用LINQ方法进行操作?

10个回答

239

后代选择器应该完全没有问题。这是一个例子:

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

结果:

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


1
如果在 XML 文档中存在重复的元素名称,你会如何解决?例如:如果 XML 包含一组带有子元素 <Part> 的 <Cars>,以及一组带有子元素 <Part> 的 <Planes>,而你只想要 Cars 的零件列表。 - pfeds
14
我会尽力为您翻译:@pfeds: 然后我会使用doc.Descendants("Cars").Descendants("Part")(如果它们仅是直接子节点,则可能使用.Elements("Part"))。 - Jon Skeet
11
六年过去了,这仍然是一个很好的例子。事实上,这仍然比MSDN的解释更有帮助 :-) - EvilDr
这仍然是一个恶劣的例子,博士,因为如果没有“汽车”,上面的代码将导致NPE。也许新的C#中的.?最终会使其有效。 - Dror Harari
3
不会抛出任何异常:尝试运行var foo = new XDocument().Descendants("Bar").Descendants("Baz");,因为Descendants返回一个空的IEnumerable<XElement>而不是null - DareDude
14年过去了,仍需要这个作为例子... - bas

61

一个指示命名空间的示例:

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

2
但是,如果我的源XML没有命名空间怎么办?我想我可以在代码中添加一个(必须研究一下),但为什么这是必要的?无论如何,root.Descendants("myTagName")无法找到在我的代码中深藏三四层的元素。 - EoRaptor013
2
谢谢!我们正在使用数据合同序列化。这将创建一个类似于<MyClassEntries xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/DataLayer.MyClass">的头文件,我曾经困惑为什么我没有得到任何后代。 我需要添加{http://schemas.datacontract.org/2004/07/DataLayer.MyClass}前缀。 - Kim
经过数小时的搜索和实验,这是唯一有帮助的答案。我无法感谢你们足够多。为将命名空间添加到后代而致敬。 - Maher Nabil

54

您可以这样做:

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

其中 xml 是一个 XDocument 对象。

请注意,属性Name返回一个对象,该对象具有LocalNameNamespace。这就是为什么如果您想按名称进行比较,必须使用 Name.LocalName


我正在尝试从C#项目文件中获取所有的EmbeddedResource节点,这是唯一有效的方法。XDocument document = XDocument.Load(csprojPath); IEnumerable<XElement> embeddedResourceElements = document.Descendants("EmbeddedResource");但是它不起作用,我不明白为什么。 - Eugene Maksimov

22

Descendants会完全按照您的需求执行操作,但请确保在元素名称中包括命名空间名称。如果省略它,您可能会得到一个空列表。


16

有两种方法可以实现这一点,

  1. LINQ to XML
  2. XPath

以下是使用这些方法的示例,

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

如果您使用XPath,您需要对IEnumerable进行一些操作:
IEnumerable<XElement> mails = ((IEnumerable)doc.XPathEvaluate("/emails/emailAddress")).Cast<XElement>();

请注意,这里需要说明的是:
var res = doc.XPathEvaluate("/emails/emailAddress");

结果可能是一个空指针,也可能没有结果。


3
只是提一下,XPathEvaluateSystem.Xml.XPath命名空间中。 - Tahir Hassan
XPathEvaluate应该可以解决问题,但是您的查询仅获取特定深度(一层)的节点。如果您想选择文档中出现的所有名为“email”的元素,则应使用路径“//email”。显然,这样的路径更加耗费资源,因为无论名称如何,整个树都必须被遍历,但是它可能非常方便-前提是您知道自己在做什么。 - The Dag

8

我正在使用XPathSelectElements扩展方法,它的工作方式与XmlDocument.SelectNodes方法相同:

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

1
这是我基于LINQ和XDocument类的Descendants方法的解决方案变体。
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();
    }
}

结果:

想要了解更多关于Desendants方法的细节,请查看这里。


1

在Francisco Goldenstein的回答后,我编写了一个扩展方法。

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

0

我们知道上面的话是真的。Jon从来不会错;现实生活中的愿望可以更进一步。

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

例如,通常的问题是如何在上述XML文档中获取EchoToken?或者如何模糊具有名称属性的元素。
  1. 您可以通过以下方式来访问带有命名空间和名称的元素:

    doc.Descendants().Where(p => p.Name.LocalName == "OTA_AirAvailRQ").Attributes("EchoToken").FirstOrDefault().Value
    
  2. 您可以通过属性内容值来查找,就像这个


-1

(代码和指令适用于C#,但可能需要稍微修改以适用于其他语言)

如果您想从具有许多子节点的父节点中读取,例如查看以下XML,则此示例非常完美;

<?xml version="1.0" encoding="UTF-8"?> 
<emails>
    <emailAddress>jdoe@set.ca</emailAddress>
    <emailAddress>jsmith@hit.ca</emailAddress>
    <emailAddress>rgreen@set_ig.ca</emailAddress> 
</emails>

现在,通过以下代码(请记住XML文件存储在资源中(有关资源的帮助,请参见片段末尾的链接),您可以获取“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());
}

结果

  1. jdoe@set.ca
  2. jsmith@hit.ca
  3. rgreen@set_ig.ca

注意:对于控制台应用程序和WPF或Windows Forms,您必须在项目顶部添加“using System.Xml.Linq;”Using指令,在添加Using指令之前,您还需要为控制台添加对此命名空间的引用。此外,对于控制台,默认情况下不会在“属性文件夹”下有资源文件,因此您必须手动添加资源文件。下面的MSDN文章详细解释了这一点。

添加和编辑资源

如何添加或删除资源


1
不想说得太刻薄,但是你的示例没有显示孙子。emailAddress是emails的子级。我想知道是否有一种方法可以在不使用命名空间的情况下使用Descendants? - SoftwareSavant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接