SVGround : cours SVG

XPath : axes de sélection

Une expression XPath retourne quelque chose. Ce quelque chose peut être un ensemble de nœuds, une valeur ou rien du tout. Dans ce chapitre, les expressions XPath retourneront principalement des ensembles de nœuds. Pour visualiser le retour de ces expressions, j'utilise le programme xpath que vous trouverez dans les outils Perl de votre distribution Linux favorite (tapez la commande dans un terminal pour savoir quel paquet installer sous Ubuntu).

Axe child

La première difficulté avec les expressions XPath est de savoir quel est le nœud contextuel. Ce nœud, c'est l'endroit dans le document XML qui est considéré comme nœud courant pour appliquer l'expression XPath. Pour faire l'analogie avec un système de fichier, c'est le répertoire dans lequel on se trouve. Alors, quel est le nœud contextuel par défaut ? La réponse est : ça dépend !

Vous le verrez plus tard, le nœud contextuel dépend du langage qui utilise XPath et de l'endroit où on utilise l'expression. Dans nos exemples, c'est plus simple : le nœud contextuel est la racine du document XML.

Il y a là une petite subtilité qui m'oblige à vous parler un peu du modèle de donnée XPath, mais rassurez vous XPath est assez naturel pour qu'on puisse l'oublier lorsqu'on écrit ses expressions. Naïvement, on peut penser que la racine est l'élément racine du document XML. Il n'en est rien : la racine et l'élément sont deux types de nœud différents pour XPath, un peu comme dans le DOM ou on doit écrire document.rootElement pour accéder à l'élément racine.

La raison est qu'il peut y avoir des commentaires et des processing-instructions en dehors de l'arbre XML.

Mais ce n'est pas si compliqué : il faut juste savoir que racine et élément racine sont différents et que l'élément racine est le fils de la racine.

Le premier des axes de localisation est l'axe child. Elle permet de sélectionner les fils du nœud courant (ou contextuel). Pour l'instant, considérons que ces fils sont forcément des éléments (et donc pas du texte, pas de commentaire, etc). Nous verrons plus tard comment sélectionner d'autres types de nœud.

Les expressions XPath sont consituées de plusieurs étapes de une ou plusieurs étapes de localisation, séparées par des /. Une étape de localisation est de la forme : axe::test. Une expression XPath est donc de la forme :

axe1::test1/axe2::test2/axe3::test3/...

Les généralités ayant été traitées, il est temps d'écrire notre première expression XPath. Prenons le fichier XML suivant :

abcd cdef ghij klmn ]]>

Le nœud courant est la racine (nous "sommes" sur la racine). Pour récupérer l'élément root, il suffit d'écrire :

child::root

Le nœud retourné est bien l'élément racine. La racine n'ayant qu'un fils (l'élément racine), il est normal qu'un seul nœud soit retourné. Maintenant, pour sélectionner tous les éléments item, il suffit d'écrire :

child::root/child::item

L'écriture d'une telle expression XPath est lourde et pas assez intuitive. C'est pourquoi la norme XPath prévoit plusieurs raccourcis. L'axe child est l'axe par défaut. Si on ne l'écrit pas, il est sous entendu. Ainsi l'expression précédente peut être réécrite comme ceci :

root/child

Idem, pour sélectionner l'élément d, on écrira :

root/item/d

qui est strictement la même chose que :

child::root/child::item/child::d

]]>

Maintenant, comment faire pour sélectionner tous les fils du dernier item ?

Il existe un joker avec XPath, qui comme souvent est l'étoile *. Il signifie « tous ».

Utiliser cette étoile sélectionnera donc tous les éléments fils du nœud courant.

