【问题标题】:Get the XPath to an XElement?获取 XElement 的 XPath?
【发布时间】:2010-10-01 21:42:33
【问题描述】:

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

例如myXElement.GetXPath()?

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

【问题讨论】:

    标签: c# xml xpath xelement


    【解决方案1】:

    扩展方法:

    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]
    

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

    【讨论】:

    • 这对于没有命名空间的 XML 非常有效。对于具有命名空间的文档,除非您愿意忍受手动构建和传递 XmlNamespaceManager 的繁重工作,否则Chaveiro's answer 是可行的方法。
    【解决方案2】:

    我更新了 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.");
        }
    }
    

    【讨论】:

    • 请注意,如果您的 namespacePrefix 计算结果为空字符串,您将得到一个带有无用分号的 ":elementName"。没什么大不了的,但我想我已经提到了。
    • @GrimaceofDespair 好点,随时编辑代码以考虑到这一点。
    【解决方案3】:

    让我分享我对这个类的最新修改。 基本上,如果元素没有兄弟元素,它会排除索引,并且包含带有 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】:

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

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

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

      【讨论】:

      • 其实并不是严格意义上的重复。这个问题是关于XDocumentXElement(LINQ to XML),而引用的问题是关于XmlNode(System.Xml)。然而,在引用的问题中提出的方法很简洁,可能很容易适应 LINQ to XML。
      • 所缺少的只是名称空间和属性,尽管这对于调整代码来提供它们应该是微不足道的。谢谢,罗伯特很整洁
      【解决方案5】:

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

      【讨论】:

        【解决方案6】:

        在某一时刻,我使用了这个更紧凑的表达式,以 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 &gt;1.0 版本的命名空间通配符之间进行选择:

        示例xpversion =&lt; 1 结果如下:/*[local-name(.) = 'AUTOSAR'][1]/*[local-name(.) = 'AR-PACKAGES'][1]/*[local-name(.) = 'AR-PACKAGE'][1]xpversion &gt; 1 结果如下:/*:AUTOSAR[1]/*:AR-PACKAGES[1]/*:AR-PACKAGE[1]/*:AR-PACKAGES[1]

        【讨论】:

          【解决方案7】:

          如果您正在寻找 .NET 原生提供的东西,答案是否定的。您必须编写自己的扩展方法才能做到这一点。

          【讨论】:

            【解决方案8】:

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

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

            【讨论】:

              【解决方案9】:

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

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

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

              【讨论】:

                【解决方案10】:

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

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

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

                • XPathSelectElement:选择单个元素
                • XPathSelectElements:选择元素并返回为IEnumerable&lt;XElement&gt;
                • XPathEvaluate:选择节点(不仅是元素,还包括文本、cmets 等)并返回为 IEnumerable&lt;object&gt;

                【讨论】:

                • OP 不是询问如何在给定 XPath 的情况下找到 XElement,而是询问如何获取特定 XElement 的 XPath,非常不同。
                猜你喜欢
                • 2020-07-07
                • 2012-02-24
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2021-06-06
                相关资源
                最近更新 更多