Une petite précision sur l'auteur de cet article. Nous sommes Geonode et nous vendons des proxys à des personnes qui extraient des données ; XPath est donc un domaine connexe à notre activité, mais n'en fait pas partie intégrante. Il est important de préciser qu’un problème de sélecteur et un problème de proxy n’ont absolument rien à voir l’un avec l’autre, et les confondre fait perdre des heures : une requête bloquée renvoie une page de vérification ou un statut d’erreur, tandis qu’une expression erronée renvoie une page parfaitement valide et un résultat vide. Si le code HTML est bien présent et que votre XPath ne trouve rien, cela signifie que le réseau fonctionne correctement et que vous êtes sur la bonne page.
Syntaxe de base
| Expression | Sélectionne |
|---|---|
/html/body/div | Chemin absolu à partir de la racine |
//div | N'importe quelle édiv, où qu'elle se trouve dans le document |
//div/p | Éléments p qui sont des enfants directs d'un div |
//div//p | Éléments p à n'importe quelle profondeur à l'intérieur d'un div |
. | Le nœud de contexte actuel |
.. | Le nœud parent du nœud de contexte |
* | N’importe quel élément |
@href | L’attribut « href » |
//@href | Tous les attributs « href » du document |
text() | Les nœuds de texte enfants du nœud de contexte |
node() | N’importe quel nœud, y compris les nœuds de texte et les commentaires |
//a | //link | Union — tout ce qui correspond à l’une ou l’autre expression |
Il est essentiel de bien assimiler la distinction entre / et //. Une barre oblique simple signifie « enfant direct » ; une double barre oblique signifie « descendant à n’importe quelle profondeur ». //div/p ne trouve pas un paragraphe contenu dans une balise section ; //div//p le trouve.
Les chemins absolus — /html/body/div[2]/div[1]/span — sont ceux générés par la fonction « Copier le XPath » du navigateur et ceux qui ne fonctionneront plus lors de la prochaine refonte. Préférez partir d’un élément identifiable et naviguer de manière relative.
Prédicats
Les crochets permettent de filtrer un ensemble de nœuds. C'est là que se déroule l'essentiel du travail utile.
| Expression | Sélectionne |
|---|---|
//div[1] | Le premier div parmi ses frères et sœurs, par parent |
(//div)[1] | Le premier div dans l'ensemble du document |
//div[last()] | Le dernier div parmi ses frères et sœurs |
//div[position() < 4] | Les trois premiers |
//a[@href] | Liens comportant un attribut « href » |
//a[@href='/about'] | Liens comportant exactement cette valeur « href » |
//div[@class and @id] | Éléments comportant les deux attributs |
//p[text()] | Paragraphes comportant au moins un enfant de type « text-node » |
//div[p] | Éléments « div » contenant au moins un enfant de type « p » |
//div[not(@hidden)] | Éléments « div » ne comportant pas d’attribut « hidden » |
//td[.='42'][@class='qty'] | Deux prédicats, appliqués successivement |
** La confusion entre «//div[1] » et « (//div)[1] » est la plus courante en XPath.** Le premier est un prédicat appliqué par parent ; il sélectionne donc le premier « div » sous chaque parent qui en possède un — ce qui peut potentiellement correspondre à de nombreux nœuds. Le second rassemble tous les éléments div dans un ensemble de nœuds selon l’ordre du document et prend le premier, ce qui correspond exactement à un seul nœud. Les deux sont utiles ; ils ne sont pas interchangeables.
Les prédicats s’enchaînent, et chacun s’applique au résultat du précédent. //td[@class='price'][1] signifie « le premier parmi les éléments cell ayant la classe price », en lisant de gauche à droite.
Axes
Les axes permettent de naviguer par rapport au nœud de contexte. La plupart des utilisateurs en utilisent trois et ont parfois besoin des autres.
| Axe | Sélectionne |
|---|---|
child:: | Enfants directs — valeur par défaut, généralement omise |
descendant:: | Tous les descendants, quelle que soit leur profondeur |
parent:: | Le parent |
ancestor:: | Tous les ancêtres jusqu’à la racine |
ancestor-or-self:: | Ancêtres plus le nœud lui-même |
following-sibling:: | Frères et sœurs postérieurs |
preceding-sibling:: | Frères et sœurs antérieurs |
following:: | Tout ce qui suit dans l’ordre du document, à l’exception des descendants |
preceding:: | Tout ce qui précède, à l’exclusion des ancêtres |
attribute:: | Attributs — abrégés par « @ » |
self:: | Le nœud lui-même |
Quatre d’entre eux sont des axes inversés — ancestor, ancestor-or-self, preceding et preceding-sibling — et sur ceux-ci, la numérotation des positions s’effectue à rebours. preceding-sibling::p[1] correspond au paragraphe précédent le plus proche, et non au premier du document. Utilisez des parenthèses pour obtenir l’ordre du document à la place. Nous avons abordé ce sujet en détail dans XPath preceding-sibling.
following et preceding ont une portée beaucoup plus large et sont beaucoup plus lents que leurs équivalents « sibling », et ils excluent respectivement les ancêtres et les descendants. Ne les utilisez que lorsque la relation est véritablement lâche.
Fonctions de chaînes de caractères
Les incontournables.
| Fonction | Description |
|---|---|
contains(a, b) | Vraie si a contient b en sous-chaîne |
starts-with(a, b) | Vraie si a commence par b |
normalize-space(s) | Supprime et regroupe les espaces internes |
string-length(s) | Compte le nombre de caractères |
substring(s, start, len) | Sous-chaîne, indexée à partir de 1 |
substring-before(a, b) | Tout ce qui précède la première occurrence de b |
substring-after(a, b) | Tout ce qui suit la première occurrence de b |
translate(s, from, to) | Remplacement caractère par caractère |
concat(a, b, ...) | Concatène des chaînes |
string(node-set) | Valeur sous forme de chaîne du premier nœud uniquement |
Trois remarques permettant d’éviter de véritables bogues.
substring() utilise un indexation à partir de 1. substring('hello', 1, 3) renvoie hel. Tout le monde se trompe là-dessus au moins une fois.
normalize-space() devrait être votre wrapper par défaut pour toute comparaison de texte. Le vrai HTML est formaté, donc la valeur sous forme de chaîne d’une cellule est souvent "\n In stock\n" plutôt que "In stock". Sans argument, il opère sur le nœud de contexte.
Il n’y a ni ends-with(), ni lower-case(), ni d’expressions régulières dans XPath 1.0. Pour ignorer la casse, translate() avec des alphabets explicites est la solution de contournement standard. Les bibliothèques côté serveur telles que lxml prennent en charge les extensions EXSLT, notamment re:test() ; ce n’est pas le cas des navigateurs et de Selenium.
Fonctions numériques et booléennes
| Fonction | Fonction |
|---|---|
count(node-set) | Nombre de nœuds |
position() | Position du nœud contextuel |
last() | Taille de l'ensemble des nœuds contextuels |
number(s) | Convertit en nombre |
sum(node-set) | Additionne les valeurs numériques |
round(), floor(), ceiling() | Comme indiqué |
not(expr) | Négation booléenne |
boolean(expr) | Convertit en booléen |
true(), false() | Booléens littéraux |
Les opérateurs de comparaison sont =, !=, <, >, <=, >=, avec and et or pour les combinaisons. Notez que dans les contextes XML, < doit être échappé sous la forme <, ce qui explique pourquoi on voit parfois des expressions écrites avec position() < 4.
Une subtilité qu’il est bon de connaître : comparer un ensemble de nœuds à une valeur est un test existentiel. //p = 'Price' est vrai si n’importe quel paragraphe est égal à « Price ». C’est souvent ce que l’on souhaite à l’intérieur d’un prédicat, mais presque jamais au niveau supérieur.
Modèles récurrents
Les expressions qui font vraiment le travail.
Correspondre correctement à une classe — l'contains(@class, 'btn')
simple correspond également à btn-primary
et unbtn
:
//div[contains(concat(' ', normalize-space(@class), ' '), ' btn ')]
Rechercher une valeur par son libellé — la demande d'extraction la plus courante qui soit :
//dt[normalize-space()='Price']/following-sibling::dd[1]
//th[normalize-space()='Weight']/following-sibling::td[1]
//td[preceding-sibling::td[1]='SKU']
Rechercher une ligne à partir d'une cellule, puis extraire une autre colonne :
//tr[td[normalize-space()='SKU-1234']]/td[3]
Rechercher un conteneur à partir d’un élément qu’il contient :
//div[contains(@class,'card')][.//span[contains(., 'Sold out')]]
Rechercher le premier élément après un en-tête :
//h2[normalize-space()='Specifications']/following-sibling::table[1]
Correspondance de texte insensible à la casse :
//p[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'abcdefghijklmnopqrstuvwxyz'), 'price')]
Exclure plutôt qu’inclure — souvent plus clair :
//tr[not(contains(@class,'header'))]
//li[not(contains(normalize-space(),'Advertisement'))]
Correspondance avec l’un ou l’autre de deux types d’éléments :
//*[self::button or self::a][normalize-space()='Continue']
Ignorer les valeurs vides :
//td[string-length(normalize-space()) > 0]
Extraire un attribut d’un élément correspondant :
//a[normalize-space()='Download']/@href
Les pièges
Classés en fonction de la fréquence à laquelle ils font perdre du temps aux utilisateurs.
La conversion d’un ensemble de nœuds en chaîne de caractères ne prend en compte que le premier nœud. contains(//p, 'Price') convertit l’ensemble des nœuds //p en une chaîne de caractères en ne retenant que le premier paragraphe et en ignorant le reste. Appliquez plutôt le prédicat à chaque nœud : //p[contains(., 'Price')]. Il s’agit là de l’erreur XPath la plus courante.
. et text() sont différents. La valeur sous forme de chaîne d’un élément correspond à la concaténation de tous ses nœuds de texte descendants ; text() ne renvoie que les nœuds de texte enfants directs, et la conversion en chaîne ne retient que le premier d’entre eux. Utilisez . sauf si vous souhaitez spécifiquement exclure le contenu imbriqué.
Numérotation à axe inversé. preceding-sibling::td[1] correspond au plus proche, et non au premier.
Espaces. //td[.='In stock'] échoue avec du code HTML formaté. normalize-space() résout ce problème.
Sensibilité à la casse. Tout dans XPath 1.0 est sensible à la casse, y compris les noms d’éléments dans les documents XML.
Espaces de noms par défaut. Dans un XML comportant un espace de noms par défaut, //item ne correspond à rien — vous devez enregistrer un préfixe et l’utiliser. Les analyseurs HTML vous épargnent généralement cette étape ; ce n’est pas le cas des analyseurs XML.
Chemins absolus issus des outils de développement du navigateur. Ils codent la structure exacte à un instant donné et cessent de fonctionner dès qu’un changement intervient.
Correspondance excessive avec « contains() ». La correspondance avec « Price » correspond également à « Historic Price ». Utilisez « normalize-space() = 'Price' » lorsque vous souhaitez une égalité.
Injection de chaînes non fiables. L’interpolation des entrées utilisateur dans une expression constitue une injection XPath. Utilisez la liaison de variables lorsque votre bibliothèque le permet. XPath 1.0 ne prévoit pas d’échappement pour les guillemets à l’intérieur d’un littéral de chaîne ; une valeur contenant les deux caractères de guillemet nécessite donc l’utilisation de concat().
XPath 1.0 par rapport aux versions ultérieures
Bon à savoir, car un extrait de code trouvé en ligne risque de ne pas fonctionner là où vous en avez besoin.
XPath 1.0 est la version implémentée par les navigateurs via document.evaluate, celle utilisée par Selenium, et celle fournie par l'API standard de lxml. Il dispose des fonctions énumérées ci-dessus, et rien de plus.
XPath 2.0 et 3.1 ajoutent les expressions régulières (matches(), replace()), les fonctions de cas (upper-case(), lower-case()), les expressions de type « ends-with() », les types de séquence, les expressions de type « for » et bien d’autres encore. Elles sont disponibles dans les processeurs XSLT 2.0 et versions ultérieures ainsi que dans certains outils XML, mais ne le sont pas dans les navigateurs.
Règle pratique : si une expression utilise une fonction qui ne figure pas dans les tableaux ci-dessus, vérifiez si votre environnement la prend en charge avant de chercher à comprendre pourquoi elle échoue. « Cela fonctionne dans un testeur XPath en ligne, mais pas dans Selenium » : c’est presque toujours la raison.
Pour combler les lacunes de XPath 1.0, la solution réside généralement dans votre langage hôte. Extrayez les données avec XPath, puis appliquez une expression régulière en Python ou en JavaScript, où vous pourrez également inspecter les éléments correspondants.
Écrire des sélecteurs qui résistent à une refonte
Une fiche pratique vous indique ce qui est possible ; cette section explique quelles options choisir parmi celles-ci, car la différence entre un sélecteur qui dure un an et un autre qui ne fonctionnera plus dès mardi prochain dépend entièrement de ce sur quoi vous vous basez.
Ancrer sur le sens, pas sur la position. (//table)[3]/tr[2]/td[4] encode la forme exacte de la page à un moment donné. Si quelqu’un ajoute un tableau au-dessus, tous les chiffres sont erronés — sans que cela se remarque, car l’expression correspond toujours à quelque chose. //th[normalize-space()='Weight']/following-sibling::td[1] encode une relation qui résiste au réagencement, car l’étiquette se déplace avec la valeur.
Privilégiez les attributs stables aux attributs générés. Les identifiants et les attributs « data-* » choisis par un développeur sont bien plus durables que les noms de classes, qui changent dès que quelqu’un modifie la mise en forme. De nombreux frameworks front-end modernes génèrent des noms de classes hachés — css-1x9dj2k — qui changent à chaque compilation ; s’ancrer sur ceux-ci garantit des dysfonctionnements.
Vérifiez la présence de données structurées intégrées avant d’écrire le moindre sélecteur. Un très grand nombre de pages contiennent du JSON-LD dans un bloc <script type="application/ld+json">, car cela alimente les fonctionnalités de recherche. L’analyse de ces données est nettement plus stable que celle du HTML rendu, puisqu’elles sont conçues pour être lues par des machines et résistent entièrement aux refontes visuelles. Trente secondes passées à vérifier cela peuvent vous épargner un après-midi de maintenance des sélecteurs.
Vérifiez la structure de ce que vous extrayez. C’est cette habitude qui distingue un pipeline qui échoue de manière flagrante de celui qui échoue silencieusement. Si un prix doit respecter un format de devise, vérifiez-le. Si une page de catégorie n’a jamais comporté moins de vingt articles, considérez qu’un nombre inférieur à vingt est une erreur plutôt qu’un résultat. Un sélecteur qui commence à correspondre au mauvais élément produit des données plausibles, bien formées, mais erronées — et aucune exception n’est levée à aucun moment.
Regroupez les sélecteurs au même endroit. Dispersées dans une base de code, quarante chaînes XPath représentent quarante sources de problèmes de maintenance distinctes. Regroupées dans un seul module avec des noms, elles constituent une carte de vos dépendances, et leur mise à jour après une refonte ne prend qu’une heure au lieu d’une journée.
Et effectuez vos tests par rapport au code HTML enregistré. En stockant une copie de chaque page que vous analysez, vous pouvez, lorsqu’un sélecteur ne fonctionne plus, comparer l’ancien balisage au nouveau et voir exactement ce qui a changé. Une nouvelle requête pour le débogage est plus lente, consomme de la bande passante et peut vous renvoyer une page différente de celle qui a posé problème.
Quand utiliser plutôt le CSS
XPath est plus puissant mais moins lisible. Le CSS est le choix par défaut recommandé pour une grande partie des opérations de sélection.
Utilisez le CSS lorsque : vous effectuez une sélection par classe, identifiant, attribut ou relation de descendance. div.card > p.price est plus clair que l’équivalent XPath, mieux pris en charge par les outils et généralement plus rapide.
Utilisez XPath lorsque : vous devez effectuer une correspondance sur du contenu textuel, ce que le CSS ne permet absolument pas ; vous devez accéder à un parent ou à un ancêtre ; vous avez besoin d’une logique de position par rapport à des éléments frères d’une manière qu’:nth-child ne peut pas exprimer ; ou vous interrogez du XML plutôt que du HTML.
Notez que le CSS a comblé une partie de ce fossé. La propriété :has() permet une sélection conditionnelle sur les éléments frères et les descendants dans les navigateurs modernes ; ainsi, dt:has(+ dd) est désormais exprimable. Ce que le CSS ne peut toujours pas faire, c’est sélectionner par texte, et la correspondance de texte est précisément ce qu’exige le modèle « étiquette-valeur ».
Mélanger les deux dans une même base de code est tout à fait acceptable et judicieux : le CSS pour les 90 % de cas simples, l’XPath pour les 10 % de cas complexes.
Questions fréquentes
À quoi sert XPath ?
À parcourir et sélectionner des nœuds dans des documents XML et HTML. Concrètement, il est utilisé pour le web scraping, l’automatisation des tests de navigateurs et l’interrogation de fichiers de configuration et de données XML. Il permet d’exprimer des relations — parents, frères et sœurs, contenu textuel — que les sélecteurs CSS ne peuvent pas rendre.
Quelle est la différence entre / et // en XPath ?
Une barre oblique simple sélectionne les enfants directs ; une double barre oblique sélectionne les descendants à n'importe quelle profondeur. //div/p correspond aux paragraphes dont le parent immédiat est une balise div, tandis que //div//p correspond aux paragraphes situés n'importe où à l'intérieur d'une balise div.
Pourquoi mon expression XPath contains() ne renvoie-t-elle aucun résultat ?
Le plus souvent, c’est parce que vous lui avez transmis un ensemble de nœuds. XPath convertit un ensemble de nœuds en chaîne de caractères en prenant le premier nœud dans l’ordre du document et en ignorant les autres ; ainsi, contains(//p, 'x') n’inspecte jamais que le premier paragraphe. Écrivez plutôt //p[contains(., 'x')].
Comment sélectionner par classe dans XPath ?
Utilisez //div[contains(concat(' ', normalize-space(@class), ' '), ' name ')], qui ajoute des caractères de remplissage à l’attribut afin que seuls les tokens entiers correspondent. Un simple contains(@class, 'btn') correspond également à btn-primary. Si vous effectuez une sélection uniquement sur des classes, un sélecteur CSS est plus clair et correct par défaut.
XPath prend-il en charge les expressions régulières ?
Pas dans XPath 1.0, qui est la version implémentée par les navigateurs et Selenium. XPath 2.0 et les versions ultérieures ajoutent matches() et replace(), et les bibliothèques côté serveur telles que lxml prennent en charge re:test() d’EXSLT. Pour l’automatisation des navigateurs, extrayez les données avec XPath et appliquez l’expression régulière dans votre langage hôte.
Quelle est la différence entre //div[1] et (//div)[1] ?
//div[1] applique le prédicat à chaque élément parent, en sélectionnant la première balise div sous chaque élément parent qui en contient une — voire plusieurs. (//div)[1] rassemble toutes les balises div dans l’ordre du document et prend la première, ce qui correspond exactement à un nœud.
XPath est-il sensible à la casse ?
Oui, dans tous les cas : noms d’éléments, noms d’attributs et comparaisons de chaînes de caractères. XPath 1.0 ne dispose pas de fonction «lower-case() », donc une correspondance insensible à la casse nécessite l’utilisation de «translate()» avec des lettres majuscules et minuscules explicites.
Dois-je utiliser les sélecteurs XPath ou CSS ?
Utilisez le CSS pour les classes, les identifiants, les attributs et les relations de descendance : il est plus clair et mieux pris en charge. Utilisez XPath lorsque vous devez effectuer une correspondance sur du contenu textuel, naviguer vers des ancêtres ou exprimer une logique positionnelle que le CSS ne permet pas. Il est courant d’utiliser les deux dans une même base de code.
En conclusion
Le cœur utile d’XPath est restreint : « // » pour effectuer une recherche n’importe où, des prédicats entre crochets pour filtrer, « @ » pour les attributs, une poignée de fonctions de chaîne de caractères, et les axes « sibling » pour naviguer par rapport à un élément identifiable.
Trois bonnes habitudes permettent d’éviter la plupart des difficultés. Encadrez les comparaisons de texte entre normalize-space(), car le code HTML réel est formaté et une correspondance exacte échouera. Appliquez contains() à l’intérieur d’un prédicat afin qu’il s’évalue par nœud, car la conversion en chaîne d’un ensemble de nœuds ne retient silencieusement que le premier. Et privilégiez l’ancrage sur du texte ou des identifiants plutôt que sur une position, car la structure change tandis que le texte reste généralement inchangé.
Enfin, n’oubliez pas pour quelle version vous écrivez. Les navigateurs et Selenium vous fournissent XPath 1.0 — pas d’expressions régulières, pas de lower-case(), pas de ends-with() — et une expression qui fonctionne dans un testeur en ligne peut ne pas utiliser ces éléments et échouer malgré tout pour une raison mentionnée plus bas dans la liste. Lorsque vous avez besoin de fonctionnalités qui manquent à la version 1.0, extrayez les données avec XPath et effectuez le reste dans votre langage hôte.
