获取XElement的XPath?

47

我有一个位于文档深处的XElement。给定这个XElement(以及XDocument?),是否有扩展方法可以获取它的完整XPath(即绝对路径,例如/root/item/element/child)?

例如,myXElement.GetXPath()?

编辑: 好的,看起来我忽视了一些非常重要的东西。糟糕!元素的索引需要被考虑进去。请查看我的最后一个答案以获取已提出的更正解决方案。

10个回答

48

这些是扩展方法:

public static class XExtensions
{
    /// <summary>
    /// Get the absolute XPath to a given XElement
    /// (e.g. "/people/person[6]/name[1]/last[1]").
    /// </summary>
    public static string GetAbsoluteXPath(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        Func<XElement, string> relativeXPath = e =>
        {
            int index = e.IndexPosition();
            string name = e.Name.LocalName;

            // If the element is the root, no index is required

            return (index == -1) ? "/" + name : string.Format
            (
                "/{0}[{1}]",
                name, 
                index.ToString()
            );
        };

        var ancestors = from e in element.Ancestors()
                        select relativeXPath(e);

        return string.Concat(ancestors.Reverse().ToArray()) + 
               relativeXPath(element);
    }

    /// <summary>
    /// Get the index of the given XElement relative to its
    /// siblings with identical names. If the given element is
    /// the root, -1 is returned.
    /// </summary>
    /// <param name="element">
    /// The element to get the index of.
    /// </param>
    public static int IndexPosition(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        if (element.Parent == null)
        {
            return -1;
        }

        int i = 1; // Indexes for nodes start at 1, not 0

        foreach (var sibling in element.Parent.Elements(element.Name))
        {
            if (sibling == element)
            {
                return i;
            }

            i++;
        }

        throw new InvalidOperationException
            ("element has been removed from its parent.");
    }
}

还有测试:

class Program
{
    static void Main(string[] args)
    {
        Program.Process(XDocument.Load(@"C:\test.xml").Root);
        Console.Read();
    }

    static void Process(XElement element)
    {
        if (!element.HasElements)
        {
            Console.WriteLine(element.GetAbsoluteXPath());
        }
        else
        {
            foreach (XElement child in element.Elements())
            {
                Process(child);
            }
        }
    }
}

还有样例输出:

/tests/test[1]/date[1]
/tests/test[1]/time[1]/start[1]
/tests/test[1]/time[1]/end[1]
/tests/test[1]/facility[1]/name[1]
/tests/test[1]/facility[1]/website[1]
/tests/test[1]/facility[1]/street[1]
/tests/test[1]/facility[1]/state[1]
/tests/test[1]/facility[1]/city[1]
/tests/test[1]/facility[1]/zip[1]
/tests/test[1]/facility[1]/phone[1]
/tests/test[1]/info[1]
/tests/test[2]/date[1]
/tests/test[2]/time[1]/start[1]
/tests/test[2]/time[1]/end[1]
/tests/test[2]/facility[1]/name[1]
/tests/test[2]/facility[1]/website[1]
/tests/test[2]/facility[1]/street[1]
/tests/test[2]/facility[1]/state[1]
/tests/test[2]/facility[1]/city[1]
/tests/test[2]/facility[1]/zip[1]
/tests/test[2]/facility[1]/phone[1]
/tests/test[2]/info[1]

那应该解决了这个问题。不是吗?


1
这对没有命名空间的XML非常有效。对于具有命名空间的文档,除非您愿意忍受手动构建和传递XmlNamespaceManager的繁重工作,否则Chaveiro的答案是正确的选择。 - DumpsterDoofus

11

我更新了Chris的代码以考虑命名空间前缀。只有GetAbsoluteXPath方法被修改。