root/item/*

Cette expression sélectionne tous les éléments fils d'un élément item lui même fils d'un élément root.

N'oubliez pas qu'en l'absence d'axe de localisation, l'axe par défaut est child:: et c'est aussi valable pour le joker. Ainsi cette expression peut aussi s'écrire :

child::root/child::item/child::*

-- NODE -- -- NODE -- -- NODE -- ]]>

Axe parent

L'axe de localisation parent est l'opposé de l'axe child. Il désigne l'élément parent du nœud sur lequel on est. Il s'agit forcément d'un élément et pas d'un attribut, d'un nœud texte, etc.

Ainsi, à partir du l'élément a, on peut remonter vers son père comme ceci :

root/item/a/parent::item

Évidemment, puisqu'il n'y a qu'un parent, on peut utiliser le joker * ce qui revient strictement au même :

root/item/a/parent::*

Enfin, il existe un raccourci pour désigner l'élément parent : .. L'expression précédent peut donc se réécrire de cette manière :

root/item/a/..

Axe self

L'axe self est l'axe égocentrique par excellence. Il permet de localiser... le nœud courant ! Il ne sélectionne donc qu'un élément (lui même) et on peut donc utiliser le joker. Exemple :

root/item/b/self::b

ou ce qui est équivalent :

root/item/b/self::*

]]>

Il existe un raccourci pour cet axe. Il s'agit du point ., la même chose que dans les systèmes de fichier.

root/item/b/.

]]>

Si vous doutez de son utilité, vous avez raison !

Axe attribute

Les éléments peuvent porter des attributs. Dans le modèle de donnée XPath, c'est un peu particulier : le père (axe parent) d'un attribut est l'élément qui le porte mais un attribut n'est pas le fils de l'élément qui le porte. Ça ne fonctionne que dans un sens.

Ici, j'ai du mal à trouver une analogie avec le monde réel...

Soit le document XML suivant :

Pour récupérer l'attribut attr1 des éléments item, on écrira :

child::racine/child::item/attribute::attr1

Soit en écriture abrégée :

racine/item/attribute::attr1

Cet axe étant, avec l'axe child un des plus utilisé, il existe un raccourci : il s'agit de @ qui veut dire la même chose que attribute::. L'expression précédente devient alors :

racine/item/@attr1

Il est également possible d'utiliser le joker * pour sélectionner tous les attributs. On écrit alors attribute::* ou sous forme raccourcie @*.

Dans le document suivant, on cherche à désigner tous les attributs des éléments contenus dans un item :

Il faut donc utiliser deux fois le joker :

racine/item/*/@*

Soit en français, et en lisant en sens inverse : tous les attributs de tous les éléments fils d'un élément item lui-même fils d'un élément racine. Lire les expressions XPath en partant de la fin les rendent souvent plus simples à comprendre !

Axe descendant

L'axe descendant désigne tous les éléments qui sont les enfants, les petits enfants, etc de l'élément courant.

Ainsi, dans le document XML suivant :

pour sélectionner tous les éléments item à partir de l'élément racine, on écrira :

racine/descendant::item

Il y avait bien 7 éléments item sous l'élément racine dans notre fichier de départ et il y a bien 7 nœuds dans le résultat.

On peut également utiliser le joker. Pour sélectionner tous les éléments sous l'élément racine, on écrira :

racine/descendant::*

Tous les éléments sous l'élément racine apparaissent.

Axe descendant-or-self

L'axe descendant-or-self est quasiment identique à l'axe descendant, sauf qu'il peut aussi sélectionner le nœud courant.

Exemple sans et avec le joker avec ce document :

racine/item/list/descendant-or-self::item

-- NODE -- -- NODE -- -- NODE -- ]]>

Avec le joker, on attrape l'élément courant en plus :

racine/item/list/descendant-or-self::*

-- NODE -- -- NODE -- -- NODE -- -- NODE -- ]]>

Axe ancestor

À l'opposé, l'axe ancestor contient l'élément parent du nœud courant, le parent de l'élément parent et ainsi de suite. Prenons le document XML suivant :

Blablabla Blablabla Blablabla Blablabla ]]>

Pour sélectionner les ancêtres item de l'élément a, on écrira :

racine/item/list/a/ancestor::item

Blablabla Blablabla Blablabla Blablabla ]]>

Et pour sélectionner tous les ancêtres de ce même élément :

