如何从 XmlNode 实例中获取 xpath

Posted

技术标签:

【中文标题】如何从 XmlNode 实例中获取 xpath【英文标题】:How to get xpath from an XmlNode instance 【发布时间】:2010-09-19 11:12:02 【问题描述】:

有人可以提供一些代码来获取 System.Xml.XmlNode 实例的 xpath 吗?

谢谢!

【问题讨论】:

澄清一下,你的意思是从根到节点的列表节点名称,用/分隔? 完全正确。就像...“root/mycars/toyota/description/paragraph” 描述元素中可能有多个段落。但我只希望 xpath 指向 XmlNode 实例所指的那个。 人们不应该只是“索要代码”——他们应该提供一些他们至少尝试过的代码。 【参考方案1】:

好吧,我忍不住想试一试。它只适用于属性和元素,但是嘿......你能在 15 分钟内期待什么 :) 同样可能有一种更清洁的方法。

在每个元素(尤其是根元素!)上都包含索引是多余的,但它比尝试找出是否存在歧义要容易。

using System;
using System.Text;
using System.Xml;

class Test

    static void Main()
    
        string xml = @"
<root>
  <foo />
  <foo>
     <bar attr='value'/>
     <bar other='va' />
  </foo>
  <foo><bar /></foo>
</root>";
        XmlDocument doc = new XmlDocument();
        doc.LoadXml(xml);
        XmlNode node = doc.SelectSingleNode("//@attr");
        Console.WriteLine(FindXPath(node));
        Console.WriteLine(doc.SelectSingleNode(FindXPath(node)) == node);
    

    static string FindXPath(XmlNode node)
    
        StringBuilder builder = new StringBuilder();
        while (node != null)
        
            switch (node.NodeType)
            
                case XmlNodeType.Attribute:
                    builder.Insert(0, "/@" + node.Name);
                    node = ((XmlAttribute) node).OwnerElement;
                    break;
                case XmlNodeType.Element:
                    int index = FindElementIndex((XmlElement) node);
                    builder.Insert(0, "/" + node.Name + "[" + index + "]");
                    node = node.ParentNode;
                    break;
                case XmlNodeType.Document:
                    return builder.ToString();
                default:
                    throw new ArgumentException("Only elements and attributes are supported");
            
        
        throw new ArgumentException("Node was not in a document");
    

    static int FindElementIndex(XmlElement element)
    
        XmlNode parentNode = element.ParentNode;
        if (parentNode is XmlDocument)
        
            return 1;
        
        XmlElement parent = (XmlElement) parentNode;
        int index = 1;
        foreach (XmlNode candidate in parent.ChildNodes)
        
            if (candidate is XmlElement && candidate.Name == element.Name)
            
                if (candidate == element)
                
                    return index;
                
                index++;
            
        
        throw new ArgumentException("Couldn't find element within parent");
    

【讨论】:

乔恩,谢谢,我最近用过这个。当一个元素前面有一个相同类型的“侄子”时,FindElementIndex 中存在一个错误。我会稍作修改来解决这个问题。 非常感谢乔恩!今天这救了我的命!我有一个源 xml/xsd 树(复选框树,因此用户可以删除节点),我将用户的选择保存在逗号分隔的 xpath 字符串中,以便稍后过滤用户的 xml 提要,以便他们只获得他们需要的节点子集。这对我有用。再次感谢。【参考方案2】:

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。每当我看到使用该方法时,脑海中浮现的问题总是“为什么它必须是一个元素?”答案通常是“哦,这段代码也需要处理文本节点吗?”

【讨论】:

【参考方案3】:

我知道,旧帖子,但我最喜欢的版本(有名字的版本)有缺陷: 当父节点有不同名称的节点时,它会在找到第一个不匹配的节点名称后停止计算索引。

这是我的固定版本:

/// <summary>
/// Gets the X-Path to a given Node
/// </summary>
/// <param name="node">The Node to get the X-Path from</param>
/// <returns>The X-Path of the Node</returns>
public 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 "";
    

    // Get the Index
    int indexInParent = 1;
    XmlNode siblingNode = node.PreviousSibling;
    // Loop thru all Siblings
    while (siblingNode != null)
    
        // Increase the Index if the Sibling has the same Name
        if (siblingNode.Name == node.Name)
        
            indexInParent++;
        
        siblingNode = siblingNode.PreviousSibling;
    

    // 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/1[2]", GetXPathToNode(node.ParentNode), node.Name, indexInParent);

【讨论】:

【参考方案4】:

