XPath pour identifier des bouts de XML

Creative Commons License
Cette création est mise à disposition sous un contrat Creative Commons.

L'article original se trouve sur http://ymettier.free.fr/articles_lmag/.

Article publié dans le numéro 51 (juin 2003) de GNU/Linux France Magazine


Table des matières

1. Introduction
2. Ce que peux sélectionner XPath
3. Un peu de pratique avant la théorie
4. Sélectionnons avec XPath...
4.1. Les éléments
4.2. Les textes, commentaires...
4.3. Les attributs
4.4. Sélections multiples
4.5. Chemins
5. Les axes
6. Les prédicats
7. Quelques fonctions XPath
7.1. Vrai et faux
7.2. position()
7.3. lang()
7.4. not()
7.5. Les fonctions de chaînes de caractères
7.6. Les fonctions sur les nombres
7.7. Les fonctions sur les noeuds
7.8. D'autres fonctions
8. Retour sur un chemin XPath
9. Conclusion

1. Introduction

XML sans XPath est un peu ce que serait un SGBD relationnel sans le langage SQL. XPath est un langage, qui au contraire de nombreux langages relatifs à XML, n'est pas XML. Sa syntaxe n'a rien à voir avec XML. Mais son rôle est de permettre d'identifier des bouts de XML, comme un élément, un texte, un attribut... Ainsi, comme nous l'avons vu dans les autres articles, XSLT sélectionne grâce à XPath un bout de XML pour y appliquer une transformation.

2. Ce que peux sélectionner XPath

