Как получить xpath из экземпляра XmlNode - PullRequest
48 голосов
/ 27 октября 2008

Может ли кто-нибудь предоставить некоторый код, который получит xpath экземпляра System.Xml.XmlNode?

Спасибо!

Ответы [ 14 ]

53 голосов
/ 27 октября 2008

Хорошо, я не мог устоять перед этим. Это будет работать только для атрибутов и элементов, но эй ... что вы можете ожидать через 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");
    }
}
23 голосов
/ 28 октября 2008

Джон прав, что существует любое количество выражений 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.");
}

(Вероятно, есть более элегантный способ сделать это с помощью LINQ, поскольку XmlNodeList реализует IEnumerable, но я пойду с тем, что знаю здесь.)

Тогда вы можете написать рекурсивный метод, подобный этому:

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)
        );
}

Как видите, я взломал его, чтобы он тоже нашел атрибуты.

Джон вошел в его версию, когда я писал свою. Что-то в его коде заставит меня немного разглагольствовать, и я заранее извиняюсь, если это звучит так, как будто я обижаю Джона. (Я не уверен. Я почти уверен, что список того, чему Джон должен научиться у меня, чрезвычайно короток.) ​​Но я думаю, что мысль, которую я собираюсь сделать, является довольно важной для тех, кто работает с XML для думать о.

Я подозреваю, что решение Джона возникло из того, что, как я вижу, делают многие разработчики: думать о XML-документах как о деревьях элементов и атрибутов. Я думаю, что это в значительной степени исходит от разработчиков, чье основное использование XML заключается в качестве формата сериализации, потому что весь используемый ими XML структурирован таким образом. Вы можете заметить этих разработчиков, потому что они используют термины «узел» и «элемент» взаимозаменяемо. Это заставляет их предлагать решения, которые рассматривают все другие типы узлов как особые случаи. (Я был одним из этих парней сам в течение очень долгого времени.)

Такое ощущение, что это упрощающее предположение, пока вы делаете это. Но это не так. Это усложняет задачи и усложняет код. Это побуждает вас обходить части технологии XML (например, функцию node() в XPath), которые специально разработаны для общей обработки всех типов узлов.

В коде Джона есть красный флаг, который заставил бы меня запросить его в обзоре кода, даже если я не знал, каковы требования, и это GetElementsByTagName. Всякий раз, когда я вижу, что этот метод используется, возникает вопрос: «Почему он должен быть элементом?». И ответ очень часто «о, этот код должен обрабатывать текстовые узлы тоже?»

6 голосов
/ 12 августа 2013

Я знаю, старый пост, но версия, которая мне понравилась больше всего (та, что с именами), была ошибочной: Когда родительский узел имеет узлы с разными именами, он прекращает подсчет индекса после того, как обнаружил первое несоответствующее имя узла.

Вот моя исправленная версия:

/// <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);
}
3 голосов
/ 09 августа 2012

Вот простой метод, который я использовал, работал для меня.

    static string GetXpath(XmlNode node)
    {
        if (node.Name == "#document")
            return String.Empty;
        return GetXpath(node.SelectSingleNode("..")) + "/" +  (node.NodeType == XmlNodeType.Attribute ? "@":String.Empty) + node.Name;
    }
3 голосов
/ 18 декабря 2009

Моя 10p стоит гибрид ответов Роберта и Кори. Я могу требовать кредит только для фактического набора дополнительных строк кода.

    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
            );
    }
2 голосов
/ 27 октября 2008

Нет такой вещи как "путь" узла. Для любого данного узла вполне может быть много выражений xpath, которые будут ему соответствовать.

Возможно, вы можете обработать дерево, чтобы создать выражение , которое будет ему соответствовать, принимая во внимание индекс определенных элементов и т. Д., Но это не будет ужасно хорошим кодом.

Зачем тебе это? Там может быть лучшее решение.

1 голос
/ 18 мая 2016

Другим решением вашей проблемы может быть «пометка» xmlnodes, которые вы позже захотите идентифицировать с помощью пользовательского атрибута:

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 (или индексы, как предлагается в других ответах).

1 голос
/ 15 ноября 2014

Я создал VBA для 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
1 голос
/ 27 июня 2014

Как насчет использования расширения класса? ;) Моя версия (основанная на других работах) использует синтаксис 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 "";
}   
1 голос
/ 27 сентября 2011

Я обнаружил, что ничего из вышеперечисленного не работает с 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
...