XPath : le language

XPath

Préambule

Présentation du language XPath pour faire des requêtes sur un fichier XML

Pré-requis

- Connaissance d'XML
- Connaissance de base de System.Xml

Introduction

Il y a quelques années encore, l'XML (eXtensible Markup Language) avait une importance assez marginale en informatique. Les fichiers de configurations et autres structures représentant des données étaient décrites avec des moyens rudimentaires comme les fichiers INI, par exemple.

Aujourd'hui, l'XML occupe une place prépondérante dans le monde de l'informatique et a
remplacé bon nombre d'autres solutions. XPath est un langage qui permet de récupérer une partie d'un document XML qui nous intéresse, en faisant une requête sur le document.

Ce tutoriel sur XPath va donc montrer comment créer une telle requête pour avoir accès aux informations voulue. Prenez bien note que le but n'est pas de présenter System.Xml.XPath mais bel et bien la façon dont ont construit une requête (en donnant beaucoup d'exemples).

Pour finir, sachez qu'XPath est extrêmement complet et qu'il ne s'agit pas non plus de présenter tous les aspects du langage dans cet article, mais seulement les bases. Le reste pouvant être approfondis par vous-même selon les besoins.

Avant de commencer

Pour tester nos différentes requêtes, on va avoir besoin d'un fichier XML. J'ai choisi de représenter une petite partie du réseau CodeS-SourceS, en décrivant la hiérarchie des différents éléments, de manière assez simple. Voici ce qu'on obtient

<CodeS-SourceS>
    <csharpfr code = "1080">
        <Sources>
            <Source name = "XML, XPath" url = "x.aspx">
                <Rating> 8 </Rating>
                <Author> Bidou </Author>
                <Visit>8 343 </Visit>
                <Updates>
                    <Update Date= "15.02.2005"> Correction d'un bug </Update>
                </Updates>
            </Source>
            <Source name = "Tuto XNA" url = "y.aspx">
                <Rating> 8.5 </Rating>
                <Author>Mx </Author>
                <Visit> 5241 </Visit>
            </Source>
            <Source name = "Interop" url = "z.aspx">
                <Rating> 10 </Rating>
                <Author> Lutinore </Author>
                <Visit> 12541 </Visit>
            </Source>
        </Sources>
        <Tutos>
        <Source name = "CrossThreading" url = "a.aspx">
            <Rating>9 </Rating>
            <Author>Mx </Author>
            <Visit>15241 </Visit>
            <Updates>
                <Update Date = "22.05.2006"> Mise en forme </Update>
                <Update Date = "23.05.2006"> Re Mise en forme </Update>
                <Update Date = "24.05.2006"> Re re Mise en forme ! </Update>
            </Updates>
        </Source>
        </Tutos>
    </csharpfr>
    <vbfrance code = "10345">
        <Sources>
            <Source name = "RegEx" url = "s.aspx">
                <Rating> 8.4 </Rating>
                <Author>Seb </Author>
                <Visit>8821 </Visit>
                <Updates>
                    <Update Date = "02.02.2004"> Ajout d'une option </Update>
                    <Update Date = "02.03.2004"> Et le Coq alors !? </Update>
                    <Update Date = "02.03.2004"> Cocorico ! </Update>
                </Updates>
            </Source>
            <Source name = "Thread" url = "f.aspx">
                <Rating>5 </Rating>
                <Author>Mx </Author>
                <Visit>1241 </Visit>
                <Updates>
                    <Update Date="22.05.2006"> Euh, oui ? </Update>
                </Updates>
            </Source>
        </Sources>
        <Tutos>
            <Source name = "VS 2005" url = "r.aspx">
                <Rating> 9.3 </Rating>
                <Author> Coq </Author>
                <Visit> 16321 </Visit>
                <Updates>
                    <Update Date = "17.04.2004"> Ajout commentaire </Update>
                </Updates>
            </Source>
            <Source name = "Codyx" url = "l.aspx">
                <Rating> 8.9 </Rating>
                <Author>Nix </Author>
                <Visit>10005 </Visit>
            </Source>
        </Tutos>
    </vbfrance>
</CodeS-SourceS>

Maintenant qu'on a la structure du fichier XML, on peut sans autre le représenter sous forme graphique pour se rendre compte un peu mieux de la forme de notre fichier. Chaque cercle avec une lettre représente ici un Node.

Voici une table de relation que l'on peut établir si on part du principe que le noeud courant est H. Les valeurs peuvent être directement utilisée dans la requête XPath.

Value Node
self H
parent B
child I
descendant I, J, K, L
descendant-or-self H, I, J, K, L
ancestor B, A
ancestor-or-self H, B, A
preceding E, D, C, F, G
preceding-sibling C
following M, N, O, P, Q, R, S, T, U, V, W, X
following-sibling -