这是我用过的一个简单方法,对我有用。

    static string GetXpath(XmlNode node)
    
        if (node.Name == "#document")
            return String.Empty;
        return GetXpath(node.SelectSingleNode("..")) + "/" +  (node.NodeType == XmlNodeType.Attribute ? "@":String.Empty) + node.Name;
    

【讨论】:

【参考方案5】:

我的 10 便士价值是罗伯特和科里的答案的混合体。我只能为实际输入的额外代码行声明功劳。

    private 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 "";
        
        //get the index
        int iIndex = 1;
        XmlNode xnIndex = node;
        while (xnIndex.PreviousSibling != null)  iIndex++; xnIndex = xnIndex.PreviousSibling; 
        // 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),
            iIndex
            );
    

【讨论】:

【参考方案6】:

没有节点的“xpath”这样的东西。对于任何给定的节点,很可能有很多 xpath 表达式可以匹配它。

您可能可以对树进行处理以构建将匹配它的 表达式,同时考虑特定元素的索引等,但它不会是非常好的代码。

为什么需要这个?可能有更好的解决方案。

【讨论】:

我正在向 XML 编辑应用程序调用 API。我需要告诉应用程序隐藏某些节点,我通过调用采用 xpath 的 ToggleVisibleElement 来做到这一点。我希望有一个简单的方法来做到这一点。 @Jon Skeet:查看我对类似问题的回答:***.com/questions/451950/… 我的解决方案会生成一个 XPath 表达式,该表达式选择可能是任何类型的节点:根、元素、属性、文本、注释、 PI 或命名空间。 验证 XML 文档的内容是一个很好的理由,当您报告的语义错误超出了通过验证模式可以确定的错误时。 XPath 是人类可读的,但也可以提供给一个自动化系统,该系统使用它来突出显示有问题的节点并将其呈现给可以更正文档的人。【参考方案7】:

如果你这样做,你将得到一个带有节点名称和位置的路径,如果你有同名的节点,如下所示: "/Service[1]/System[1]/Group[1]/Folder[2]/File[2]"

public 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 "";
    

    //get the index
    int iIndex = 1;
    XmlNode xnIndex = node;
    while (xnIndex.PreviousSibling != null && xnIndex.PreviousSibling.Name == xnIndex.Name)
    
         iIndex++;
         xnIndex = xnIndex.PreviousSibling; 
    

    // 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/1[2]", GetXPathToNode(node.ParentNode), node.Name, iIndex);

【讨论】:

【参考方案8】:

我发现上述方法都不适用于XDocument,所以我编写了自己的代码来支持XDocument,并使用了递归。我认为这段代码比这里的一些其他代码更好地处理多个相同的节点,因为它首先尝试尽可能深入地进入 XML 路径,然后备份以仅构建所需的内容。所以如果你有/home/white/bob/home/white/mike 并且你想创建/home/white/bob/garage 代码将知道如何创建它。但是,我不想弄乱谓词或通配符,所以我明确禁止使用它们;但是添加对它们的支持很容易。

Private Sub NodeItterate(XDoc As XElement, XPath As String)
    'get the deepest path
    Dim nodes As IEnumerable(Of XElement)

    nodes = XDoc.XPathSelectElements(XPath)

    'if it doesn't exist, try the next shallow path
    If nodes.Count = 0 Then
        NodeItterate(XDoc, XPath.Substring(0, XPath.LastIndexOf("/")))
        'by this time all the required parent elements will have been constructed
        Dim ParentPath As String = XPath.Substring(0, XPath.LastIndexOf("/"))
        Dim ParentNode As XElement = XDoc.XPathSelectElement(ParentPath)
        Dim NewElementName As String = XPath.Substring(XPath.LastIndexOf("/") + 1, XPath.Length - XPath.LastIndexOf("/") - 1)
        ParentNode.Add(New XElement(NewElementName))
    End If

    'if we find there are more than 1 elements at the deepest path we have access to, we can't proceed
    If nodes.Count > 1 Then
        Throw New ArgumentOutOfRangeException("There are too many paths that match your expression.")
    End If

    'if there is just one element, we can proceed
    If nodes.Count = 1 Then
        'just proceed
    End If

End Sub

Public Sub CreateXPath(ByVal XDoc As XElement, ByVal XPath As String)

    If XPath.Contains("//") Or XPath.Contains("*") Or XPath.Contains(".") Then
        Throw New ArgumentException("Can't create a path based on searches, wildcards, or relative paths.")
    End If

    If Regex.IsMatch(XPath, "\[\]()@='<>\|") Then
        Throw New ArgumentException("Can't create a path based on predicates.")
    End If

    'we will process this recursively.
    NodeItterate(XDoc, XPath)

