Notre enjeu, en termes clairs : nous sommes Geonode et nous vendons des proxys à des personnes qui pratiquent le scraping ; XPath est donc étroitement lié à notre activité. Il convient de préciser qu’un sélecteur défaillant n’est jamais un problème lié au proxy. Si votre extraction ne fonctionne plus, c’est que le site a modifié son balisage, et aucune quantité de bande passante ni aucune rotation d’adresses ne permet de résoudre ce problème. On confond souvent ces deux cas, car ils se traduisent tous deux par « mon scraper ne renvoie plus de données » — mais une défaillance du proxy génère des réponses bloquées et des pages d’erreur, tandis qu’une défaillance du sélecteur produit des réponses réussies qui ne renvoient aucune donnée. Vérifiez le code HTML brut avant toute autre chose. Si la page est bien présente et que votre XPath renvoie un ensemble de nœuds vide, cet article s’applique à votre cas et votre proxy fonctionne correctement.
Ce que sélectionne réellement l’axe « preceding-sibling »
La spécification XPath 1.0 le définit en une seule ligne : l’axe « preceding-sibling » « contient tous les frères et sœurs précédents du nœud de contexte ; si le nœud de contexte est un nœud d’attribut ou un nœud d’espace de noms, l’axe « preceding-sibling » est vide ».
Deux mots en résument le sens. Frères et sœurs désigne les nœuds partageant le même parent — pas les cousins, ni les ancêtres, ni aucun élément situé à un niveau différent. Précédents signifie « antérieurs » dans l’ordre du document.
<div>
<p>First</p>
<p>Second</p>
<span id="here">Context</span>
<p>Third</p>
</div>
En prenant l’span comme nœud de contexte :
preceding-sibling::p → First, Second
following-sibling::p → Third
preceding-sibling::* → both p elements
Il est utile de garder à l’esprit cette précision concernant les nœuds d’attribut, car elle explique une catégorie de résultats vides. Si vous avez navigué jusqu’à un attribut — //@class —, alors l’axe « preceding-sibling » à partir de là est vide par définition, quel que soit ce qui entoure l’élément auquel appartient l’attribut. Les attributs n’ont de frères et sœurs avec aucun autre élément.
Le piège des axes inversés : « [1]
» ne signifie pas « premier » Il s’agit là de la source la plus courante de résultats erronés, et la spécification explique précisément pourquoi.
XPath classe ancestor
, ancestor-or-self
, preceding
et preceding-sibling
parmi les axes inversés. Pour les axes inversés, la position de proximité est déterminée en classant les nœuds dans l’ordre inversé du document.
Ainsi, sur preceding-sibling
, la position 1 correspond au frère le plus proche en amont — celui qui précède immédiatement le nœud de contexte — et non au premier dans le document.
<div>
<p>Alpha</p>
<p>Beta</p>
<p>Gamma</p>
<span id="here">Context</span>
</div>
preceding-sibling::p[1] → Gamma (nearest)
preceding-sibling::p[2] → Beta
preceding-sibling::p[3] → Alpha (furthest)
(preceding-sibling::p)[1] → Alpha (first in document order)
Les parenthèses changent tout. Sans elles, [1]
est un prédicat appliqué le long de l’axe inverse et signifie « le plus proche ». Avec elles, le résultat de l’axe est d’abord rassemblé dans un ensemble de nœuds dans l’ordre du document et [1]
indexe dans cet ensemble, ce qui signifie « premier ».
Comparez avec following-sibling
, qui est un axe avant où les deux coïncident :
following-sibling::p[1] → the next one
(following-sibling::p)[1] → also the next one
C’est cette asymétrie qui explique pourquoi les personnes ayant appris sur following-sibling
se font piéger par preceding-sibling
. L’habitude se transpose, mais pas la sémantique.
En pratique, l’absence de parenthèses correspond presque toujours à ce que vous recherchez. « L’étiquette située immédiatement avant cette valeur » est l’exigence habituelle, et cela correspond à preceding-sibling::label[1]
. N’utilisez des parenthèses que lorsque vous voulez véritablement dire « la première dans le document », ce qui est un besoin plus rare qu’il n’y paraît.
« preceding-sibling » (nœuds contextuels) et « preceding » (nœuds contextuels)
Il s’agit de deux concepts distincts dont les noms se ressemblent à s’y méprendre, et cette distinction est importante.
La spécification définit « preceding » comme contenant « tous les nœuds du même document que le nœud contexte qui se trouvent avant ce dernier dans l’ordre du document, à l’exclusion de tout ancêtre, ainsi que des nœuds d’attribut et des nœuds d’espace de noms ».
Ainsi, « preceding » correspond à tout ce qui se trouve en amont dans le document, à n’importe quelle profondeur, à l’exception des ancêtres. « preceding-sibling » ne concerne que les nœuds partageant le même parent.
<body>
<header><h1>Title</h1></header>
<div>
<p>One</p>
<span id="here">Context</span>
</div>
</body>
Extrait de l’span :
preceding-sibling::* → the p only
preceding::* → the p, the h1, and the header
C’est l’exclusion des ancêtres qui surprend le plus. L’div contenant la balise span ne figure pas dans preceding, même si sa balise d’ouverture apparaît plus tôt dans le code source. Les ancêtres sont exclus par définition, car l’axe porte sur ce qui précède le nœud en question, et non sur ce qui le contient.
Quand utiliser quoi. preceding-sibling pour les relations structurées — une étiquette et sa valeur, un titre et le paragraphe qui suit, les cellules d’une ligne. preceding pour les relations véritablement lâches, telles que « le titre le plus proche situé n’importe où au-dessus de cet élément, quel que soit le niveau d’imbrication ». preceding a une portée plus large, est considérablement plus lent et a beaucoup plus de chances de trouver des correspondances que vous ne souhaitiez pas.
Le modèle « étiquette-valeur »
C’est la raison pour laquelle le modèle « étiquette-valeur » (preceding-sibling
) existe dans le domaine du scraping, et il vaut la peine de bien le maîtriser, car la plupart des balisages réels en sont une variante.
Le problème : vous recherchez une valeur qui n’est identifiable que par l’étiquette qui se trouve à côté d’elle. La valeur elle-même ne possède aucune classe utile, aucun identifiant (id), rien qui la distingue.
Listes de définitions :
<dl>
<dt>Price</dt>
<dd>£42.00</dd>
<dt>Stock</dt>
<dd>In stock</dd>
</dl>
Sélectionner le prix revient à trouver l’dd
dont l’dt
immédiatement précédente indique « Prix » :
//dd[preceding-sibling::dt[1] = 'Price']
Lisez cela à l’envers : pour chaque dd
, prenez son dt
immédiatement précédent ; conservez l’dd
si ce texte est « Prix ». Notez que l’[1]
est essentielle. Sans elle, preceding-sibling::dt = 'Price'
est vrai si n’importe quelle dt
précédente correspond, ce qui ferait également correspondre la deuxième dd
. Il s’agit là d’un bug réel et fréquent.
Cellules de tableau :
<tr>
<td>SKU</td>
<td>ABC-123</td>
</tr>
//td[preceding-sibling::td[1] = 'SKU']
Ou à partir de la colonne d’en-tête sur une ligne d’un tableau de spécifications à deux colonnes :
//th[normalize-space() = 'Weight']/following-sibling::td[1]
Cette deuxième forme est généralement préférable lorsque l’étiquette est une «th
», car elle se lit de gauche à droite et correspond à la structure du tableau.
Améliorations de robustesse permettant à ces expressions de résister à des balises réelles :
//dd[preceding-sibling::dt[1][normalize-space() = 'Price']]
normalize-space()
supprime les espaces internes et tronque les extrémités, ce qui permet de gérer le code HTML « pretty-printed » qui, sinon, fausserait une comparaison exacte de chaînes. C’est la fonction la plus utile pour l’extraction de données via XPath.
Pour les correspondances partielles, contains()
est plus tolérante et, par conséquent, moins précise :
//dd[preceding-sibling::dt[1][contains(., 'Price')]]
Attention : contains(., 'Price')
correspond également à « Prix hors TVA » et « Prix historique ». Si la page comporte plusieurs étiquettes de ce type, vous obtiendrez plusieurs résultats et votre code prendra le premier sans avertissement.
Titres et contenu suivant, qui correspond au même modèle dans l’autre sens :
//h2[normalize-space() = 'Specifications']/following-sibling::table[1]
Le premier tableau après ce titre. Il s’agit d’une exigence très courante sur les pages de documentation et de produits, et il n’existe pas d’équivalent CSS permettant d’exprimer « le premier tableau après ce titre spécifique ».
Combinaison avec des prédicats et des conditions
preceding-sibling
se combine avec le reste de XPath, et il est utile de connaître quelques combinaisons.
Comptage des éléments frères — utile pour trouver le premier ou le dernier élément, ou pour vérifier une position :
//li[count(preceding-sibling::li) = 0] first li
//li[count(preceding-sibling::li) < 3] first three
Vérification de l’existence — un ensemble de nœuds dans un contexte booléen est vrai s’il n’est pas vide :
//p[preceding-sibling::h2] paragraphs with an h2 somewhere before
//p[not(preceding-sibling::p)] first paragraph among its siblings
Enchaînement d’axes :
//span[@class='value']/preceding-sibling::*[1]/text()
L’élément immédiatement précédent, quelle que soit sa balise, et son texte.
Conditions multiples :
//td[preceding-sibling::td[1] = 'Status'][normalize-space() != '']
Deux prédicats appliqués successivement : la cellule située après une étiquette « Status », et non vide.
Remarque sur le raccourci ..
, souvent plus clair qu’un axe :
//dt[.='Price']/following-sibling::dd[1]
est généralement plus lisible que la forme preceding-sibling
pour un résultat identique, et se lit dans le sens de rédaction du document. À privilégier lorsque l’ancre correspond à l’étiquette.
Dans quels cas les sélecteurs CSS peuvent-ils ou ne peuvent-ils pas le remplacer ?
L'idée reçue selon laquelle « le CSS ne peut pas remonter en amont » doit être remise à jour.
Le CSS permet désormais de définir des conditions de fratrie grâce à la propriété :has(). Largement prise en charge par les navigateurs modernes, elle permet à un sélecteur d'être conditionné par une relation de fratrie :
dt:has(+ dd) a dt immediately followed by a dd
li:has(~ li.active) an li with a later sibling that is active
Ce que le CSS ne peut toujours pas faire :
Sélectionner en fonction du contenu textuel. Il n’existe pas d’équivalent CSS de [text() = 'Price']. C’est la seule raison pour laquelle le modèle « étiquette-valeur » reste l’apanage d’XPath, car la correspondance d’étiquettes est une correspondance textuelle.
Accéder à un ancêtre arbitraire. :has() fournit une forme de sélection parentale, mais il n’existe pas d’axe ancestral général.
Indexer le long d’un axe inverse. Aucune construction CSS ne signifie « l’élément précédent le plus proche de ce type ».
Et ce que le CSS fait mieux : tout ce qui est simple. La sélection par classe et par identifiant, les relations de descendance, la correspondance d’attributs. Les sélecteurs CSS sont plus lisibles, mieux pris en charge par les outils et plus rapides dans la plupart des moteurs.
La stratégie la plus judicieuse consiste à utiliser le CSS par défaut et à recourir à XPath uniquement lorsque vous avez besoin d’une correspondance de texte ou d’une navigation en amont. Il est tout à fait possible de les mélanger dans une même base de code, et un scraper qui utilise le CSS pour 90 % de ses sélecteurs et XPath pour les 10 % restants est plus facile à maintenir qu’un scraper qui s’en tient exclusivement à l’un ou à l’autre.
Performances et fragilité
Deux contraintes pratiques.
Performances. La fonction preceding-sibling est limitée par le nombre de nœuds frères, qui est généralement faible — ce qui ne pose pas de problème. La fonction preceding parcourt tout le document jusqu’au nœud de contexte, ce qui est coûteux sur une page volumineuse, et son coût est quadratique lorsqu’elle est utilisée dans une boucle. Si un sélecteur utilisant preceding est lent, c’est presque certainement pour cette raison. La solution habituelle consiste à le réécrire en utilisant preceding-sibling à partir d’un nœud de contexte plus proche.
Fragilité. Les sélecteurs basés sur les éléments frères dépendent de la structure du document, qui est précisément ce que modifie une refonte. preceding-sibling::td[1] cesse de fonctionner sans avertissement dès qu’une colonne est insérée. Aucune erreur n’est signalée ; le sélecteur correspond à une cellule différente et vos données sont discrètement erronées.
Trois mesures d’atténuation réellement efficaces :
Ancrer sur du texte plutôt que sur la position lorsque c’est possible. //dt[.='Price']/following-sibling::dd[1] résiste à un réordonnancement de la liste. (//dd)[3], en revanche, ne le fait pas.
Vérifier ce que vous extrayez. Si un prix doit correspondre à un modèle de devise, vérifiez-le. Un sélecteur qui commence à renvoyer l’état des stocks au lieu du prix passe inaperçu, à moins qu’un élément ne valide le format.
Privilégiez les données structurées intégrées lorsqu’elles existent. Si la page contient du JSON-LD dans un bloc <script type="application/ld+json">, analysez-le à la place. Il est conçu pour être lu par une machine, il est bien plus stable lors des refontes et il élimine toute la catégorie des sélecteurs fragiles. Prendre trente secondes pour vérifier sa présence avant d’écrire un XPath en vaut la peine.
Les défaillances structurelles silencieuses relèvent de la même catégorie de problèmes que celle décrite dans Pourquoi il est important de tester les proxys : la requête aboutit, l’analyse est réussie, mais les données sont erronées.
Quand ne pas l'utiliser
Lorsque l'élément dispose d'un identifiant exploitable. S'il existe un identifiant (id), une classe ou un attribut « data », utilisez-le. Un sélecteur qui repose sur la structure est nettement plus fragile que celui qui repose sur un nom choisi délibérément par le développeur.
Lorsque des données structurées sont disponibles. JSON-LD, les microdonnées, une charge utile JSON derrière une requête XHR. N’importe laquelle de ces solutions est préférable à l’analyse du code HTML rendu.
Lorsque la relation est véritablement lâche. Si vous vous retrouvez à écrire « preceding::*[5] », la structure ne vous apporte pas vraiment d’information et le sélecteur cessera de fonctionner lors du prochain déploiement. Reconsidérez votre approche.
Lorsque le CSS suffit. Pour une sélection simple, le CSS est plus lisible et mieux pris en charge. Réservez XPath à la correspondance de texte et à la navigation en arrière.
Lorsque vous comptez sur les fonctionnalités d’XPath 2.0 dans un navigateur. Les navigateurs implémentent XPath 1.0 via document.evaluate, et Selenium suit le navigateur. Donc pas d’matches()s, pas d’expressions régulières, pas d’upper-case()s, pas de types de séquences. Les bibliothèques côté serveur telles que lxml utilisent également XPath 1.0 pour l’API commune. Si un extrait de code trouvé en ligne ne fonctionne pas, vérifiez s’il utilise une fonction qui n’existe que dans une version ultérieure.
Questions fréquentes
À quoi sert la fonction « preceding-sibling » en XPath ?
Il sélectionne tous les nœuds qui partagent le même parent que le nœud de contexte et qui apparaissent avant celui-ci dans l'ordre du document. Il n'inclut pas les ancêtres, les descendants ni les nœuds situés à d'autres niveaux de l'arborescence — uniquement les nœuds frères. Il est vide si le nœud de contexte est un nœud d'attribut ou d'espace de noms.
Pourquoi « preceding-sibling[1] » renvoie-t-il un élément erroné ?
Parce que « preceding-sibling » est un axe inversé, et que la numérotation des positions sur les axes inversés suit l’ordre inverse du document. « [1] » désigne donc le frère précédent le plus proche, et non le premier dans le document. Pour obtenir le premier dans l’ordre du document, placez l’axe entre parenthèses : « (preceding-sibling::p)[1] ».
Quelle est la différence entre « preceding » et « preceding-sibling » ? «
preceding-sibling » ne couvre que les nœuds ayant le même parent. « preceding » couvre tous les nœuds situés avant dans le document, à n’importe quelle profondeur, à l’exclusion des ancêtres et des nœuds d’attribut. « preceding » a une portée bien plus large, est considérablement plus lent et risque beaucoup plus de faire correspondre des éléments non souhaités.
Comment sélectionner une valeur en fonction de son libellé dans XPath ?
Recherchez le libellé par son texte, puis sélectionnez l’élément adjacent : //dt[normalize-space()='Price']/following-sibling::dd[1], ou à partir de la valeur : //dd[preceding-sibling::dt[1]='Price']. L’[1] est importante : sans elle, le prédicat est vrai dès lors qu’un libellé précédent correspond.
Les sélecteurs CSS peuvent-ils faire ce que fait « preceding-sibling » ?
En partie. La propriété :has() permet une sélection conditionnelle des frères dans les navigateurs modernes. Ce que le CSS ne peut toujours pas faire, c’est sélectionner en fonction du contenu textuel ou par index le long d’un axe inverse, et la correspondance de texte est précisément ce qu’exige le modèle « étiquette-valeur ». Utilisez le CSS pour les sélections simples et XPath lorsque vous avez besoin de texte ou d’une navigation en arrière.
« preceding-sibling » fonctionne-t-il dans Selenium et les navigateurs ?
Oui. Les navigateurs implémentent XPath 1.0 via document.evaluate, et Selenium utilise le moteur du navigateur. L’axe est celui de XPath 1.0 et est universellement disponible. Ce qui n’est pas disponible, ce sont les fonctionnalités d’XPath 2.0 ou des versions ultérieures : pas d’expressions régulières, pas de « matches() », pas de « upper-case() ».
L’axe « preceding-sibling » est-il lent ?
En général, non. Sa performance dépend du nombre de frères et sœurs, qui est généralement faible. C’est l’axe « preceding » qui est lent, car il parcourt tout ce qui se trouve en amont dans le document, et ce au sein d’une boucle dont la complexité devient quadratique. Si un sélecteur basé sur les frères et sœurs est lent, vérifiez si vous avez bien utilisé « preceding ».
Comment rendre les sélecteurs XPath moins fragiles ?
Ancrer sur du texte plutôt que sur la position lorsque c’est possible, utiliser normalize-space() pour pallier les différences d’espaces, privilégier les identifiants et les attributs de données plutôt que la structure, vérifier la présence de JSON-LD intégré avant même de parser le HTML, et valider la forme de ce que vous extrayez afin qu’un sélecteur correspondant à un mauvais élément échoue de manière flagrante plutôt que discrète.
Conclusion : `
`preceding-siblingest un outil très spécifique dont la seule fonction est de remonter en amont parmi les nœuds d’un même niveau. Il est important de retenir qu’il s’agit d’un axe inversé : ainsi,[1]`` désigne le nœud le plus proche plutôt que le premier, et l’ajout de parenthèses inverse complètement ce sens. Cette seule distinction explique la plupart des résultats erronés obtenus par les utilisateurs.
Sa véritable valeur réside dans le modèle « étiquette-valeur » : extraire un champ qui n’est identifiable que par le texte qui le précède. Le CSS a comblé une partie de ce fossé avec :has(), mais il ne permet toujours pas de sélectionner en fonction du contenu textuel, et le texte est précisément ce qu’est une étiquette. C’est dans ce cas précis que l’XPath reste l’outil le plus adapté, plutôt que celui auquel on a l’habitude de recourir.
Il convient toutefois de garder à l’esprit que les sélecteurs structurels peuvent échouer de manière silencieuse. Une nouvelle colonne, une liste réorganisée, une balise div d’encapsulation : votre sélecteur correspond alors à autre chose, alors que tout semble toujours fonctionner correctement. Ancrer sur du texte lorsque c’est possible, vérifier la présence de données structurées intégrées avant d’écrire le moindre sélecteur, et valider le résultat — car l’échec que vous pouvez voir n’est jamais le plus coûteux.
