Может ли кто-нибудь предоставить код, который получит xpath экземпляра System.Xml.XmlNode?
Спасибо!
Может ли кто-нибудь предоставить код, который получит xpath экземпляра System.Xml.XmlNode?
Спасибо!
Хорошо, я не мог удержаться от этого. Это будет работать только на атрибуты и элементы, но эй... что вы можете ожидать через 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");
}
}
Поправьте, что существует любое количество выражений 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 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 для подумать о.
Я подозреваю, что решение Jon возникло из того, что, как я вижу, делают многие разработчики: рассматривая XML-документы как деревья элементов и атрибутов. Я думаю, что это в значительной степени исходит от разработчиков, чье основное использование XML является форматом сериализации, потому что весь используемый ими XML структурирован таким образом. Вы можете заметить этих разработчиков, потому что они используют термины "узел" и "элемент" взаимозаменяемо. Это заставляет их предлагать решения, которые рассматривают все другие типы узлов как особые случаи. (Я был одним из этих парней сам в течение очень долгого времени.)
Это похоже на упрощающее предположение, пока вы делаете это. Но это не так. Это усложняет задачи и усложняет код. Это заставляет вас обойти части технологии XML (например, функцию node()
в XPath), которые специально предназначены для общей обработки всех типов узлов.
В коде Jon есть красный флаг, который заставляет меня запрашивать его в обзоре кода, даже если я не знаю, каковы требования, и что GetElementsByTagName
. Всякий раз, когда я вижу, что этот метод используется, возникает вопрос: "Почему он должен быть элементом?". И ответ очень часто "о, этот код должен обрабатывать текстовые узлы тоже?"
Я знаю, старый пост, но версия, которая мне больше всего понравилась (с именами), была ошибочной: Когда родительский node имеет узлы с разными именами, он перестает считать индекс после того, как он нашел первое несоответствующее node -name.
Вот моя фиксированная версия:
/// <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);
}
Мой 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
);
}
Вот простой способ, который я использовал, работал у меня.
static string GetXpath(XmlNode node)
{
if (node.Name == "#document")
return String.Empty;
return GetXpath(node.SelectSingleNode("..")) + "/" + (node.NodeType == XmlNodeType.Attribute ? "@":String.Empty) + node.Name;
}
Нет такой вещи, как "the" xpath для node. Для любого заданного node может быть много выражений xpath, которые будут соответствовать ему.
Возможно, вы можете обработать дерево, чтобы создать выражение, которое будет соответствовать ему, принимая во внимание индекс отдельных элементов и т.д., но это не будет ужасно приятным кодом.
Зачем вам это нужно? Может быть лучшее решение.
Если вы сделаете это, вы получите Путь с именами узлов и позиции, если у вас есть узлы с таким же именем: "/Услуги [1]/System [1]/Группа [1]/Папка [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);
}
Я обнаружил, что ни один из вышеперечисленных действий не работал с 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
Как насчет использования расширения класса?;) Моя версия (построение на других работает) использует имя синтаксиса [index]... с индексом omited элемент не имеет "братьев". Цикл для получения индекса элемента является внешним в независимой процедуре (также расширении класса).
Просто пропустите следующее в любом классе утилиты (или в главном классе программы)
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 "";
}
Я создал VBA для Excel, чтобы сделать это для рабочего проекта. Он выводит кортежи Xpath и связанный с ними текст из элемента или атрибута. Цель состояла в том, чтобы позволить бизнес-аналитикам идентифицировать и сопоставить некоторые xml. Цените, что это форум С#, но подумал, что это может представлять интерес.
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 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
Это еще проще
''' <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 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 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
Другим решением вашей проблемы может быть "отметить" xmlnodes, которые вы хотите позже идентифицировать с помощью настраиваемого атрибута:
var id = _currentNode.OwnerDocument.CreateAttribute("some_id");
id.Value = Guid.NewGuid().ToString();
_currentNode.Attributes.Append(id);
который вы можете хранить в словаре, например. И вы можете позже идентифицировать node с запросом xpath:
newOrOldDocument.SelectSingleNode(string.Format("//*[contains(@some_id,'{0}')]", id));
Я знаю, что это не прямой ответ на ваш вопрос, но он может помочь, если причина, по которой вы хотите знать xpath для node, - это способ "достичь" node позже после того, как вы потерял ссылку на него в коде.
Это также устраняет проблемы, когда документ получает элементы, добавленные/перемещенные, что может испортить xpath (или индексы, как предложено в других ответах).
public static string GetFullPath(this XmlNode node)
{
if (node.ParentNode == null)
{
return "";
}
else
{
return $"{GetFullPath(node.ParentNode)}\\{node.ParentNode.Name}";
}
}
Я должен был сделать это недавно. Только элементы должны быть рассмотрены. Вот что я придумал:
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);
}