如果我在节点上放置了XPathNavigator
,我如何从根目录获取表示该节点路径的XPath表达式?
例如,如果XML是:
<data>
<class name='dogs'>
<item name='doberman />
<item name='husky' />
</class>
<class name='cats'>
<item name='persian' />
<item name='tabby' />
</class> </data>
</data>
...然后波斯猫的路径可以表示为/data/class[2]/item[1]
我可以使用SelectAncestors()
枚举相关节点的祖先(或者我可以迭代地爬上与SelectParent()
的父关系),但这并不能获得位置信息。
我是否必须为每个祖先使用position()
评估XPath,还是有更好的方法来执行此操作?
答案 0 :(得分:6)
假设您只对xml元素的xpath感兴趣,我在XmlElement
上实现了一个强力算法(即遍历XML结构)作为扩展方法。这与@ Zenexer的答案非常相似,虽然我在发布他的时候已经开始使用我自己的版本了。
另外,受到Alexei关于性能的提示的启发,我使用一个有点复杂的XML文件创建了一种测试用例。然后我实现了相同算法的两个版本;一个依赖于PreviousSibling,另一个依赖迭代节点。第三个版本依赖于XPath的position()
函数,但它没有按预期工作并被丢弃。
虽然你应该自己检查一下,但在我的机器中,结果显示对于迭代版本来说显着的性能优势 - 对于兄弟姐妹版本得分为21秒而言是1.7秒。
Importart:这些扩展方法在static class XmlElementExtension
。
public static string GetXPath_UsingPreviousSiblings(this XmlElement element)
{
string path = "/" + element.Name;
XmlElement parentElement = element.ParentNode as XmlElement;
if (parentElement != null)
{
// Gets the position within the parent element, based on previous siblings of the same name.
// However, this position is irrelevant if the element is unique under its parent:
XPathNavigator navigator = parentElement.CreateNavigator();
int count = Convert.ToInt32(navigator.Evaluate("count(" + element.Name + ")"));
if (count > 1) // There's more than 1 element with the same name
{
int position = 1;
XmlElement previousSibling = element.PreviousSibling as XmlElement;
while (previousSibling != null)
{
if (previousSibling.Name == element.Name)
position++;
previousSibling = previousSibling.PreviousSibling as XmlElement;
}
path = path + "[" + position + "]";
}
// Climbing up to the parent elements:
path = parentElement.GetXPath_UsingPreviousSiblings() + path;
}
return path;
}
public static string GetXPath_SequentialIteration(this XmlElement element)
{
string path = "/" + element.Name;
XmlElement parentElement = element.ParentNode as XmlElement;
if (parentElement != null)
{
// Gets the position within the parent element.
// However, this position is irrelevant if the element is unique under its parent:
XmlNodeList siblings = parentElement.SelectNodes(element.Name);
if (siblings != null && siblings.Count > 1) // There's more than 1 element with the same name
{
int position = 1;
foreach (XmlElement sibling in siblings)
{
if (sibling == element)
break;
position++;
}
path = path + "[" + position + "]";
}
// Climbing up to the parent elements:
path = parentElement.GetXPath_SequentialIteration() + path;
}
return path;
}
private static void Measure(string functionName, int iterations, Action implementation)
{
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < iterations; i++)
{
implementation();
}
watch.Stop();
Console.WriteLine("{0}: {1}ms", functionName, watch.ElapsedMilliseconds);
}
private static void Main(string[] args)
{
XmlDocument doc = new XmlDocument();
doc.Load(@"location of some large and complex XML file");
string referenceXPath = "/vps/vendorProductSets/vendorProductSet/product[100]/prodName/locName";
Measure("UsingPreviousSiblings", 10000,
() =>
{
XmlElement target = doc.SelectSingleNode(referenceXPath) as XmlElement;
Debug.Assert(referenceXPath == target.GetXPath_UsingPreviousSiblings());
});
Measure("SequentialIteration", 10000,
() =>
{
XmlElement target = doc.SelectSingleNode(referenceXPath) as XmlElement;
Debug.Assert(referenceXPath == target.GetXPath_SequentialIteration());
});
}
答案 1 :(得分:3)
未测试;仅适用于从XmlDocument对象创建的XPathNavigator对象:
private static string GetPath(this XPathNavigator navigator)
{
StringBuilder path = new StringBuilder();
for (XmlNode node = navigator.UnderlyingObject as XmlNode; node != null; node = node.ParentNode)
{
string append = "/" + path;
if (node.ParentNode != null && node.ParentNode.ChildNodes.Count > 1)
{
append += "[";
int index = 1;
while (node.PreviousSibling != null)
{
index++;
}
append += "]";
}
path.Insert(0, append);
}
return path.ToString();
}
以下是您将如何使用它:
XPathNavigator navigator = /* ... */;
string path = navigator.GetPath();
XPathNavigator对象通常位于根节点上。创建它们后,它们的位置无法更改,但您可以使用它们来选择后代。也许有办法完全避免这个问题?例如,如果您只想要当前节点,则可以使用XPathNavigator.UnderlyingObject
,如示例所示。
答案 2 :(得分:2)
使用ParentNode的简单解决方案。只需向上移动直到到达根节点并记住您传递的每个节点名称。测试!
// Get the node full path
static string getPath(XmlNode node)
{
string path = node.Name;
XmlNode search = null;
// Get up until ROOT
while ((search = node.ParentNode).NodeType != XmlNodeType.Document)
{
path = search.Name + "/" + path; // Add to path
node = search;
}
return "//"+path;
}
答案 3 :(得分:0)
这是我从 XPathDocument 创建的 XPathNavigator 的解决方案。它迭代到根节点,同时检查路径中的每个节点是否在其上方有兄弟节点。
//returns full xpath, can make it extension method
protected string GetFullXpath(XPathNavigator navigator)
{
var firstNodeEscape = navigator.NodeType == XPathNodeType.Attribute
? "/@"
: "/";
string path = string.Concat(firstNodeEscape, $"{navigator.Name}{GetXmlArrayIndex(navigator)}");
while (navigator.MoveToParent())
{
path = string.Concat($"/{navigator.Name}{GetXmlArrayIndex(navigator)}", path);
}
return path;
}
//returns index of element in array, used inside GetFullXpath method to add node position in array to path
private string GetXmlArrayIndex(XPathNavigator navigator)
{
var backwardsNavigator = navigator.Clone();
var elementsBeforeCount = 0;
while (backwardsNavigator.MoveToPrevious()) //true while has siblings
{
if (backwardsNavigator.NodeType != XPathNodeType.Element) continue;
if (backwardsNavigator.Name == navigator.Name) elementsBeforeCount++;
else break;
}
return elementsBeforeCount > 0
? $"[{++elementsBeforeCount}]" //++ because xpath array indexes are 1-based
: string.Empty;
}