2009-01-16 51 views
39

我在文档中深入了解了XElement。鉴于XElement(和XDocument?),是否有扩展方法来获取其全部(即绝对,例如)XPath?获取XPath到XElement?

E.g. myXElement.GetXPath()?

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

回答

36

的扩展方法:

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] 

这应该解决这个。没有?

+0

这对于没有名称空间的XML非常适用。对于具有名称空间的文档,除非您愿意忍受手动构建和传递XmlNamespaceManager的繁琐工作,否则[Chaveiro的答案](http://stackoverflow.com/a/23541182/3051203)就是要走的路。 – DumpsterDoofus 2015-12-28 21:22:39

0

如果您正在寻找.NET本地提供的东西,答案是否定的。你将不得不编写自己的扩展方法来做到这一点。

0

可能有几个xpath导致相同的元素,因此找到通向节点的最简单的xpath并不是微不足道的。

也就是说,找到节点的xpath非常简单。只需加紧节点树,直到读取根节点并合并节点名称并且有一个有效的xpath。

0

通过“full xpath”我假设你的意思是一个简单的标签链,因为可能匹配任何元素的xpaths的数量可能是非常大的

这里的问题是,如果不是特别不可能建立任何给定的xpath,它将可逆地追溯到相同的元素是非常困难的 - 是一个条件?

如果“否”,那么也许您可以通过递归循环引用当前元素parentNode来构建查询。如果“是”,那么你会考虑通过交叉引用索引位置在同级集内进行扩展,如果它们存在,则引用ID类属性,如果通用解决方案将非常依赖于XSD是可能的。

4

这实际上是this问题的副本。尽管它没有被标记为答案,但是对于该问题的my answer中的方法是将XMLath明确地表达为XML文档中的节点的唯一方式,该XML文档将始终在所有情况下工作。 (它也适用于所有节点类型,而不仅仅是元素。)

正如你所看到的,它产生的XPath是丑陋和抽象的。但它解决了许多回答者在此提出的担忧。这里提出的大部分建议都会生成一个XPath,用于搜索原始文档时,将生成一组包含目标节点的一个或多个节点。这就是“甚至更多”这就是问题所在。例如,如果我有一个DataSet的XML表示,那么对特定DataRow元素/DataSet1/DataTable1的朴素XPath还会返回DataTable中所有其他DataRow的元素。如果不知道XML是如何进行论坛化的(例如,是否存在主键元素?),您就不能消除歧义。

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

+1

其实并不是严格意义上的重复。这个问题是关于`XDocument`和`XElement`(LINQ to XML),而引用的问题是关于`XmlNode`(System.Xml)。尽管如此,被引用问题中提出的方法很简单,可能很容易适应与LINQ to XML一起工作。 – SteveWilkinson 2011-06-20 14:54:01

+0

所有缺少的是名称空间和属性,尽管这应该是微不足道的以适应代码提供它们。谢谢,罗伯特非常整洁 – Newtopian 2015-02-12 15:55:13

10

我更新了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”。没什么大不了的,但我想我会提到的。 – 2016-07-01 09:03:28

4

让我分享我最新的修改到这个类。 Basicaly它排除索引如果元素没有兄弟姐妹,并包含名称空间与本地名()运算符我有问题的命名空间前缀。

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."); 
    } 
} 
-1

微软已经提供了一个扩展的方法来做到这一点,因为.NET Framework 3.5的:

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

只是使用添加到System.Xml.XPath并调用下面的方法:

  • XPathSelectElement :选择单个元素
  • XPathSelectElements:选择元素,并返回为IEnumerable<XElement>
  • XPathEvaluate:选择节点(不仅是要素,而且文本,注释等),并返回作为IEnumerable<object>