Jon 说得对,有任意数量的 XPath 表达式会在实例文档中产生相同的节点。构建明确产生特定节点的表达式的最简单方法是使用谓词中的节点位置的节点测试链,例如:
/node()[0]/node()[2]/node()[6]/node()[1]/node()[2]
显然,这个表达式没有使用元素名称,但是如果您只想在文档中定位一个节点,则不需要它的名称。它也不能用于查找属性(因为属性不是节点,也没有位置;您只能通过名称找到它们),但它会查找所有其他节点类型。
要构建此表达式,您需要编写一个方法来返回节点在其父节点的子节点中的位置,因为XmlNode 不会将其作为属性公开:
static int GetNodePosition(XmlNode child)
{
for (int i=0; i<child.ParentNode.ChildNodes.Count; i++)
{
if (child.ParentNode.ChildNodes[i] == child)
{
// tricksy XPath, not starting its positions at 0 like a normal language
return i + 1;
}
}
throw new InvalidOperationException("Child node somehow not found in its parent's ChildNodes property.");
}
(由于XmlNodeList 实现了IEnumerable,因此使用 LINQ 可能有一种更优雅的方法,但我在这里使用我所知道的。)
然后你可以写一个这样的递归方法:
static string GetXPathToNode(XmlNode node)
{
if (node.NodeType == XmlNodeType.Attribute)
{
// attributes have an OwnerElement, not a ParentNode; also they have
// to be matched by name, not found by position
return String.Format(
"{0}/@{1}",
GetXPathToNode(((XmlAttribute)node).OwnerElement),
node.Name
);
}
if (node.ParentNode == null)
{
// the only node with no parent is the root node, which has no path
return "";
}
// the path to a node is the path to its parent, plus "/node()[n]", where
// n is its position among its siblings.
return String.Format(
"{0}/node()[{1}]",
GetXPathToNode(node.ParentNode),
GetNodePosition(node)
);
}
如你所见,我用一种方法让它也能找到属性。
乔恩在我写我的版本时偷偷加入了他的版本。他的代码有些地方现在会让我有点咆哮,如果这听起来像是我对乔恩的抨击,我提前道歉。 (我不是。我很确定 Jon 必须向我学习的东西非常短。)但我认为我要说明的一点对于任何使用 XML 的人来说都是非常重要的。想一想。
我怀疑 Jon 的解决方案源于我看到许多开发人员所做的事情:将 XML 文档视为元素和属性的树。我认为这主要来自开发人员,他们主要使用 XML 作为序列化格式,因为他们习惯使用的所有 XML 都是以这种方式构建的。您可以发现这些开发人员,因为他们交替使用术语“节点”和“元素”。这导致他们提出将所有其他节点类型视为特殊情况的解决方案。 (很长一段时间以来,我自己都是这些人中的一员。)
这感觉就像你在做它时的一个简化假设。但事实并非如此。它使问题更难,代码更复杂。它会引导您绕过专门设计用于通用处理所有节点类型的 XML 技术(如 XPath 中的 node() 函数)。
Jon 的代码中有一个危险信号,即使我不知道要求是什么,我也会在代码审查中查询它,那就是 GetElementsByTagName。每当我看到使用该方法时,脑海中浮现的问题总是“为什么它必须是一个元素?”答案通常是“哦,这段代码也需要处理文本节点吗?”