Utilisation de Xpath avec Namespace par défaut en C#
j'ai un document XML avec un espace de noms par défaut. J'utilise XPathNavigator pour sélectionner un ensemble de noeuds en utilisant Xpath comme suit:
XmlElement myXML = ...;
XPathNavigator navigator = myXML.CreateNavigator();
XPathNodeIterator result = navigator.Select("/outerelement/innerelement");
Je n'obtiens aucun résultat en retour: je suppose que c'est parce que je ne spécifie pas l'espace de noms. Comment puis-je inclure le namespace dans mon select?
12 réponses
tout d'abord - vous n'avez pas besoin d'un navigateur; SelectNodes / SelectSingleNode devrait suffire.
vous pouvez, cependant, avoir besoin d'un namespace-manager - par exemple:
XmlElement el = ...; //TODO
XmlNamespaceManager nsmgr = new XmlNamespaceManager(
el.OwnerDocument.NameTable);
nsmgr.AddNamespace("x", el.OwnerDocument.DocumentElement.NamespaceURI);
var nodes = el.SelectNodes(@"/x:outerelement/x:innerelement", nsmgr);
vous pourriez vouloir essayer un outil visualiseur XPath pour vous aider à travers.
XPathVisualizer est gratuit, facile à utiliser.
IMPORTANT: si vous utilisez Windows 7/8 et ne voyez pas les éléments du Menu File, Edit and Help, s'il vous plaît appuyez sur ALT key.
pour toute personne à la recherche d'une solution de piratage rapide, en particulier dans les cas où vous connaître le XML et n'ont pas besoin de se soucier des espaces de noms et tout cela, vous pouvez contourner cette ennuyeuse petite "fonctionnalité" en lisant simplement le fichier à une chaîne et en remplaçant l'attribut offensant:
XmlDocument doc = new XmlDocument();
string fileData = File.ReadAllText(fileName);
fileData = fileData.Replace(" xmlns=\"", " whocares=\"");
using (StringReader sr = new StringReader(fileData))
{
doc.Load(sr);
}
XmlNodeList nodeList = doc.SelectNodes("project/property");
je trouve cela plus facile que tous les autres non-sens nécessitant un préfixe pour un espace de noms par défaut quand je traite avec un seul fichier. Espérons que cela aider.
lorsque vous utilisez XPath dans .NET (via un navigateur ou SelectNodes / SelectSingleNode) en XML avec des espaces de noms, vous devez:
-
fournir votre propre XmlNamespaceManager
-
et explicitement préfixe de tous les éléments d'une expression XPath, qui sont dans l'espace de noms.
ce dernier est (paraphrasé à partir de MS source liée ci-dessous): parce que XPath 1.0 ignore les spécifications de l'espace de noms par défaut (xmlns="some_namespace"). Ainsi, lorsque vous utilisez l'élément name sans préfixe, il suppose que l'espace de noms est nul.
C'est pourquoi .NET implémentation de XPath ignore namespace avec chaîne de préfixe.Empty in XmlNamespaceManager et allways utilise null namespace.
Voir XmlNamespaceManager et UndefinedXsltContext ne sais pas gérer l'espace de noms par défaut pour plus d'informations.
je trouve cette "fonctionnalité" est très incommode car vous ne pouvez pas rendre l'ancien namespace XPath conscient en ajoutant simplement une déclaration par défaut, mais c'est comme ça que ça marche.
vous pouvez utiliser la déclaration XPath sans utiliser XmlNamespaceManager comme ceci:
...
navigator.Select("//*[ local-name() = 'innerelement' and namespace-uri() = '' ]")
...
qui est un moyen simple de sélectionner un élément dans XML avec namespace par défaut défini.
Le point est d'utiliser:
namespace-uri() = ''
qui trouvera l'élément avec l'espace de noms par défaut sans utiliser de préfixes.
dans le cas où les espaces de noms diffèrent pour l'élément extérieur et l'élément intérieur
XmlNamespaceManager manager = new XmlNamespaceManager(myXmlDocument.NameTable);
manager.AddNamespace("o", "namespaceforOuterElement");
manager.AddNamespace("i", "namespaceforInnerElement");
string xpath = @"/o:outerelement/i:innerelement"
// For single node value selection
XPathExpression xPathExpression = navigator.Compile(xpath );
string reportID = myXmlDocument.SelectSingleNode(xPathExpression.Expression, manager).InnerText;
// For multiple node selection
XmlNodeList myNodeList= myXmlDocument.SelectNodes(xpath, manager);
j'ai rencontré un problème similaire avec un espace de noms vierge par défaut. Dans cet exemple XML, j'ai un mélange d'éléments avec des préfixes d'espace de noms, et un seul élément (DataBlock) sans:
<src:SRCExample xmlns="urn:some:stuff:here" xmlns:src="www.test.com/src" xmlns:a="www.test.com/a" xmlns:b="www.test.com/b">
<DataBlock>
<a:DocID>
<a:IdID>7</a:IdID>
</a:DocID>
<b:Supplimental>
<b:Data1>Value</b:Data1>
<b:Data2/>
<b:Extra1>
<b:More1>Value</b:More1>
</b:Extra1>
</b:Supplimental>
</DataBlock>
</src:SRCExample>
j'ai essayé D'utiliser un XPath qui fonctionnait en visualiseur XPath, mais ne fonctionnait pas dans mon code:
XmlDocument doc = new XmlDocument();
doc.Load( textBox1.Text );
XPathNavigator nav = doc.DocumentElement.CreateNavigator();
XmlNamespaceManager nsman = new XmlNamespaceManager( nav.NameTable );
foreach ( KeyValuePair<string, string> nskvp in nav.GetNamespacesInScope( XmlNamespaceScope.All ) ) {
nsman.AddNamespace( nskvp.Key, nskvp.Value );
}
XPathNodeIterator nodes;
XPathExpression failingexpr = XPathExpression.Compile( "/src:SRCExample/DataBlock/a:DocID/a:IdID" );
failingexpr.SetContext( nsman );
nodes = nav.Select( failingexpr );
while ( nodes.MoveNext() ) {
string testvalue = nodes.Current.Value;
}
Je l'ai réduit à l'élément "DataBlock" du XPath, mais je n'ai pas pu le faire fonctionner, sauf en créant simplement le datablock élément:
XPathExpression workingexpr = XPathExpression.Compile( "/src:SRCExample/*/a:DocID/a:IdID" );
failingexpr.SetContext( nsman );
nodes = nav.Select( failingexpr );
while ( nodes.MoveNext() ) {
string testvalue = nodes.Current.Value;
}
après beaucoup de recherche de têtes et de googling (qui m'a amené ici) j'ai décidé d'aborder l'espace de noms par défaut directement dans mon chargeur XmlNamespaceManager en le changeant en:
foreach ( KeyValuePair<string, string> nskvp in nav.GetNamespacesInScope( XmlNamespaceScope.All ) ) {
nsman.AddNamespace( nskvp.Key, nskvp.Value );
if ( nskvp.Key == "" ) {
nsman.AddNamespace( "default", nskvp.Value );
}
}
donc maintenant" par défaut " et "" pointent vers le même espace de noms. Une fois que j'ai fait cela, le XPath "/src:SRCExample/default:DataBlock/a:DocID/a:IdID" a retourné mes résultats comme je le voulais. Espérons que cela aide à clarifier la question pour les autres.
ma réponse prolonge la réponse précédente de Brandon. J'ai utilisé son exemple pour créer une méthode d'extension comme suit:
static public class XmlDocumentExt
{
static public XmlNamespaceManager GetPopulatedNamespaceMgr(this System.Xml.XmlDocument xd)
{
XmlNamespaceManager nmsp = new XmlNamespaceManager(xd.NameTable);
XPathNavigator nav = xd.DocumentElement.CreateNavigator();
foreach (KeyValuePair<string,string> kvp in nav.GetNamespacesInScope(XmlNamespaceScope.All))
{
string sKey = kvp.Key;
if (sKey == "")
{
sKey = "default";
}
nmsp.AddNamespace(sKey, kvp.Value);
}
return nmsp;
}
}
puis dans mon code D'analyse XML, Je n'ajoute qu'une seule ligne:
XmlDocument xdCandidate = new XmlDocument();
xdCandidate.Load(sCandidateFile);
XmlNamespaceManager nmsp = xdCandidate.GetPopulatedNamespaceMgr(); // 1-line addition
XmlElement xeScoreData = (XmlElement)xdCandidate.SelectSingleNode("default:ScoreData", nmsp);
j'aime vraiment cette méthode parce qu'elle est complètement dynamique en termes de chargement des espaces de noms à partir du fichier XML source, et elle ne fait pas complètement Abstraction du concept D'espaces de noms XML de sorte que cela peut être utilisé avec XML qui nécessite plusieurs espaces de noms pour la déconfliction.
dans mon cas, ajouter un préfixe n'était pas pratique. Trop de xml ou de xpath ont été déterminés à l'exécution. Finalement j'ai étendu la méthode sur XmlNode. Cela n'a pas été optimisé pour la performance et il ne gère probablement pas tous les cas, mais il fonctionne pour moi jusqu'à présent.
public static class XmlExtenders
{
public static XmlNode SelectFirstNode(this XmlNode node, string xPath)
{
const string prefix = "pfx";
XmlNamespaceManager nsmgr = GetNsmgr(node, prefix);
string prefixedPath = GetPrefixedPath(xPath, prefix);
return node.SelectSingleNode(prefixedPath, nsmgr);
}
public static XmlNodeList SelectAllNodes(this XmlNode node, string xPath)
{
const string prefix = "pfx";
XmlNamespaceManager nsmgr = GetNsmgr(node, prefix);
string prefixedPath = GetPrefixedPath(xPath, prefix);
return node.SelectNodes(prefixedPath, nsmgr);
}
public static XmlNamespaceManager GetNsmgr(XmlNode node, string prefix)
{
string namespaceUri;
XmlNameTable nameTable;
if (node is XmlDocument)
{
nameTable = ((XmlDocument) node).NameTable;
namespaceUri = ((XmlDocument) node).DocumentElement.NamespaceURI;
}
else
{
nameTable = node.OwnerDocument.NameTable;
namespaceUri = node.NamespaceURI;
}
XmlNamespaceManager nsmgr = new XmlNamespaceManager(nameTable);
nsmgr.AddNamespace(prefix, namespaceUri);
return nsmgr;
}
public static string GetPrefixedPath(string xPath, string prefix)
{
char[] validLeadCharacters = "@/".ToCharArray();
char[] quoteChars = "\'\"".ToCharArray();
List<string> pathParts = xPath.Split("/".ToCharArray()).ToList();
string result = string.Join("/",
pathParts.Select(
x =>
(string.IsNullOrEmpty(x) ||
x.IndexOfAny(validLeadCharacters) == 0 ||
(x.IndexOf(':') > 0 &&
(x.IndexOfAny(quoteChars) < 0 || x.IndexOfAny(quoteChars) > x.IndexOf(':'))))
? x
: prefix + ":" + x).ToArray());
return result;
}
}
alors dans votre code utilisez juste quelque chose comme
XmlDocument document = new XmlDocument();
document.Load(pathToFile);
XmlNode node = document.SelectFirstNode("/rootTag/subTag");
Espérons que cette aide
j'ai utilisé l'approche hacky-but-useful décrite par SpikeDog ci-dessus. Il fonctionnait très bien jusqu'à ce que je lance une expression xpath qui utilisait des pipes pour combiner plusieurs chemins.
donc je l'ai réécrit en utilisant des expressions régulières, et j'ai pensé que je partagerais:
public string HackXPath(string xpath_, string prefix_)
{
return System.Text.RegularExpressions.Regex.Replace(xpath_, @"(^(?![A-Za-z0-9\-\.]+::)|[A-Za-z0-9\-\.]+::|[@|/|\[])(?'Expression'[A-Za-z][A-Za-z0-9\-\.]*)", x =>
{
int expressionIndex = x.Groups["Expression"].Index - x.Index;
string before = x.Value.Substring(0, expressionIndex);
string after = x.Value.Substring(expressionIndex, x.Value.Length - expressionIndex);
return String.Format("{0}{1}:{2}", before, prefix_, after);
});
}
ou, si quelqu'un doit utiliser un XPathDocument, comme moi:
XPathDocument xdoc = new XPathDocument(file);
XPathNavigator nav = xdoc.CreateNavigator();
XmlNamespaceManager nsmgr = new XmlNamespaceManager(nav.NameTable);
nsmgr.AddNamespace("y", "http://schemas.microsoft.com/developer/msbuild/2003");
XPathNodeIterator nodeIter = nav.Select("//y:PropertyGroup", nsmgr);
dans ce cas, c'est probablement namespace resolution qui est la cause du problème, mais il est aussi possible que votre expression XPath ne soit pas correcte en soi. Vous voudrez peut-être évaluer en premier.
voici le code utilisant un XPathNavigator.
//xNav is the created XPathNavigator.
XmlNamespaceManager mgr = New XmlNamespaceManager(xNav.NameTable);
mgr.AddNamespace("prefix", "http://tempuri.org/");
XPathNodeIterator result = xNav.Select("/prefix:outerelement/prefix:innerelement", mgr);