Jak uzyskać XPath z instancji XmlNode
Czy ktoś mógłby podać jakiś kod, który dostałby xpath systemu?Xml.Instancja XmlNode?
Dzięki!
14 answers
Dobra, nie mogłem się oprzeć. Będzie działać tylko dla atrybutów i elementów, ale hej... czego można się spodziewać w 15 minut :) podobnie może być czystszy sposób.
Zbyteczne jest umieszczanie indeksu na każdym elemencie (szczególnie na głównym!) ale to łatwiejsze niż próba ustalenia, czy jest jakaś dwuznaczność inaczej.
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");
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-06-04 18:51:19
Jon zgadza się, że istnieje wiele wyrażeń XPath, które dadzą ten sam węzeł w dokumencie instancji an. Najprostszym sposobem na zbudowanie wyrażenia, które jednoznacznie daje konkretny węzeł, jest łańcuch testów węzłów wykorzystujących pozycję węzła w predykacie, np.:]}
/node()[0]/node()[2]/node()[6]/node()[1]/node()[2]
Oczywiście to wyrażenie nie używa nazw elementów, ale jeśli próbujesz zlokalizować węzeł w dokumencie, nie potrzebujesz jego nazwy. Nie można go również użyć do znajdowania atrybutów (ponieważ atrybuty nie są węzłami i nie mają pozycji; można je znaleźć tylko po nazwie), ale znajdzie wszystkie inne typy węzłów.
Aby zbudować to wyrażenie, musisz napisać metodę, która zwróci pozycję węzła w jego węzłach potomnych, ponieważ XmlNode
nie ujawnia tego jako właściwości:
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.");
}
(prawdopodobnie jest bardziej elegancki sposób, aby to zrobić za pomocą LINQ, ponieważ XmlNodeList
implementuje IEnumerable
, ale idę z tym, co wiem tutaj.)
Wtedy można napisać metodę rekurencyjną tak:
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)
);
}
Jak widzisz, włamałem się w sposób, aby znaleźć atrybuty, jak również.
Jon wślizgnął się, w którym jego Wersja, gdy pisałem moją. Jest coś w jego kodzie, co sprawi, że będę teraz trochę narzekać, i z góry przepraszam, jeśli brzmi to tak, jakbym nabijał się na Jona. (Nie jestem. Jestem pewien, że lista rzeczy, których Jon musi się ode mnie nauczyć jest wyjątkowo krótka.) Ale myślę, że punkt, który zamierzam zrobić, jest dość ważny dla każdego, kto pracuje z XML do przemyślenia.
Podejrzewam, że rozwiązanie Jona powstało z czegoś, co widzę u wielu programistów: myślenia o dokumentach XML jako drzewach elementów i atrybutów. Myślę, że w dużej mierze pochodzi to od programistów, których głównym zastosowaniem XML jest jako format serializacji, ponieważ cały XML, do którego są przyzwyczajeni, jest skonstruowany w ten sposób. Możesz zauważyć tych programistów, ponieważ używają oni zamiennie terminów "węzeł" i "element". To skłania ich do wymyślania rozwiązań, które traktuj wszystkie inne typy węzłów jako szczególne przypadki. (Sam byłem jednym z nich przez bardzo długi czas.)
/ Align = "left" / Ale tak nie jest. To sprawia, że problemy trudniejsze i Kod bardziej skomplikowane. Prowadzi to do ominięcia elementów technologii XML (takich jak funkcjanode()
W XPath), które są specjalnie zaprojektowane do ogólnego traktowania wszystkich typów węzłów.
Jest czerwona flaga w kodzie Jona, która zmusiłaby mnie do sprawdzenia go w przeglądzie kodu, nawet jeśli Nie wiedziałem jakie są wymagania i to jest GetElementsByTagName
. Ilekroć widzę tę metodę w użyciu, pytanie, które przychodzi mi do głowy, zawsze brzmi: "dlaczego to musi być element?"A odpowiedź brzmi bardzo często" och, czy ten kod też musi obsługiwać węzły tekstowe?"
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-27 21:42:43
Wiem, stary post ale Wersja mi się najbardziej podobała (ta z nazwiskami) była wadliwa: Gdy węzeł nadrzędny ma węzły o różnych nazwach, przestał liczyć indeks po znalezieniu pierwszej niepasującej nazwy węzła.
Oto moja poprawiona wersja:
/// <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);
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-12 10:25:59
Moje 10 pensów jest hybrydą odpowiedzi Roberta i Coreya. Mogę się pochwalić tylko za faktyczne wpisanie dodatkowych linijek kodu.
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
);
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-12-18 01:37:45
Oto prosta metoda, której użyłem, zadziałała dla mnie.
static string GetXpath(XmlNode node)
{
if (node.Name == "#document")
return String.Empty;
return GetXpath(node.SelectSingleNode("..")) + "/" + (node.NodeType == XmlNodeType.Attribute ? "@":String.Empty) + node.Name;
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-09 16:37:45
Nie ma czegoś takiego jak "xpath" węzła. Dla dowolnego węzła może być wiele wyrażeń xpath, które będą pasowały do niego.
Prawdopodobnie możesz pracować nad drzewem, aby zbudować wyrażenie , które będzie pasować do niego, biorąc pod uwagę indeks poszczególnych elementów itp., ale nie będzie to strasznie miły kod.
Po co ci to? Może być lepsze rozwiązanie.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-27 20:19:00
Jeśli to zrobisz, otrzymasz ścieżkę z nazwami węzłów der i pozycją, jeśli masz węzły o tej samej nazwie: "/Serwis[1] / System[1] / Grupa[1] / Folder[2] / Plik[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);
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-08-31 09:48:11
Stwierdziłem, że żadne z powyższych nie działa z XDocument
, więc napisałem własny kod do obsługi XDocument
i użyłem rekurencji. Myślę, że ten kod obsługuje wiele identycznych węzłów lepiej niż niektóre z innych kodów tutaj, ponieważ najpierw próbuje wejść tak głęboko w ścieżkę XML, jak to możliwe, a następnie tworzy kopię zapasową, aby zbudować tylko to, co jest potrzebne. Więc jeśli masz /home/white/bob
i /home/white/mike
i chcesz utworzyć /home/white/bob/garage
kod będzie wiedział, jak to stworzyć. Jednak nie chciałem zadzierać z predykatami lub symbolami wieloznacznymi, więc wyraźnie wykluczają je; ale łatwo byłoby dodać dla nich wsparcie.
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
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-09-27 02:18:44
A co z użyciem rozszerzenia klasy ? ;) Moja wersja (bazując na innych pracach) używa nazwy składni [index]... z pominięciem indeksu is element nie ma "braci". Pętla do uzyskania indeksu elementu znajduje się na zewnątrz w niezależnej rutynie (również rozszerzeniu klasy).
Tuż obok następujących W dowolnej klasie użytkowej (lub w głównej klasie Programu)
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 "";
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-27 12:45:57
Stworzyłem VBA dla Excela, aby zrobić to dla projektu roboczego. Wyświetla krotki Xpath i powiązanego tekstu z elemen lub atrybutu. Celem było umożliwienie analitykom biznesowym identyfikacji i mapowania niektórych xml. Doceniam, że to jest C # forum, ale pomyślałem, że może to być interesujące.
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
Zarządza zliczaniem elementów za pomocą:
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
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-11-14 21:50:09
To jeszcze łatwiej
''' <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
Innym rozwiązaniem problemu może być 'oznaczanie' xmlnodes, które później będziesz chciał zidentyfikować za pomocą niestandardowego atrybutu:
var id = _currentNode.OwnerDocument.CreateAttribute("some_id");
id.Value = Guid.NewGuid().ToString();
_currentNode.Attributes.Append(id);
, które można zapisać np. w słowniku. I możesz później zidentyfikować węzeł za pomocą zapytania xpath:
newOrOldDocument.SelectSingleNode(string.Format("//*[contains(@some_id,'{0}')]", id));
Wiem, że nie jest to bezpośrednia odpowiedź na twoje pytanie, ale może pomóc, jeśli powodem, dla którego chcesz poznać xpath węzła, jest posiadanie sposobu "dotarcia" do węzła później po utracie odniesienia do niego w kod.
Rozwiązuje to również problemy, gdy dokument pobiera elementy dodane/przeniesione, co może zepsuć ścieżkę xpath (lub indeksy, jak sugerowano w innych odpowiedziach).
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-05-18 14:28:35
public static string GetFullPath(this XmlNode node)
{
if (node.ParentNode == null)
{
return "";
}
else
{
return $"{GetFullPath(node.ParentNode)}\\{node.ParentNode.Name}";
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-29 08:26:54
Musiałem to zrobić niedawno. Należy wziąć pod uwagę tylko elementy. Oto co wymyśliłem:
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);
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-28 14:55:24