Les requêtes

Remarque : tous les exemples qui suivent se réfèrent au fichier XML que j'ai décris plus haut.

Pour faire les différentes requêtes, je vais commencer par créer un XmlDocument. C'est une des façons de faire une requête sur un fichier.

// Load the xml file
XmlDocument xDoc = new XmlDocument();
xDoc.Load("CS.xml");

Rappel: XmlDocument est une sous-classe de XmlNode
Afin d'alléger le document, je ne propose que la requête pour les exemples. A chaque fois, il faut donc appeler les méthodes SelectNodes (liste de Nodes) et/ou SelectSingleNode (un Node) qui appartiennent au XmlDocument.

XmlNode xNode = xDoc.SelectSingleNode(xPathExp);
XmlNodeList xNodes = xDoc.SelectNodes(xPathExp);

Les requêtes de base, sans utiliser de fonctions

Sélection simple

Pour se rendre sur un Node spécifique, il suffit de spécifier son path, voici deux exemples

// Retourne l'arborescence à partir de l'élément Sources de csharpfr
"CodeS-SourceS/csharpfr/Sources"
// Retourne une collection de noeuds représentant chaque source de csharpfr
"CodeS-SourceS/csharpfr/Sources/Source"

Remarque : L'élément child est omis pour des questions de simplifications. On peut donc faire la modification suivante :

"child::CodeS-SourceS/child::csharpfr/child::Sources"

par

"CodeS-SourceS/csharpfr/Sources"

Sélection avec booléens et égalités

Les crochets permettent de saisir des expressions (conditions , prédicats).
On s'intéresse à toutes les sources qui ont une note plus grande ou égale à 8.5

// Les descendants de CodeS-SourceS avec un élément Source qui a un attribut
// plus grand où égal à 8.5
// Retourne 5 noeuds (les 5 sources ayant la note qui répond au critère)
"CodeS-SourceS/descendant::Source[Rating>=8.5]"

L'étoile permet de remplacer n'importe quel élément. Dans ce cas, on n'obtient pas le même résultat que le précédent car on cherche uniquement dans les sources, et pas les tutoriaux

// Recherche sur tous les sites les Sources (pas les Tutos) qui ont une source avec
// une note plus grande ou égal à 8.5
"CodeS-SourceS/*/Sources/Source[Rating>=8.5]"

On peut combiner des critères avec les opérateurs and et or

// Note plus grande que 8 et visite plus grande que 10'000
"CodeS-SourceS/descendant::Source[Rating>8 and Visit>10000]"

Cela fonctionne également sur les strings (toutes les sources de Mx)

// Les sources de l'auteurs s'appelant Mx
"CodeS-SourceS/descendant::Source[Author='Mx']"

Ou toutes les sources qui ne sont pas de Bidou

// Les sources n'appartenant pas à Bidou
"CodeS-SourceS/descendant::Source[Author!='Bidou']"