public static class XExtensions
{
    /// <summary>
    /// Get the absolute XPath to a given XElement, including the namespace.
    /// (e.g. "/a:people/b:person[6]/c:name[1]/d:last[1]").
    /// </summary>
    public static string GetAbsoluteXPath(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        Func<XElement, string> relativeXPath = e =>
        {
            int index = e.IndexPosition();

            var currentNamespace = e.Name.Namespace;

            string name;
            if (currentNamespace == null)
            {
                name = e.Name.LocalName;
            }
            else
            {
                string namespacePrefix = e.GetPrefixOfNamespace(currentNamespace);
                name = namespacePrefix + ":" + e.Name.LocalName;
            }

            // If the element is the root, no index is required
            return (index == -1) ? "/" + name : string.Format
            (
                "/{0}[{1}]",
                name,
                index.ToString()
            );
        };

        var ancestors = from e in element.Ancestors()
                        select relativeXPath(e);

        return string.Concat(ancestors.Reverse().ToArray()) +
               relativeXPath(element);
    }

    /// <summary>
    /// Get the index of the given XElement relative to its
    /// siblings with identical names. If the given element is
    /// the root, -1 is returned.
    /// </summary>
    /// <param name="element">
    /// The element to get the index of.
    /// </param>
    public static int IndexPosition(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        if (element.Parent == null)
        {
            return -1;
        }

        int i = 1; // Indexes for nodes start at 1, not 0

        foreach (var sibling in element.Parent.Elements(element.Name))
        {
            if (sibling == element)
            {
                return i;
            }

            i++;
        }

        throw new InvalidOperationException
            ("element has been removed from its parent.");
    }
}

2
请注意,如果您的namespacePrefix计算为空字符串,则会得到一个带有无用分号的“:elementName”。这不是什么大问题,但我想提一下。 - Grimace of Despair
@GrimaceofDespair 很好的建议,随意编辑代码以考虑此事。 - Bernard Vander Beken

9

让我分享一下对这个类的最新修改。基本上,它在元素没有兄弟姐妹的情况下排除了索引,并使用local-name()运算符包括命名空间,因为我在命名空间前缀方面遇到了问题。

public static class XExtensions
{
    /// <summary>
    /// Get the absolute XPath to a given XElement, including the namespace.
    /// (e.g. "/a:people/b:person[6]/c:name[1]/d:last[1]").
    /// </summary>
    public static string GetAbsoluteXPath(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }


        Func<XElement, string> relativeXPath = e =>
        {
            int index = e.IndexPosition();

            var currentNamespace = e.Name.Namespace;

            string name;
            if (String.IsNullOrEmpty(currentNamespace.ToString()))
            {
                name = e.Name.LocalName;
            }
            else
            {
                name = "*[local-name()='" + e.Name.LocalName + "']";
                //string namespacePrefix = e.GetPrefixOfNamespace(currentNamespace);
                //name = namespacePrefix + ":" + e.Name.LocalName;
            }

            // If the element is the root or has no sibling elements, no index is required
            return ((index == -1) || (index == -2)) ? "/" + name : string.Format
            (
                "/{0}[{1}]",
                name,
                index.ToString()
            );
        };

        var ancestors = from e in element.Ancestors()
                        select relativeXPath(e);

        return string.Concat(ancestors.Reverse().ToArray()) +
               relativeXPath(element);
    }

    /// <summary>
    /// Get the index of the given XElement relative to its
    /// siblings with identical names. If the given element is
    /// the root, -1 is returned or -2 if element has no sibling elements.
    /// </summary>
    /// <param name="element">
    /// The element to get the index of.
    /// </param>
    public static int IndexPosition(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        if (element.Parent == null)
        {
            // Element is root
            return -1;
        }

        if (element.Parent.Elements(element.Name).Count() == 1)
        {
            // Element has no sibling elements
            return -2;
        }

        int i = 1; // Indexes for nodes start at 1, not 0

        foreach (var sibling in element.Parent.Elements(element.Name))
        {
            if (sibling == element)
            {
                return i;
            }

            i++;
        }

        throw new InvalidOperationException
            ("element has been removed from its parent.");
    }
}

4

这实际上是这个问题的重复。虽然它没有被标记为答案,但在我的回答中提到的方法是在所有情况下都能无歧义地制定XML文档中节点的XPath的唯一方式。(它也适用于所有节点类型,而不仅仅是元素.)