End Sub

【讨论】:

【参考方案9】:

使用类扩展怎么样? ;) 我的版本(基于其他工作)使用语法 name[index]... 省略了索引是元素没有“兄弟”。 获取元素索引的循环在一个独立的例程之外(也是一个类扩展)。

任何实用程序类(或主程序类)中的以下内容

static public int GetRank( this XmlNode node )

    // return 0 if unique, else return position 1...n in siblings with same name
    try
    
        if( node is XmlElement ) 
        
            int rank = 1;
            bool alone = true, found = false;

            foreach( XmlNode n in node.ParentNode.ChildNodes )
                if( n.Name == node.Name ) // sibling with same name
                
                    if( n.Equals(node) )
                    
                        if( ! alone ) return rank; // no need to continue
                        found = true;
                    
                    else
                    
                        if( found ) return rank; // no need to continue
                        alone = false;
                        rank++;
                    
                

        
    
    catch
    return 0;


static public string GetXPath( this XmlNode node )

    try
    
        if( node is XmlAttribute )
            return String.Format( "0/@1", (node as XmlAttribute).OwnerElement.GetXPath(), node.Name );

        if( node is XmlText || node is XmlCDataSection )
            return node.ParentNode.GetXPath();

        if( node.ParentNode == null )   // the only node with no parent is the root node, which has no path
            return "";

        int rank = node.GetRank();
        if( rank == 0 ) return String.Format( "0/1",        node.ParentNode.GetXPath(), node.Name );
        else            return String.Format( "0/1[2]",   node.ParentNode.GetXPath(), node.Name, rank );
    
    catch
    return "";
   

【讨论】:

【参考方案10】:

我为一个工作项目制作了 VBA for Excel。它输出 Xpath 的元组和来自元素或属性的关联文本。目的是让业务分析师识别和映射一些 xml。感谢这是一个 C# 论坛,但认为这可能很有趣。

Sub Parse2(oSh As Long, inode As IXMLDOMNode, Optional iXstring As String = "", Optional indexes)


Dim chnode As IXMLDOMNode
Dim attr As IXMLDOMAttribute
Dim oXString As String
Dim chld As Long
Dim idx As Variant
Dim addindex As Boolean
chld = 0
idx = 0
addindex = False


'determine the node type:
Select Case inode.NodeType

    Case NODE_ELEMENT
        If inode.ParentNode.NodeType = NODE_DOCUMENT Then 'This gets the root node name but ignores all the namespace attributes
            oXString = iXstring & "//" & fp(inode.nodename)
        Else

            'Need to deal with indexing. Where an element has siblings with the same nodeName,it needs to be indexed using [index], e.g swapstreams or schedules

            For Each chnode In inode.ParentNode.ChildNodes
                If chnode.NodeType = NODE_ELEMENT And chnode.nodename = inode.nodename Then chld = chld + 1
            Next chnode

            If chld > 1 Then '//inode has siblings of the same nodeName, so needs to be indexed
                'Lookup the index from the indexes array
                idx = getIndex(inode.nodename, indexes)
                addindex = True
            Else
            End If

            'build the XString
            oXString = iXstring & "/" & fp(inode.nodename)
            If addindex Then oXString = oXString & "[" & idx & "]"

            'If type is element then check for attributes
            For Each attr In inode.Attributes
                'If the element has attributes then extract the data pair XString + Element.Name, @Attribute.Name=Attribute.Value
                Call oSheet(oSh, oXString & "/@" & attr.Name, attr.Value)
            Next attr

        End If

    Case NODE_TEXT
        'build the XString
        oXString = iXstring
        Call oSheet(oSh, oXString, inode.NodeValue)

    Case NODE_ATTRIBUTE
    'Do nothing
    Case NODE_CDATA_SECTION
    'Do nothing
    Case NODE_COMMENT
    'Do nothing
    Case NODE_DOCUMENT
    'Do nothing
    Case NODE_DOCUMENT_FRAGMENT
    'Do nothing
    Case NODE_DOCUMENT_TYPE
    'Do nothing
    Case NODE_ENTITY
    'Do nothing
    Case NODE_ENTITY_REFERENCE
    'Do nothing
    Case NODE_INVALID
    'do nothing
    Case NODE_NOTATION
    'do nothing
    Case NODE_PROCESSING_INSTRUCTION
    'do nothing