Sous ce titre de paragraphe se cache la liste des types de noeuds que peut sélectionner XPath. Les voici:

  • Le noeud racine (attention: ce noeud contient tout le document: un noeud est bien plus qu'un élément);

  • les noeuds éléments;

  • les noeuds attributs;

  • les noeuds textes;

  • les noeuds de commentaires;

  • les noeuds d'instructions de traitement;

  • les noeuds d'espace de nom.

3. Un peu de pratique avant la théorie

Pour tester les expressions XPath que vous voulez créer, nous verrons un bout de code en C avec libxml2 dans les briques de base en C, mais voici un moyen plus simple de faire:

 1 <?xml version="1.0" encoding="ISO-8859-1"?>
 2 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 3   <xsl:output method="text" />
 4 
 5   <xsl:template match="@*|node()">
 6     <xsl:value-of select='substring("XSLT XPath XML",6,5)'/>
 7     <xsl:text>&#xa;</xsl:text>
 8   </xsl:template>
 9 
10 </xsl:stylesheet>

Vous voyez que l'expression XPath doit être placée dans <xsl:value-of select='Expression XPath ici'/>. Et pour le test, ayez un fichier XML sous la main, peu importe son contenu, ou au contraire, avec le contenu de votre choix si vous faites un test sur un jeu de noeuds, pour effectuer:

$ xsltproc test_xpath.xsl fichier.xml

Le résultat pour l'exemple précédent est:

XPath

4. Sélectionnons avec XPath...

4.1. Les éléments

Un document XML a une forme arborescente. Pour sélectionner l'élément <title> qui se trouve dans un document XHTML, voici un chemin XPath: /html/head/title. Si cela vous fait penser à une arborescence de fichier Unix, c'est parce que l'on a aussi une arborescence dans le système de fichiers. L'analogie peut encore être poussée un peu plus loin, car suivant l'endroit où l'on est dans l'arborescence, on peut sélectionner un bout de XML simplement avec son nom, comme title, ou même avec un chemin relatif head/title. Mais l'analogie s'arrête là.

4.2. Les textes, commentaires...

Nous avons vu comment sélectionner un élément. Pour sélectionner son contenu, il faut utiliser une localisation spéciale: text(). De la même manière, pour un commentaire, c'est comment() et pour une instruction de traitement processing-instruction(). Pour récupérer le texte du titre et non pas l'élément <title>, voici quelques exemples: /html/head/title/text() ou title/text() ou ... Pour les commentaires, le code suivant remplace tout commentaire par le commentaire comme quoi le précédent commentaire a été supprimé:

1 <xsl:template match="comment()">
2   <xsl:comment>Commentaire supprimé</xsl:comment>
3 </xsl:template>

4.3. Les attributs

Pour sélectionner un attribut, par exemple la taille du bord d'un tableau défini par <table border="0">, il faut faire précéder le nom de l'attribut par un @. Cela donne dans ce cas: table/@border qui doit contenir 0.

Toujours pour l'exemple précédent, et en guise d'illustration, nous pouvons rajouter une légende indiquant la taille du bord:

1 <xsl:template match="table">
2   <!-- copier ici la table ou en faire ce qu'on veut -->
3   La taille de la bordure de la table est <xsl:value-of select="@border"/>
4 </xsl:template>	

4.4. Sélections multiples

Il existe plusieurs manières d'effectuer une sélection multiple. L'une est d'utiliser le symbole | comme dans les expressions rationnelles. Pour sélectionner n'importe quel sous-titre XHTML par exemple, utilisez h1|h2|h3|h4|h5|h6. Mais il existe aussi des jokers, un peu comme le point et l'étoile des expressions rationnelles. N'importe quel noeud d'élément, peu importe son nom, se sélectionne avec l'étoile. Ainsi, /html/body/* sélectionnera tous les noeuds fils du noeud body dans l'arborescence d'un document XHTML. Cependant, l'étoile ne sélectionne que les noeuds d'éléments. Pour sélectionner n'importe quel noeud d'attribut, il faut utiliser @*. Et s'il s'agit de sélectionner n'importe quel noeud, qu'il soit d'élément, d'attribut, de texte, de commentaire... de n'importe quel noeud, utilisez node().

Explication du chemin @*|node() que l'on rencontre souvent: nous sommes en train de traiter le noeud courant. Si nous sélectionnons juste node(), nous aurons tous les noeuds, attributs compris, mais fils. Nous n'aurons pas les attributs du noeud courant. C'est pour cela que nous ajoutons @* pour sélectionner aussi les attributs du noeud courant.

4.5. Chemins

Enfin, pour créer un chemin, nous avons vu / dont la signification est triviale. Mais il y a aussi . et .. dont la signification est triviale aussi: le noeud courant pour le point, et le noeud parent pour le double point. Par contre // est moins trivial! Cela permet de sélectionner parmi tous les déscendants, pas uniquement les fils du noeud qui précède le double slash. Dans un exemple précédent, nous sélectionnions tous les noeuds fils du noeud body avec /html/body/*. Pour sélectionner non seulement les noeuds fils, mais les fils des fils et ainsi de suite, voici le chemin: /html/body//*.

5. Les axes

Dans les chemins précédents, nous avons utilisé l'axe par défaut, child, qui peut être omis. Si on ne l'omet pas, voici ce que donne /html/body: /child::html/child::body. Nous avons vu aussi l'axe opposé, parent, dans sa forme abrégée: ... Il en existe 13 en tout, que vous retrouvez dans le tableau suivant:

Tableau 1. Les axes

AxeForme abrégéeExplication
self.Noeud courant (. et self::node() sont similaires)
parent..Noeud parent (.. et parent::node() sont similaires)
ancestorn/aTout noeud contenant le noeud courant, parent et racine inclus
ancestor-or-selfn/aComme ancestor, mais avec le noeud courant inclus
child Noeud fils (nom et child::nom sont similaires)
descendantn/aNoeuds fils et tous les noeuds dont le noeud courant est le père
descendant-or-self//Comme descendant, mais avec le noeud courant inclus (//nom et descendant-or-self::nom()/ sont similaires
preceding-siblingn/aLes noeuds frères précédent le noeud courant, ou plus précisément tous les noeuds pécédent le noeud courant qui ont le même noeud parent
precedingn/aComme preceding-sibling, mais sans les noeuds d'attributs et d'espace de noms
following-siblingn/aLes noeuds frères suivant le noeud courant, ou plus précisément tous les noeuds suivant le noeud courant qui ont le même noeud parent
followingn/aComme following-sibling, mais sans les noeuds d'attributs et d'espace de noms
attribute@Tous les noeuds d'attributs du noeud courant (@nom et attribute::nom sont similaires)
namespacen/aTous les espaces de noms du noeud courant

Vous pouvez remarquer ainsi que sur les 13 axes possibles, seuls 5 ont une forme abrégée. Pour les 8 autres, c'est la forme complète qu'il faut prendre! Mais cela n'est pas génant, car à l'image des autres articles, l'utilisation des 8 axes qui n'ont pas de forme abrégée est plus rare.

6. Les prédicats

Un prédicat est une expression booléenne, qui si elle est fausse, retire le noeud sélectionné par le chemin XPath de la liste. Cette expression se place entre crochets. Nous avons vu un chemin XPath assez simple lors de la création du site web: ./text[@lang=$lang]. $lang est une variable, et @lang la valeur de l'attribut lang. Le chemin ./text correspondant aux éléments fils du noeud courant dont le nom est text (à ne pas confondre avec text() s'il vous plait), est donc restreint avec [@lang=$lang] à ces même noeuds, mais que s'ils ont un attribut lang égal à la variable $lang.

Les prédicats font grand usage des fonctions XPath que nous allons voir après. En effet, les prédicats nécessitent souvent des calculs, et les fonctions XPath sont là pour cela.

7. Quelques fonctions XPath

7.1. Vrai et faux

Avant de commencer avec les fonctions, les VRAI et FAUX booléens qui sont habituellement des valeurs dans d'autres langages sont ici des fonctions: true() et false().

7.2. position()

La première fonction que nous allons voir est la fonction position() qui renvoie un nombre, la position du noeud actuel dans la liste des noeuds courants. Nous avons vu cette fonction avec les nouvelles, car on ne voulait en afficher qu'un nombre limité. Pour cela, le prédicat était [$nb_news >= position()].

7.3. lang()

Nous avons vu aussi dans les autres articles la fonction lang(), qui renvoie une valeur booléenne vraie si l'argument fourni à la fonction lang() correspond à la langue du noeud courant. Cette langue s'exprime dans le noeud courant avec l'attribut xml:lang.

7.4. not()

not() comme son nom l'indique, et comme dans de nombreux langages, inverse la valeur de l'argument qui lui est fourni en paramètre. Il nous a servi aussi dans les autres articles.

7.5. Les fonctions de chaînes de caractères

Nous pouvons manier les chaînes de caractères avec XPath, avec quelques fonctions:

  • concat() concatène les chaînes de caractères fournies en argument et retourne le résultat. Le nombre d'arguments est quelconque;

  • contains() renvoie VRAI si le second argument est une sous-chaîne de la chaîne fournie en premier argument;

  • normalize-space() remplace tous les groupes de caractères blancs par une espace unique, et supprime les caractères blancs en début et fin de la chaîne fournie en argument;

  • start-with() renvoie VRAI si la chaîne de caractères fournie en second argument correspond au début de la chaîne fournie en premier argument;

  • string() convertit l'argument qui lui est fourni en une chaîne de caractères qu'elle renvoie;

  • substring() renvoie la sous-chaîne de la chaîne de caractères fournie en premier argument, qui démarre au caractère dont l'index est fourni en second argument, et de longueur le troisième argument qui est par ailleurs facultatif

  • substring-after() recherche la chaîne de caractères fournie en second argument dans celle fournie en premier argument. Si elle est trouvée, la fonction renvoie tout ce qui suit. Sinon, elle renvoie une chaîne vide;

  • substring-before() recherche la chaîne de caractères fournie en second argument dans celle fournie en premier argument. Si elle est trouvee, la fonction renvoie tout ce qui précède. Sinon, elle renvoie une chaîne vide;

  • string-length ne renvoie pas une chaîne de caratères, mais la longueur de celle fournie en argument;

  • translate() remplace dans la chaîne fournie en premier argument tous les caractères se trouvant dans la chaîne fournie en second argument, par le caractères situé en même position dans la chaîne fournie en troisième argument. Un exemple sera plus expressif: translate("Je suis un warlordz","aelo","4310") renvoie "J3 suis un w4r10rdz".

7.6. Les fonctions sur les nombres

Les nombres, entieres ou réèls, ont aussi droit à leurs fonctions:

  • ceiling() retourne le plus petit entier supérieur au nombre fourni en argument;

  • floor() retourne le plus grand entier inférieur ou égal au nombre fourni en argument;

  • round() retourne l'entier le plus proche du nombre fourni en argument;

  • number() convertit son argument en nombre, ou en NaN si cela n'est pas un nombre;

  • sum() convertit tous les noeuds fournis en arguments en nombre de la même manière que number(), puis ajoute ces nombres, et renvoie le résultat;

7.7. Les fonctions sur les noeuds

  • count() renvoie le nombre de noeuds dans le jeu de noeuds fourni en argument;

  • id() renvoie un jeu de noeuds correspondant aux ID spécifiés en argument;

7.8. D'autres fonctions

La liste de fonctions ci-dessus n'est pas exhaustive, et surtout chaque processeur XSLT peut implémenter des fonctions qui lui sont propres. Avec xsltproc, xsltproc --dumpextensions vous affichera les extensions supportées, dont par exemple tokenize() pour les chaînes de caractères, ou sin() et cos() pour les nombres.

8. Retour sur un chemin XPath

Je voudrais revenir sur la fonction XPath qui nous a permis de retrouver toutes les langues d'un document. Cette expression était: //span[not(@xml:lang=preceding::span/@xml:lang)]/@xml:lang. Cette expression est construite ainsi: nous recherchons tous les attributs xml:lang des éléments span. Ceci pourrait suffire à établir la liste des langues, mais nous nous retrouverions à coup sur avec des doublons. Il s'agit donc de supprimer les doublons avec ce que contient le prédicat. Ce prédicat teste si l'attribut courant xml:lang est égal à un attribut xml:lang d'un élément span faisant partie de l'ensemble des noeuds précédent: precedent::span/@xml:lang. Si le test renvoie vrai, c'est que nous avons un doublon. Pour avoir le bon prédicat, nous inversons donc le résultat du test avec not(), ce qui donne le prédicat que vous retrouvez dans l'article vous montrant comment générer la page des nouvelles.

Un autre chemin XPath intéressant est celui que l'on trouve dans les briques en C pour tester les permissions: /authorizations/group[@name=/authorizations/action[@name='visiter' and @object='planete']/authorization/@grantedto]/user/text()[.='toto']. J'ai remplacé les %s que l'on trouve dans l'article par des valeurs plus concrètes ici. Voyez l'article pour le format du fichier XML des permissions. Ici, voici comment la requête est construite. Nous voulons un utilisateur qui s'appelle toto: /authorizations/group/user/text()[.='toto']. Nous voulons aussi que cet utilisateur se trouve dans un groupe dont le nom est un nom que l'on retrouve dans les actions autorisées. Nous remplaçons group par group[@name=/authorizations/action/authorization/@grantedto]. Enfin, ce n'est pas n'importe quelle action, mais l'action dont le nom est 'visiter' et l'objet est 'planete'. Nous remplaçons encore action par action[@name='visiter' and @object='planete']. Et nous aboutissons à la requète voulue. Cette requète nous renvoie donc le nom de l'utilisateur qui a justement ce nom, s'il a les permissions, et rien s'il n'y a pas de tel utilisateur. Dans l'exemple des briques en C, il n'y a pas de tel utilisateur pour effectuer une telle action, donc le résultat est vide!

9. Conclusion

XML, donc XSLT, sans XPath, n'est rien. Avec XPath, vous pouvez spécifier une partie d'un document d'un document XML qui vous intéresse, pour l'extraire comme nous le verrons avec les briques en C sur libxml2, ou pour le transformer comme nous avons vu dans les articles qui expliquent comment faire le site web.

création est mise à disposition sous un contrat Creative Commons