web-dev-qa-db-fra.com

Utilisation de Xpath avec un espace de noms par défaut en C #

J'ai un document XML avec un espace de noms par défaut. J'utilise un XPathNavigator pour sélectionner un ensemble de nœuds à l'aide de Xpath, comme suit: 

XmlElement myXML = ...;  
XPathNavigator navigator = myXML.CreateNavigator();
XPathNodeIterator result = navigator.Select("/outerelement/innerelement");

Je ne reçois aucun résultat: je suppose que c'est parce que je ne spécifie pas l'espace de noms. Comment puis-je inclure l'espace de noms dans ma sélection?

58
macleojw

Tout d'abord, vous n'avez pas besoin d'un navigateur. SelectNodes/SelectSingleNode devrait suffire.

Cependant, vous aurez peut-être besoin d'un gestionnaire d'espaces de noms, 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);
77
Marc Gravell

Vous voudrez peut-être essayer un outil XPath Visualizer pour vous aider. 

XPathVisualizer est gratuit, facile à utiliser. 

 alt text

IMPORTANT: Si vous utilisez Windows 7/8 et ne voyez pas les éléments Fichier, Éditer et Menu Aide, veuillez appuyer sur la touche ALT.

48
Cheeso

Lorsque vous utilisez XPath dans .NET (via un navigateur ou SelectNodes/SelectSingleNode) sur XML avec des espaces de noms, vous devez:

  • fournir votre propre XmlNamespaceManager

  • et préfixent explicitement tous les éléments de l'expression XPath, qui se trouvent dans un espace de noms.

Ce dernier est (paraphrasé à partir de la source MS liée ci-dessous): parce que XPath 1.0 ignore les spécifications d'espace de nom par défaut (xmlns = "nom_un_espace"). Ainsi, lorsque vous utilisez un nom d'élément sans préfixe, il suppose un espace de nom nul.

C'est pourquoi l'implémentation .NET de XPath ignore l'espace de noms avec le préfixe String.Empty dans XmlNamespaceManager et utilise toujours un espace de noms nul.

Voir XmlNamespaceManager et UndefinedXsltContext ne traitent pas l'espace de noms par défaut pour plus d'informations.

Je trouve cette "fonctionnalité" très gênante car vous ne pouvez pas créer un ancien espace de nommage XPath en ajoutant simplement une déclaration d'espace de nom par défaut, mais c'est ainsi que cela fonctionne.

19
Tomek Szpakowicz

Vous pouvez utiliser l'instruction XPath sans utiliser XmlNamespaceManager comme ceci:

...
navigator.Select("//*[ local-name() = 'innerelement' and namespace-uri() = '' ]")
...

C'est un moyen simple de sélectionner un élément dans XML avec un espace de nom par défaut défini .

Le but est d'utiliser: 

namespace-uri() = ''

qui trouvera un élément avec un espace de noms par défaut sans utiliser de préfixes.

6

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

Ensuite, dans mon code d'analyse XML, je viens d'ajouter 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 beaucoup cette méthode car elle est complètement dynamique en termes de chargement des espaces de noms à partir du fichier XML source et qu'elle ne néglige pas complètement le concept d'espaces de noms XML. Elle peut donc être utilisée avec XML qui nécessite plusieurs espaces de noms pour la résolution.

5
Kent

J'ai rencontré un problème similaire avec un espace de noms par défaut vide. Dans cet exemple XML, j’ai un mélange d’éléments avec des préfixes d’espace de nommage 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 dans le 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" de XPath, mais je n'ai pas réussi à le faire fonctionner, sauf en utilisant des caractères génériques dans l'élément DataBlock:

  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 casse-tête et de recherches sur Google (ce qui m'a amené ici), j'ai décidé de m'attaquer à l'espace de noms par défaut directement dans mon chargeur XmlNamespaceManager en le remplaçant par:

  foreach ( KeyValuePair<string, string> nskvp in nav.GetNamespacesInScope( XmlNamespaceScope.All ) ) {
    nsman.AddNamespace( nskvp.Key, nskvp.Value );
    if ( nskvp.Key == "" ) {
      nsman.AddNamespace( "default", nskvp.Value );
    }
  }

Alors maintenant, "default" et "" désignent le même espace de noms. Une fois que j'ai fait cela, le XPath "/ src: SRCExample/default: DataBlock/a: DocID/a: IDID" a renvoyé mes résultats exactement comme je le voulais. Espérons que cela aide à clarifier la question pour les autres.

5
Brandon

Si les espaces de noms diffèrent pour outerelement et innerelement

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);
5
Rashmi Pandit

Dans mon cas, ajouter un préfixe n'était pas pratique. Trop de xml ou xpath ont été déterminés au moment de l'exécution. Finalement, j'ai étendu les méthodes sur XmlNode. Cela n’a pas été optimisé pour la performance et cela ne gère probablement pas tous les cas, mais cela fonctionne jusqu’à présent pour moi.

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

Ensuite, dans votre code, utilisez quelque chose comme

        XmlDocument document = new XmlDocument();
        document.Load(pathToFile);
        XmlNode node = document.SelectFirstNode("/rootTag/subTag");

J'espère que cela t'aides

3
SpikeDog

Ou, si quelqu'un utilise 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);
1
Zak

J'ai utilisé l'approche simpliste mais utile décrite par SpikeDog ci-dessus. Cela a très bien fonctionné jusqu'à ce que je lance une expression xpath qui utilise des tuyaux pour combiner plusieurs chemins.

Alors je l'ai réécrit en utilisant des expressions régulières et j'ai pensé partager:

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

1] Si vous avez un fichier XML sans préfixe dans l’espace de nom:

<bookstore xmlns="http://www.contoso.com/books">
…
</bookstore>

vous avez cette solution de contournement:

XmlTextReader reader = new XmlTextReader(@"C:\Temp\books.xml");
// ignore the namespace as there is a single default namespace:
reader.Namespaces = false;
XPathDocument document = new XPathDocument(reader);
XPathNavigator navigator = document.CreateNavigator();
XPathNodeIterator nodes = navigator.Select("//book");

2] Si vous avez un fichier XML avec un préfixe dans l'espace de noms:

<bookstore xmlns:ns="http://www.contoso.com/books">
…
</bookstore>

Utilisez ceci:

XmlTextReader reader = new XmlTextReader(@"C:\Temp\books.xml");
XPathDocument document = new XPathDocument(reader);
XPathNavigator navigator = document.CreateNavigator();
XPathNodeIterator nodes = navigator.Select("//book");

Bien sûr, vous pouvez utiliser un espace de noms à gérer si nécessaire:

XmlTextReader reader = new XmlTextReader(@"C:\Temp\books.xml");
XPathDocument document = new XPathDocument(reader);
XPathNavigator navigator = document.CreateNavigator();
XmlNamespaceManager nsmgr = new XmlNamespaceManager(reader.NameTable);
nsmgr.AddNamespace("ns", "http://www.contoso.com/book");
XPathNodeIterator nodes = navigator.Select("//book", nsmgr);

Je pense que c'est le moyen le plus simple de faire fonctionner le code dans la plupart des cas.

J'espère que cette aide aidera à résoudre ce problème de Microsoft…

0
Corto

Dans ce cas, c'est probablement la résolution de l'espace de noms qui est à l'origine du problème, mais il est également possible que votre expression XPath ne soit pas correcte en soi. Vous voudrez peut-être l'é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);
0
Cerebrus