End Select

'Now call Parser2 on each of inode's children.
If inode.HasChildNodes Then
    For Each chnode In inode.ChildNodes
        Call Parse2(oSh, chnode, oXString, indexes)
    Next chnode
Set chnode = Nothing
Else
End If

End Sub

使用以下方法管理元素的计数:

Function getIndex(tag As Variant, indexes) As Variant
'Function to get the latest index for an xml tag from the indexes array
'indexes array is passed from one parser function to the next up and down the tree

Dim i As Integer
Dim n As Integer

If IsArrayEmpty(indexes) Then
    ReDim indexes(1, 0)
    indexes(0, 0) = "Tag"
    indexes(1, 0) = "Index"
Else
End If
For i = 0 To UBound(indexes, 2)
    If indexes(0, i) = tag Then
        'tag found, increment and return the index then exit
        'also destroy all recorded tag names BELOW that level
        indexes(1, i) = indexes(1, i) + 1
        getIndex = indexes(1, i)
        ReDim Preserve indexes(1, i) 'should keep all tags up to i but remove all below it
        Exit Function
    Else
    End If
Next i

'tag not found so add the tag with index 1 at the end of the array
n = UBound(indexes, 2)
ReDim Preserve indexes(1, n + 1)
indexes(0, n + 1) = tag
indexes(1, n + 1) = 1
getIndex = 1

End Function

【讨论】:

【参考方案11】:

解决您的问题的另一种方法可能是“标记”您以后要使用自定义属性标识的 xmlnode:

var id = _currentNode.OwnerDocument.CreateAttribute("some_id");
id.Value = Guid.NewGuid().ToString();
_currentNode.Attributes.Append(id);

例如,您可以将其存储在字典中。 稍后您可以使用 xpath 查询识别节点:

newOrOldDocument.SelectSingleNode(string.Format("//*[contains(@some_id,'0')]", id));

我知道这不是您问题的直接答案,但如果您希望了解节点的 xpath 的原因是在您丢失对它在代码中。

这也解决了文档添加/移动元素时的问题,这可能会弄乱 xpath(或索引,如其他答案中所建议的)。

【讨论】:

【参考方案12】:

这更容易

 ''' <summary>
    ''' Gets the full XPath of a single node.
    ''' </summary>
    ''' <param name="node"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function GetXPath(ByVal node As Xml.XmlNode) As String
        Dim temp As String
        Dim sibling As Xml.XmlNode
        Dim previousSiblings As Integer = 1

        'I dont want to know that it was a generic document
        If node.Name = "#document" Then Return ""

        'Prime it
        sibling = node.PreviousSibling
        'Perculate up getting the count of all of this node's sibling before it.
        While sibling IsNot Nothing
            'Only count if the sibling has the same name as this node
            If sibling.Name = node.Name Then
                previousSiblings += 1
            End If
            sibling = sibling.PreviousSibling
        End While

        'Mark this node's index, if it has one
        ' Also mark the index to 1 or the default if it does have a sibling just no previous.
        temp = node.Name + IIf(previousSiblings > 0 OrElse node.NextSibling IsNot Nothing, "[" + previousSiblings.ToString() + "]", "").ToString()

        If node.ParentNode IsNot Nothing Then
            Return GetXPath(node.ParentNode) + "/" + temp
        End If

        Return temp
    End Function

【讨论】:

【参考方案13】:

我最近不得不这样做。只有元素需要考虑。这是我想出的:

    private string GetPath(XmlElement el)
    
        List<string> pathList = new List<string>();
        XmlNode node = el;
        while (node is XmlElement)
        
            pathList.Add(node.Name);
            node = node.ParentNode;
        
        pathList.Reverse();
        string[] nodeNames = pathList.ToArray();
        return String.Join("/", nodeNames);
    

【讨论】:

【参考方案14】:
 public static string GetFullPath(this XmlNode node)
        
            if (node.ParentNode == null)
            
                return "";
            
            else
            
                return $"GetFullPath(node.ParentNode)\\node.ParentNode.Name";
            
        

【讨论】:

什么是索引

以上是关于如何从 XmlNode 实例中获取 xpath的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Windows Phone 中检查 XMLNode 是不是存在

使用 System.Xml.XmlDocument 时是不是可以从 System.Xml.XmlNode 获取行号/位置?

获取 XmlNode 的特定属性

如何将 XmlNode 从一个 XmlDocument 复制到另一个?

如何在 C# 中从 XmlNode 读取属性值?

如何检查多个 XMLNode 属性的空值?