Cómo obtener xpath de una instancia de XmlNode

¿Podría alguien proporcionar algún código que obtenga el xpath de una instancia de System.Xml.XmlNode?

¡Gracias!

De acuerdo, no pude resistirme a intentarlo. Solo funcionará para los atributos y elementos, pero bueno … ¿qué se puede esperar en 15 minutos? De la misma manera, puede haber una forma más limpia de hacerlo.

Es superfluo incluir el índice en cada elemento (¡especialmente en el de raíz!), Pero es más fácil que tratar de determinar si existe alguna ambigüedad en caso contrario.

using System; using System.Text; using System.Xml; class Test { static void Main() { string xml = @"        "; 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"); } } 

Jon está en lo cierto al decir que hay varias expresiones XPath que producirán el mismo nodo en un documento de instancia. La forma más sencilla de construir una expresión que ceda inequívocamente un nodo específico es una cadena de pruebas de nodo que utiliza la posición del nodo en el predicado, por ejemplo:

 /node()[0]/node()[2]/node()[6]/node()[1]/node()[2] 

Obviamente, esta expresión no usa nombres de elementos, pero si lo único que intenta hacer es ubicar un nodo dentro de un documento, no necesita su nombre. Tampoco se puede usar para buscar atributos (porque los atributos no son nodos y no tienen posición, solo se pueden encontrar por nombre), pero encontrará todos los demás tipos de nodos.

Para crear esta expresión, debe escribir un método que devuelva la posición de un nodo en los nodos secundarios de sus padres, porque XmlNode no lo expone como una propiedad:

 static int GetNodePosition(XmlNode child) { for (int i=0; i 

(Probablemente haya una manera más elegante de hacerlo con LINQ, ya que XmlNodeList implementa IEnumerable , pero voy con lo que sé aquí).

Entonces puedes escribir un método recursivo como este:

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

Como pueden ver, he pirateado el camino para que también encuentre atributos.

Jon resbaló en su versión mientras yo escribía la mía. Hay algo en su código que me va a hacer despotricar un poco ahora, y me disculpo de antemano si suena como si estuviera hablando mal de Jon. (No estoy. Estoy bastante seguro de que la lista de cosas que Jon tiene que aprender de mí es extremadamente corta.) Pero creo que el punto que voy a presentar es bastante importante para cualquiera que trabaje con XML para pensar en.

Sospecho que la solución de Jon surgió de algo que veo muchos desarrolladores: pensar en documentos XML como árboles de elementos y atributos. Creo que esto proviene en gran medida de los desarrolladores cuyo uso principal de XML es como un formato de serialización, porque todo el XML que están acostumbrados a usar está estructurado de esta manera. Puede detectar estos desarrolladores porque están usando los términos "nodo" y "elemento" indistintamente. Esto les lleva a encontrar soluciones que tratan a todos los demás tipos de nodos como casos especiales. (Yo fui uno de estos muchachos por mucho tiempo).

Parece que es una suposición simplificadora mientras lo haces. Pero no lo es. Hace que los problemas sean más difíciles y el código más complejo. Le lleva a omitir las piezas de tecnología XML (como la función de node() en XPath) que están diseñadas específicamente para tratar genéricamente todos los tipos de nodos.

Hay una bandera roja en el código de Jon que me haría consultarlo en una revisión del código, incluso si no sabía cuáles son los requisitos, y eso es GetElementsByTagName . Cada vez que veo ese método en uso, la pregunta que me viene a la mente es siempre "¿por qué tiene que ser un elemento?" Y la respuesta es muy a menudo "oh, ¿este código también necesita manejar nodos de texto?"

Lo sé, publicación anterior, pero la versión que más me gustaba (la que tenía nombres) era defectuosa: cuando un nodo padre tiene nodos con nombres diferentes, dejó de contar el índice después de encontrar el primer nombre de nodo no coincidente.

Aquí está mi versión fija de esto:

 ///  /// Gets the X-Path to a given Node ///  /// The Node to get the X-Path from /// The X-Path of the Node 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); } 

Mi valor de 10p es un híbrido de las respuestas de Robert y Corey. Solo puedo reclamar el crédito por el tipeo real de las líneas adicionales de código.

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

Aquí hay un método simple que he usado, funcionó para mí.

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

No existe tal cosa como “el” xpath de un nodo. Para cualquier nodo dado, bien puede haber muchas expresiones xpath que lo emparejarán.

Probablemente pueda trabajar en el árbol para construir una expresión que coincida, teniendo en cuenta el índice de elementos particulares, etc., pero no va a ser un código terriblemente agradable.

¿Por qué necesitas esto? Puede haber una mejor solución.

Si hace esto, obtendrá una ruta de acceso con los nombres de los nodos Y la posición, si tiene nodos con el mismo nombre como este: “/ Servicio [1] / Sistema [1] / Grupo [1] / Carpeta [2] ] / Archivo [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); } 

Descubrí que ninguno de los anteriores funcionaba con XDocument , así que escribí mi propio código para soportar XDocument y usé recursividad. Creo que este código maneja múltiples nodos idénticos mejor que algunos de los otros códigos aquí porque primero trata de ir tan profundo en la ruta XML como pueda y luego hace una copia de seguridad para construir solo lo que se necesita. Entonces, si tiene /home/white/bob y /home/white/mike y quiere crear /home/white/bob/garage el código sabrá cómo crear eso. Sin embargo, no quería meterme con predicados o comodines, así que explícitamente los desactivé; pero sería fácil agregar soporte para ellos.

 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 

¿Qué hay de usar la extensión de clase? 😉 Mi versión (basada en el trabajo de otros) usa el nombre de la syntax [índice] … con el índice omitido, el elemento no tiene “hermanos”. El ciclo para obtener el índice del elemento está afuera en una rutina independiente (también una extensión de clase).

Justo después de lo siguiente en cualquier clase de utilidad (o en la clase principal de Progtwig)

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

Produje VBA para Excel para hacer esto para un proyecto de trabajo. Emite tuplas de un Xpath y el texto asociado de un elemento o atributo. El objective era permitir a los analistas de negocios identificar y mapear algunos xml. Apreciar que este es un foro de C #, pero pensé que esto podría ser de interés.

 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], eg 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 

Administra el conteo de elementos usando:

 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 

Esto es aún más fácil

  '''  ''' Gets the full XPath of a single node. '''  '''  '''  '''  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 

Otra solución a su problema podría ser ‘marcar’ los xmlnodes que luego querrá identificar con un atributo personalizado:

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

que puedes almacenar en un diccionario, por ejemplo. Y luego puede identificar el nodo con una consulta xpath:

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

Sé que esta no es una respuesta directa a su pregunta, pero puede ser útil si la razón por la que desea conocer el xpath de un nodo es tener una forma de ‘llegar’ al nodo más tarde después de haber perdido la referencia al mismo en el código .

Esto también supera los problemas cuando el documento obtiene elementos agregados / movidos, lo que puede arruinar el xpath (o índices, como se sugiere en otras respuestas).

  public static string GetFullPath(this XmlNode node) { if (node.ParentNode == null) { return ""; } else { return $"{GetFullPath(node.ParentNode)}\\{node.ParentNode.Name}"; } } 

Tuve que hacer esto recientemente. Solo los elementos deben ser considerados. Esto es lo que se me ocurrió:

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