45

我在文档中有一个 XElement。给定 XElement(和 XDocument?),是否有扩展方法来获取其完整(即绝对,例如/root/item/element/child)XPath?

例如 myXElement.GetXPath()?

编辑:好的,看起来我忽略了一些非常重要的事情。哎呀!需要考虑元素的索引。有关建议的更正解决方案,请参阅我的最后一个答案。

4

10 回答 10

46

扩展方法:

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]

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

于 2009-01-18T03:34:17.010 回答
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.");
    }
}
于 2010-03-22T14:40:42.743 回答
7

让我分享我对这门课的最新修改。基本上,如果元素没有兄弟元素,它会排除索引,并且包含带有 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.");
    }
}
于 2014-05-08T12:02:53.637 回答
4

这实际上是这个问题的副本。虽然它没有被标记为答案,但对这个问题的回答中的方法是明确地将 XPath 公式化到 XML 文档中的节点的唯一方法,这种方法在所有情况下都始终有效。(它也适用于所有节点类型,而不仅仅是元素。)

如您所见,它生成的 XPath 既丑陋又抽象。但它解决了许多回答者在这里提出的担忧。此处提出的大多数建议都会生成一个 XPath,当用于搜索原始文档时,它将生成一组包含目标节点的一个或多个节点。这就是“或更多”的问题。例如,如果我有一个 DataSet 的 XML 表示,则指向特定 DataRow 元素的原始 XPath/DataSet1/DataTable1也返回 DataTable 中所有其他 DataRows 的元素。如果不了解 XML 的论坛化方式(例如,是否有主键元素?),您无法消除歧义。

但是/node()[1]/node()[4]/node()[11],无论如何,它只会返回一个节点。

于 2009-01-17T01:54:20.627 回答
3

作为另一个项目的一部分,我开发了一种扩展方法来为元素生成简单的 XPath。它与所选答案类似,但除 XElement 外,还支持 XAttribute、XText、XCData 和 XComment。它可以作为代码 nuget 使用,项目页面在这里:xmlspecificationcompare.codeplex.com

于 2013-09-01T12:51:15.420 回答
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]

于 2021-02-14T05:50:42.217 回答
0

如果您正在寻找 .NET 原生提供的东西,答案是否定的。您必须编写自己的扩展方法来执行此操作。

于 2009-01-16T21:07:45.823 回答
0

可以有多个 xpath 通向同一个元素,因此找到通向节点的最简单 xpath 并非易事。

也就是说,很容易找到节点的 xpath。只需向上提升节点树,直到您读取根节点并组合节点名称并且您有一个有效的 xpath。

于 2009-01-16T21:12:50.510 回答
0

“完整的 xpath”我假设您的意思是一个简单的标签链,因为可能匹配任何元素的 xpath 的数量可能非常大。

这里的问题是,即使不是特别不可能,也很难构建任何给定的 xpath,它将可逆地追溯到同一个元素——这是一个条件吗?

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

于 2009-01-16T21:14:18.323 回答
-1

自 .NET Framework 3.5 起,Microsoft 提供了一种扩展方法来执行此操作:

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

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

  • XPathSelectElement: 选择一个元素
  • XPathSelectElements: 选择元素并返回IEnumerable<XElement>
  • XPathEvaluate:选择节点(不仅是元素,还包括文本、评论等)并返回为IEnumerable<object>
于 2014-06-07T18:27:35.673 回答