我在文档中有一个XElement。给定XElement(和XDocument?),是否有一个扩展方法来获取其完整(即绝对,例如/root/item/element/child
)XPath?
E.g。 myXElement.GetXPath()?
修改: 好吧,看起来我忽略了一些非常重要的事情。哎呦!需要考虑元素的索引。请参阅我对修正后的解决方案的最后答案。
答案 0 :(得分:42)
扩展方法:
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 :(得分: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 :(得分:4)
这实际上是this问题的副本。虽然它没有被标记为答案,但my answer中针对该问题的方法是明确地将XPath制定为XML文档中的节点的唯一方法,该节点在所有情况下都将始终有效。 (它也适用于所有节点类型,而不仅仅是元素。)
正如您所看到的,它产生的XPath是丑陋而抽象的。但它解决了许多回答者在这里提出的担忧。这里提出的大多数建议都会产生一个XPath,当用于搜索原始文档时,它将生成一组包含目标节点的一个或多个节点。这就是“或更多”这就是问题所在。例如,如果我有一个DataSet的XML表示,那么对特定DataRow的元素/DataSet1/DataTable1
的朴素XPath也会返回DataTable中所有其他DataRows的元素。如果不知道如何对XML进行论坛化(例如,是否存在主键元素?),就无法消除歧义。
但是/node()[1]/node()[4]/node()[11]
,无论如何,它只会返回一个节点。
答案 3 :(得分:4)
让我分享我对这堂课的最新修改。 如果元素没有兄弟,并且包含带有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 :(得分:2)
作为different project的一部分,我开发了一种扩展方法来为元素生成简单的XPath。它与选定的答案类似,但除XElement外还支持XAttribute,XText,XCData和XComment。 它以code nuget形式提供,项目页面位于xmlspecificationcompare.codeplex.com
答案 5 :(得分: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.0
和 XPath >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]
答案 6 :(得分:0)
如果你正在寻找.NET本身提供的东西,答案是否定的。您必须编写自己的扩展方法才能执行此操作。
答案 7 :(得分:0)
可能有几个xpath导致相同的元素,因此找到导致节点的最简单的xpath并不是一件容易的事。
也就是说,找到节点的xpath非常容易。只需升级节点树,直到您读取根节点并组合节点名称,并且您有一个有效的xpath。
答案 8 :(得分:0)
通过“完整的xpath”我假设你的意思是一个简单的标签链,因为可能与任何元素匹配的xpath数量可能非常大。
这里的问题是,如果没有特别不可能构建任何给定的xpath,它将可逆地追溯到同一个元素,这是一个非常困难的条件吗?
如果“no”那么也许您可以通过引用当前元素parentNode的递归循环来构建查询。如果“是”,那么您将考虑通过交叉引用兄弟集中的索引位置来扩展它,如果它们存在则引用类似ID的属性,如果是一般解决方案,这将非常依赖于您的XSD是可能的。
答案 9 :(得分:-1)
自.NET Framework 3.5以来,Microsoft提供了一种扩展方法:
http://msdn.microsoft.com/en-us/library/bb156083(v=vs.100).aspx
只需将使用添加到System.Xml.XPath
并调用以下方法:
XPathSelectElement
:选择一个元素XPathSelectElements
:选择元素并返回IEnumerable<XElement>
XPathEvaluate
:选择节点(不仅是元素,还包括文字,评论等)并以IEnumerable<object>