racine/item/list/a/ancestor::*

Blablabla Blablabla Blablabla Blablabla -- NODE -- Blablabla Blablabla Blablabla Blablabla -- NODE -- Blablabla Blablabla Blablabla Blablabla ]]>

Ici les trois ancêtres de a sont bien présent, du père de l'élément jusqu'à l'élément racine.

Axe ancestor-or-self

L'axe ancestor-or-self est quasiment identique, il inclut en plus le nœud courant dans les nœuds résultat possibles. Ainsi, en reprenant l'exemple précédent cela donne :

Blablabla Blablabla Blablabla Blablabla ]]>

racine/item/list/a/ancestor-or-self::*

Blablabla Blablabla Blablabla Blablabla -- NODE -- Blablabla Blablabla Blablabla Blablabla -- NODE -- Blablabla Blablabla Blablabla Blablabla -- NODE -- Blablabla]]>

On obtient bien les trois ancêtres, plus le nœud courant pour l'expression ancestor-or-self::*

Axe following-sibling

En plus de pouvoir avoir un père et des fils, un élément XML peut aussi avoir des frères. Que sont les frères pour un élément XML ? Ce sont les éléments qui sont au même niveau dans l'arbre XML, donc qui ont le même père. Ainsi dans le document suivant :

Blablabla Blebleble Bliblibli Blobloblo Blublublu Blyblybly ]]>

les deux item sont frères, les éléments a, b, c et d sont frères et enfin e et f le sont. L'élément racine n'a évidemment pas de frère puisqu'il est unique !

Pour sélectionner les frères suivants, on utilisera l'axe following-sibling. Ainsi, pour désigner les nœuds suivant b, qui sont c et d, on écrira :

racine/item/b/following-sibling::*

Bliblibli -- NODE -- Blobloblo]]>

Si on avait voulu sélectionner seulement les frères suivant d, on aurait écrit :

racine/item/b/following-sibling::d

Axe preceding-sibling

L'axe preceding-sibling sélectionne quand à lui les frères précédents. Exemple :

Blablabla Blebleble Bliblibli Blobloblo Blublublu Blyblybly ]]>

racine/item/d/preceding-sibling::*

Blablabla -- NODE -- Blebleble -- NODE -- Bliblibli]]>

Axe following

Dans la norme XPath, les nœuds d'un document XML sont parcourus dans un ordre bien précis. En ce qui concerne les éléments, il s'agit d'un parcours profondeur de l'arbre XML. Plus précisément, il suffit pour avoir l'ordre de parcourir le document XML du début en suivant les tags ouvrant.

Comme un petit document XML vaut mieux qu'un long discours, voici un exemple (suivez l'odre alphabétique !) :

]]>

L'axe following désigne tous les nœuds qui suivent le nœud courant dans le document XML, sans les éléments descendant. Exemple :

racine/descendant::k/following::*

-- NODE -- -- NODE -- -- NODE --

-- NODE --

-- NODE -- -- NODE -- ]]>

Bien sûr le joker n'est pas obligatoire, on peut spécifier le nom d'un élément.

Axe preceding

Vous l'aurez deviné, l'axe preceding fait l'inverse de l'axe following. Il sélectionne tous les nœuds qui sont avant le nœud courant dans l'ordre d'apparition des nœuds dans le document, mais cette fois ci sans les ancêtres. Et, logiquement, sans les descendants puisqu'ils arrivent après dans l'ordre du document. Reprenons l'exemple précédent :

racine/descendant::l/preceding::*

-- NODE -- -- NODE -- -- NODE -- -- NODE -- -- NODE -- -- NODE -- -- NODE -- -- NODE -- -- NODE -- ]]>

Vous remarquerez qu'il n'y a ni les ancêtres (i et racine manquent) ni les enfants.

Axe namespace

TODO

Types de nœud

Chaque axe a un type de nœud primaire. Pour quasiment tous les axes, le type de nœud primaire est l'élément. Il y a deux exceptions : l'axe namespace que nous laisserons de côté et l'axe attribute dont le type de nœud primaire est l'attribut et non pas l'élément.