正如你所看到的,它生成的XPath非常丑陋和抽象,但它解决了很多回答者所提出的问题。这里提出的大多数建议会产生一个XPath,当用于搜索原始文档时,将产生包括目标节点的一个或多个节点集。问题在于“或多个”。例如,如果我有一个DataSet的XML表示形式,对于特定DataRow元素的简单XPath,/DataSet1/DataTable1,也会返回DataTable中所有其他DataRows的元素。除非你知道XML是如何构造的(比如,有一个主键元素吗?),否则你无法消除歧义。

但是,/node()[1]/node()[4]/node()[11]只有一个节点会被返回,无论什么情况。


1
实际上并不是严格的重复。这个问题是关于XDocumentXElement(LINQ to XML)的,而参考的问题是关于XmlNode(System.Xml)的。尽管如此,参考问题中提出的方法很好,并且可以很容易地适应于LINQ to XML。 - paytools-steve
只缺少命名空间和属性,不过将代码适应以提供它们应该很容易。谢谢,Robert非常棒。 - Newtopian

3

1

我曾经在某个时候使用过这种更紧凑的表达方式,目标是 C#.Net Framework 4.8

public static string GetAbsoluteXPath(XElement element,int xpversion)
{
    IEnumerable<XElement> ancestors = element.AncestorsAndSelf();
    string xpath = ancestors.Aggregate(new StringBuilder(),
                        (str, elem) => str.Insert(0, (xpversion > 1 ? ("/*:" + elem.Name.LocalName) : ("/*[local-name(.) = '" + elem.Name.LocalName + "']")) + "[" + (int)(elem.ElementsBeforeSelf().Where(el => el.Name.LocalName == elem.Name.LocalName).Count() + 1) + "]"),
                        str => str.ToString());
    return xpath;
}

作为一般解决方案表现良好,但有时速度有点慢(不太符合我的口味 :-))。使用xpversion可以在XPath 1.0XPath >1.0版本的命名空间通配符之间进行选择: 示例:使用xpversion =< 1,结果如下所示:/*[local-name(.) = 'AUTOSAR'][1]/*[local-name(.) = 'AR-PACKAGES'][1]/*[local-name(.) = 'AR-PACKAGE'][1];而使用xpversion > 1,结果如下:/*:AUTOSAR[1]/*:AR-PACKAGES[1]/*:AR-PACKAGE[1]/*:AR-PACKAGES[1]

0

如果你正在寻找.NET原生提供的功能,答案是否定的。你需要编写自己的扩展方法来实现这一点。


0

可能有几个XPath可以导向同一个元素,因此找到导向节点的最简单XPath并不容易。

话虽如此,找到节点的XPath还是相当容易的。只需沿着节点树向上走,直到读取根节点并组合节点名称,就可以得到有效的XPath。


0

通过“完整的xpath”,我认为您指的是一系列简单的标签,因为可能与任何元素匹配的xpath数量非常大。

问题在于,如果不是特别不可能构建任何给定的xpath,以便可逆地跟踪回同一元素-这是一个条件吗?

如果是“否”,那么您可以通过引用当前元素的parentNode进行递归循环来构建查询。如果是“是”,那么您将需要查看扩展交叉引用兄弟集中的索引位置,引用类似ID的属性(如果存在),并且这将非常依赖于您的XSD,如果可能的话,需要一个通用解决方案。


-1

自 .NET Framework 3.5 起,Microsoft 提供了一个扩展方法来实现此功能:

http://msdn.microsoft.com/en-us/library/bb156083(v=vs.100).aspx

只需添加一个 using 到 System.Xml.XPath 并调用以下方法:

  • XPathSelectElement:选择单个元素
  • XPathSelectElements:选择元素并作为 IEnumerable<XElement> 返回
  • XPathEvaluate:选择节点(不仅限于元素,还包括文本、注释等)并作为 IEnumerable<object> 返回

3
OP并不是在问如何根据XPath查找XElement,而是如何获取特定XElement的XPath,这两者非常不同。 - Cameron

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