获取XPath到XElement?
我在文档中有一个XElement。 鉴于XElement(和XDocument?),是否有扩展方法来获得它的完整(即绝对,例如/root/item/element/child
)XPath?
例如myXElement.GetXPath()?
编辑:好吧,看起来像我忽视了一些非常重要的东西。 哎呦! 元素的索引需要考虑在内。 请参阅我的最后一个答案提出更正解决scheme。
扩展方法:
public static class XExtensions { /// <summary> /// Get the absolute XPath to a given XElement /// (eg "/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."); } }
和testing:
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]
这应该解决这个问题。 没有?
我更新了克里斯的代码,以考虑命名空间前缀。 只有GetAbsoluteXPath方法被修改。
public static class XExtensions { /// <summary> /// Get the absolute XPath to a given XElement, including the namespace. /// (eg "/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."); } }
这实际上是这个问题的重复。 虽然它没有被标记为答案,但是我对这个问题的答案中的方法是明确地将XPathexpression式化为XML文档中的一个节点的唯一方式,该文档将始终在任何情况下都能正常工作。 (它也适用于所有节点types,而不仅仅是元素。)
正如你所看到的,它产生的XPath是丑陋而抽象的。 但它解决了许多答复者在这里提出的担忧。 这里提出的大部分build议都会生成一个XPath,用于search原始文档时,将生成一组包含目标节点的一个或多个节点。 这是“或者更多”,这就是问题所在。 例如,如果我具有DataSet的XML表示forms,那么对特定DataRow元素/DataSet1/DataTable1
的朴素XPath也将返回/DataSet1/DataTable1
中所有其他DataRow的元素。 如果不知道XML是如何进行论坛化的(例如,是否有一个主键元素?),你就不能消除歧义。
但是/node()[1]/node()[4]/node()[11]
,无论如何,只有一个节点会返回。
让我分享一下这个class的最新修改。 基本上它排除索引,如果元素没有兄弟姐妹,并包括名称空间与本地名()运算符我有问题的命名空间前缀。
public static class XExtensions { /// <summary> /// Get the absolute XPath to a given XElement, including the namespace. /// (eg "/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."); } }
作为另一个项目的一部分,我开发了一个扩展方法来生成一个简单的XPath元素。 它与所选的答案类似,但除了XElement外,还支持XAttribute,XText,XCData和XComment。 它可以作为代码nuget ,项目页面在这里: xmlspecificationcompare.codeplex.com
如果你正在寻找.NET本地提供的东西,答案是否定的。 你将不得不编写自己的扩展方法来做到这一点。
可以有几个xpath导致相同的元素,所以find通向节点的最简单的xpath并不是微不足道的。
也就是说,find节点的xpath是很容易的。 只需加紧节点树,直到读取根节点并合并节点名称,并且有一个有效的xpath。
通过“完整xpath”,我假设你的意思是一个简单的标签链,因为可能匹配任何元素的xpath数量可能非常大。
这里的问题是,如果不是特别不可能build立任何可逆地追溯到相同元素的给定xpath,这是非常困难的 – 是一个条件?
如果“否”,那么也许你可以通过recursion循环引用当前元素parentNode来build立一个查询。 如果“是”,那么你将会考虑通过交叉引用索引位置在同级集合中进行扩展,引用类似于ID的属性(如果它们存在的话),如果一个通用的解决scheme将会非常依赖于你的XSD是可能的。
微软提供了一个自.NET Framework 3.5开始的扩展方法:
http://msdn.microsoft.com/en-us/library/bb156083(v=vs.100).aspx
只需将一个using添加到System.Xml.XPath
并调用以下方法:
-
XPathSelectElement
:select一个元素 -
XPathSelectElements
:select元素并返回IEnumerable<XElement>
-
XPathEvaluate
:select节点(不仅是元素,还包括文本,注释等),并返回IEnumerable<object>