Qu'est ce que cela implique-t-il ? Souvenez vous, une étape de localisation est de la forme : axe::test. Nous avons vu les test nommés (on donne le nom du nœud à sélectionner, child::para par exemple ou attribute::href) et le joker *.

Et bien le type de nœud primaire est impliqué dans le type des nœuds qui seront sélectionnés par ces tests : les nœuds sélectionnés ne seront que du type primaire de l'axe.

Un petit exemple pour illustrer la chose. Lorsqu'on écrit child::*, seuls les éléments fils seront sélectionnés. Exit donc les commentaires, les nœuds textuels, etc. Dans l'exemple suivant, vous voyez bien que seuls l'élément est sélectionné. Pas les nœuds textuels (au nombre de 3), ni le commentaire.

abc def efg hij

]]>

L'élément p a bien trois nœuds textuels comme fils :

Utilisons le joker pour récupérer les fils de l'élément p :

  • "abc "
  • " efg "
  • " hij"
def]]>

Seul l'unique élément a été sélectionné.

Il existe évidemment des tests de nœud qui permettent de récupérer tous les types possibles. Le premier d'entre permet de récupérer les nœuds textuels : c'est text(). Ainsi, pour récupérer les nœuds textuels de l'élément p, on remplace le joker par text(). Cela donne :

root/p/text()

qui est équivalent à

root/p/child::text()

puisqu'en l'absence d'axe explicite, l'axe par défaut est l'axe child.

Remarquez comme les espaces ont été conservés.

Pour les commentaires, c'est la même chose sauf que le test s'appelle comment(). Récupérons le commentaire de l'exemple précédent :

root/p/comment()

abc def efg hij

]]>
]]>

Mais bon, comme vous ne devriez jamais avoir de données utiles dans les commentaires (vraiment !), ça ne vous servira pas souvent.

C'est encore pareil pour les instructions de traitement (PI) avec le test processing-instruction() ou processing-instruction("cible"). Exemple :

]]>
-- NODE -- ]]>
]]>

Enfin, le dernier test de nœud, le plus utile sans doute, permet de sélectionner tous les nœuds d'un axe, quelquesoit le type. C'est le test node(). Soit le document suivant qui comporte a peu près tous les types de nœuds que l'on peut rencontrer dans un document XML :

Du texte ! Un élément !

]]>

L'utilisation du test node() permet de récupérer tous les fils (les $ marquent les retours à la ligne) :

Un M-ilM-iment !$ -- NODE -- $ $ -- NODE -- $ -- NODE -- $ $ -- NODE -- $ -- NODE -- $ $]]>

Apprenez a bien compter les nœuds textuels. Ici, il y en a 4. Les espaces, tabulations et retour à la ligne comptent aussi !

Tous les fils ont été sélectionnés. Contrairement à l'attribut, mais c'est normal puisque,rappel, l'attribut n'est pas le fils de l'élément qui le porte !

Parfois, vous aurez besoin de récupérer des nœuds de type différent. Par exemple, tous les éléments et tous les commentaires. Impossible de faire ça avec ce que nous avons vu jusqu'ici. Ou alors vous aurez besoin de rassembler en un seul ensemble de nœuds des éléments que vous ne pouvez sélectionner en une expression. Dans ce cas, on utilise l'opérateur d'union | qui rassemble deux ou plusieurs ensembles de nœuds en un seul.

Prenons un cas concret : en Xslt, pour faire une copie conforme d'un nœud, il faut copier tous ses fils et tous ses attributs. On utilise l'opérateur d'union pour rassembler les deux ensemble de nœud comme ceci :

child::node()|attribute::*

qui peut se réécrire :

node()|@*

Exemple avec le fichier XML suivant :

On utilise l'expression XPath suivante :

root/p/node()|root/p/@*

$ -- NODE -- $ $ -- NODE -- $ -- NODE -- $ $ -- NODE -- $ -- NODE -- $ $]]>