Not peut également être utilisé (toutes les sources qui n'ont pas de note inférieure à six)

// Les sources avec une note plus grande ou égal à 6
"CodeS-SourceS/descendant::Source[not(Rating<6)]"

Si on s'intéresse aux auteurs distinct, on peut faire quelque chose comme ceci

// Tous les auteurs de source (sans doublon)
"//Source[not(Author = preceding::Source/Author)]/Author"

Vérifie si le Rating courant est plus grand que le rating qui le précède. Dans ce cas, les noeuds suivant nous sont retournés : F, G, I, O, V, X

// Rien de très intéressant dans notre exemple: vérifie si le node courant
// est plus grand que celui qui le précède
"//Source[(Rating>preceding::Rating)]"

Accéder aux Attributs

Les sources avec l'attribut url qui vaut "x.aspx"

// Les sources qui ont une url qui vaut x.aspx
"CodeS-SourceS/descendant-or-self::Source[attribute::url='x.aspx']"

Remarque : L'élément descendant-or-self est remplacé par // et attribute par @ des questions de simplifications. On peut donc faire la modification suivante :

"CodeS-SourceS/descendant-or-self::Source[attribute::url='x.aspx']"

par

"CodeS-SourceS//Source[@url='x.aspx']"

Expression numérique

Retourne les sites qui ont un nombre de code étant un multiple de 4

// Les sites ayant un nombre de code source étant multiple de quatre
"CodeS-SourceS/*[(@code mod 4) = 0]"

Les requêtes avec quelques fonctions

En fait, XPath peut aller encore beaucoup plus loin : il contient des fonctions au sein de son langage. On peut donc faire des sommes et autres manipulations très pratiques. Dans le cas d'une somme par exemple, il est clair que l'on ne doit plus retourner un set de Nodes comme on le faisait auparavant, mais juste une valeur numérique.

Il faut donc utiliser un autre objet, le XPathNavigator, qui contient une méthode d'évaluation. Voici comment on le construit:

XPathDocument xPathDoc = new XPathDocument("CS.xml");
XPathNavigator xPathNav = xPathDoc.CreateNavigator();

Comme avant, afin d'alléger le document, je ne propose que la requête pour les exemples. A chaque fois, il faut donc appeler la méthode Evaluate.

object o = xPathNav.Evaluate(xPathExp);

Sum et Count

Ici, on aimerait trouver le nombre de code disponible sur CodeS-SourceS

// Le nombre de code disponible en tout. @ représente toujours un attribut
"sum(CodeS-SourceS/*/@code)"

On peut aussi très facilement avoir le nombre de site sur CodeS-SourceS

// Il s'agit dans notre exemple de vbfrance et csharpfr
"count(CodeS-SourceS/*)"

Et donc, la moyenne des code par site

// On effectue la division (expression numérique)
"sum(CodeS-SourceS/*/@code) div count(CodeS-SourceS/*)"

Pas mal, non?
Sans XPath en tout cas, il faut plus de code pour faire la même fonction. Un grand gain de temps donc, si on sait manipuler la chose.

XPath propose plein d'autres fonctions mais le but n'est pas de toutes les présenter. Sachez simplement qu'on peut effectuer des opérations sur des strings (substring par exemple), des entiers, etc. Mieux vaut lire la doc et chercher la fonction dont on a besoin sur le coup, plutôt que de les apprendre toutes par coeur! ;-)

Comparaison

But de l'exercice:
Créer une fonction, une fois avec XPath et une fois sans, qui retourne les auteurs de codes ayant posté une source avec au moins deux mises à jour dont la dernière en 2005 ou après ainsi qu'un minimum de 10'000 visites ou une note plus grande ou égal à 9.

Solution XPath:

XPathDocument xPathDoc = new XPathDocument("CS.xml");
XPathNavigator xPathNav = xPathDoc.CreateNavigator();
string visitsOrRating = "((Visit>10000) or (Rating>=9))";
string lastUpdate = "(*/Update[last()][(substring(@Date, 7) > 2004)])";
string nbUpdate = "(count(*/Update) > 1)";
string xPathExp = string.Format("//Source[{0} and {1} and {2}]/Author", visitsOrRating, lastUpdate, nbUpdate);
// L'appel de compile permet de compiler l'expression ce qui permet une exécution (beaucoup) plus rapide
XPathNodeIterator authors = xPathNav.Select(xPathNav.Compile(xPathExp));

Solution « Standard »:

XmlDocument xDoc = new XmlDocument();
xDoc.Load("CS.xml");
List<string> result = new List<string>();

foreach (XmlElement xElementSite in xDoc.DocumentElement)
{
    foreach (XmlNode xNodeSources in xElementSite.ChildNodes)
    { 
        foreach (XmlNode xNodeSource in xNodeSources.ChildNodes)
        {
            int visit = 0;
            Int32.TryParse(xNodeSource["Visit"].InnerText, out visit);
            double rating = 0;
            Double.TryParse(xNodeSource["Rating"].InnerText, out rating);

            if (visit > 10000 || rating >= 9)
            {
                XmlElement updates = xNodeSource["Updates"];
                if (updates != null && updates.ChildNodes.Count > 1) 
                {
                    string yearStr = updates.LastChild.Attributes["Date"].Value;
                    int year = 0;
                    Int32.TryParse(yearStr.Split('.')[2], out year);
                    if (year > 2004) result.Add(xNodeSource["Author"].InnerText);
                }
            }
        }
    }
}

Résultat et Conclusion

La StopWatch pour mesurer le temps est enclenchée juste avant le Select pour le premier cas, et juste avant le premier foreach pour le second.
Les chiffres dans le tableau plus bas sont une moyenne de plusieurs tests, ils représentent le nombre de Ticks écoulés.

File size - nb de match XPath Standard Coef
3Ko - 1 42'408 76'488 1.8
157Ko - 1 46'864 3'667'252 78
242Ko - 90 47'796 5'257'592 110
425Ko - 90 46'964 9'356'804 203
6170Ko - 15'228 50'988 134'623'548 2640 !

XPath est un langage à part entière tout comme les expressions régulières. La différence étant que la technologie XML est utilisée presque partout: apprendre comment manipuler les expressions régulières est certainement fort utile, mais dans la pratique on ne l'utilisera qu'assez rarement.
Apprendre XPath est un très bon investissement qui sera très vite rentabilisé: non seulement le nombre de ligne de codes est moindre, mais en plus les temps de réponses sont sans appels.

Article XPath sur mon blog

Ce document intitulé « XPath : le language » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous