Notre intérêt dans cette affaire est modeste, mais mérite tout de même d’être précisé : nous sommes Geonode et nous vendons des proxys à des personnes effectuant des tâches d’extraction de données ; les sélecteurs reviennent donc constamment dans nos échanges d’assistance. Avant de procéder à la comparaison, il convient de préciser qu’aucun de ces deux choix n’a d’incidence sur le risque de blocage. Une question relative à un sélecteur et une question relative au réseau semblent identiques à première vue — les deux se traduisent par « mon scraper a cessé de renvoyer des données » — mais elles n’ont rien en commun. Si la page s’est affichée correctement et que votre expression n’a correspondu à rien, il s’agit d’un problème de sélecteur et aucune décision concernant l’infrastructure n’y changera quoi que ce soit.
En bref
| CSS | XPath | |
|---|---|---|
| Sélection par classe, identifiant ou attribut | Oui, de manière claire | Oui, de manière peu pratique |
| Relations de descendance et de parenté | Oui | Oui |
| Sélection par contenu textuel | Non | Oui |
| Accéder au parent ou à l'ancêtre | En partie, via :has() | Oui, directement |
| Accéder au frère précédent | En partie, via :has() | Oui, directement |
| Sélection par position | Famille « :nth-child() » | « position() », « last() », prédicats |
| Fonctions de chaîne | Non | Oui |
| Interrogation de XML avec des espaces de noms | Non | Oui |
| Lisibilité | Meilleure | Moins bonne |
| Outils et prise en charge par les navigateurs | Universelle | Universelle pour la version 1.0 |
Deux lignes font toute la différence. Le CSS ne permet absolument pas de sélectionner du contenu textuel, ce qui l'écarte d'emblée du modèle d'extraction le plus courant : trouver une valeur à partir de l'étiquette qui l'accompagne. Et l'XPath est plus difficile à lire, ce qui a plus d'importance qu'on ne veut bien l'admettre lorsqu'un collègue doit assurer la maintenance de votre code un an plus tard.
Ce que le CSS fait mieux
La sélection des classes et des attributs, et de loin.
div.product-card
a[href^="https://"]
input[type="checkbox"]:checked
ul > li:first-child
section.content p:not(.footnote)
Les équivalents XPath sont plus longs et, pour les classes, vraiment peu pratiques :
//div[contains(concat(' ', normalize-space(@class), ' '), ' product-card ')]
//a[starts-with(@href, 'https://')]
//ul/li[1]
Le premier exemple illustre l’idiome de correspondance de classe, qui existe parce que @class
est une chaîne unique séparée par des espaces et que XPath ne reconnaît pas les tokens à l’intérieur de celle-ci. Une contains(@class, 'product-card')
naïve correspond également à product-card-large
et old-product-card
; la version complétée est donc la bonne. Elle est également quatre fois plus longue que div.product-card
et nettement plus difficile à parcourir.
Si vos critères de sélection sont des classes, des identifiants, des attributs et des relations structurelles, utilisez le CSS. Cela couvre la grande majorité des opérations de sélection réelles, et choisir XPath dans ce cas revient à sacrifier la lisibilité au profit de fonctionnalités que vous n’utilisez pas.
Le CSS dispose également de meilleurs outils. L’inspecteur d’éléments de chaque navigateur génère nativement des sélecteurs CSS, la plupart des frameworks de test les utilisent par défaut, et document.querySelectorAll
est accessible partout sans avoir besoin d’un outil supplémentaire.
Ce que XPath fait mieux
La correspondance de texte, ce que le CSS ne peut absolument pas faire :
//button[normalize-space()='Continue']
//a[contains(., 'Download')]
//dt[normalize-space()='Price']/following-sibling::dd[1]
Ce dernier modèle — trouver l’étiquette, récupérer la valeur adjacente — est la pierre angulaire de l’extraction de pages structurées, et il n’existe aucune expression CSS pour cela, car le CSS n’a pas accès au contenu textuel. C’est cette seule capacité qui explique pourquoi XPath continue d’être utilisé pour l’extraction de données dans des bases de code qui, par ailleurs, utilisent le CSS partout.
Navigation vers les ancêtres :
//span[@class='price']/ancestor::div[contains(@class,'card')][1]
Remonter depuis une valeur jusqu’au conteneur qui la renferme. :has()
offre au CSS une forme de cette fonctionnalité, avec des limites abordées ci-dessous.
Logique de position relative au contenu :
//h2[normalize-space()='Specifications']/following-sibling::table[1]
Le premier tableau après un en-tête spécifique. :nth-child()
compte les positions parmi les éléments frères ; il ne peut pas exprimer « après l’élément dont le texte est X ».
Fonctions de chaîne de caractères. normalize-space()
, substring-before()
, translate()
et les autres vous permettent d’effectuer des opérations au sein de l’expression. normalize-space()
, en particulier, est pratiquement indispensable, car le HTML réel est formaté et une comparaison textuelle exacte avec "\n In stock\n"
échouera.
XML avec espaces de noms. Si vous interrogez du XML plutôt que du HTML — un plan de site, un flux RSS, une réponse SOAP —, le CSS n’est pas l’outil adapté. XPath est conçu pour cela, et la gestion des espaces de noms fait partie intégrante de sa conception.
Comment « :has() » a changé la donne
Il s'agit de l'évolution la plus significative mise en évidence dans cette comparaison, et de nombreux articles l'ont déjà mentionnée auparavant.
La documentation MDN décrit l':has() comme représentant « un élément si l'un des sélecteurs relatifs passés en argument correspond à au moins un élément lorsqu'il est ancré par rapport à cet élément », offrant ainsi « un moyen de sélectionner un élément parent ou un élément frère précédent par rapport à un élément de référence ». Son statut de base est « largement disponible » ; il est pris en charge par tous les navigateurs depuis décembre 2023.
Le CSS peut donc désormais exprimer des choses qu’il ne pouvait pas faire auparavant :
div.card:has(span.sold-out) /* a card containing a sold-out marker */
h1:has(+ p) /* an h1 immediately followed by a p */
li:has(~ li.active) /* an li with a later active sibling */
tr:has(td.error) /* a row containing an error cell */
Cela couvre une grande partie de ce pour quoi les utilisateurs avaient auparavant besoin d’XPath : la sélection d’un élément parent et la conditionnalité par rapport à un élément frère.
Trois limites sont documentées et méritent d’être connues. :has() « ne peut pas être imbriqué dans un autre :has() ». Les pseudo-éléments « ne sont pas des sélecteurs valides au sein de :has() » et ne constituent pas des ancrages valides pour celui-ci. Enfin, sa spécificité correspond à « la spécificité du sélecteur le plus spécifique parmi ses arguments », ce qui correspond au comportement de :is() et :not().
La restriction la plus importante pour l’extraction ne figure pas dans cette liste : :has() ne permet toujours pas de correspondre à du texte. div:has(span) fonctionne ; div:has(span:contains('Price')) n’existe pas, car :contains() n’est pas un sélecteur CSS standard. Il a été proposé puis abandonné, et les navigateurs ne l’implémentent pas. Le modèle « étiquette-valeur » reste donc du ressort de XPath, indépendamment de :has().
Traductions côte à côte
Le moyen le plus rapide de se faire une idée de ce compromis est de voir la même intention exprimée des deux façons.
| Intention | CSS | XPath |
|---|---|---|
| Élément avec une classe | div.card | //div[contains(concat(' ',normalize-space(@class),' '),' card ')] |
| Élément avec un identifiant | #main | //*[@id='main'] |
| L'attribut existe | a[href] | //a[@href] |
| L'attribut commence par | a[href^="/docs"] | //a[starts-with(@href,'/docs')] |
| L'attribut contient | a[href*="pdf"] | //a[contains(@href,'pdf')] |
| Enfant direct | ul > li | //ul/li |
| N'importe quel descendant | div p | //div//p |
| Premier enfant | li:first-child | //li[1] |
| Dernier enfant | li:last-child | //li[last()] |
| N-ième enfant | li:nth-child(3) | //li[3] |
| Frère ou sœur suivant(e) | h2 + p | //h2/following-sibling::p[1] |
| N'importe quel frère ou sœur suivant(e) | h2 ~ p | //h2/following-sibling::p |
| Négation | p:not(.footnote) | //p[not(contains(@class,'footnote'))] |
| Parent d'une correspondance | div:has(> span.price) | //span[contains(@class,'price')]/.. |
| Contient du texte | impossible | //p[contains(., 'Price')] |
| Texte exact | impossible | //p[normalize-space()='Price'] |
| Valeur à côté d’une étiquette | impossible | //dt[normalize-space()='Price']/following-sibling::dd[1] |
En parcourant ce tableau, la tendance est claire. Pour tout ce qui se trouve au-dessus de la ligne « parent », le CSS est plus court et plus clair, et choisir XPath revient à opter pour la verbosité sans aucun avantage. Pour les trois lignes du bas, il n’y a tout simplement pas de colonne CSS.
Deux traductions méritent qu’on s’y attarde. La ligne « class » présente le contraste le plus frappant de toute la comparaison — neuf caractères contre septante-quelques — et la version XPath n’est pas superflue, car la forme abrégée contains(@class,'card') correspond bel et bien à card-large et discard. Et la ligne « premier enfant » cache un piège : li:first-child et //li[1] sont identiques ici, mais //li[1] est un prédicat appliqué à chaque parent ; il sélectionne donc le premier li sous chaque liste du document. Le CSS est sans ambiguïté ; le XPath nécessite (//li)[1] si vous ne visiez qu’un seul élément.
Un exemple mixte concret
Voici à quoi cela ressemble en pratique, avec l'extraction d'une page produit.
# CSS for the structural work — clear and adequate
cards = tree.cssselect('div.product-grid > article.product-card')
for card in cards:
name = card.cssselect('h3.product-name')[0].text_content().strip()
image = card.cssselect('img.product-image')[0].get('src')
# XPath for the label-value pairs, which CSS cannot express
price = card.xpath(
".//dt[normalize-space()='Price']/following-sibling::dd[1]"
)[0].text_content().strip()
stock = card.xpath(
".//span[contains(., 'in stock') or contains(., 'out of stock')]"
)
Trois éléments méritent d'être repris.
L’.e en début de chaque expression XPath. .//dt effectue une recherche au sein de la fiche actuelle ; //dt effectuerait une recherche dans l’ensemble du document à partir de la racine, renvoyant la première étiquette correspondante sur la page pour chaque fiche. C’est l’une des erreurs les plus courantes dans le code d’extraction mixte, et cela produit la même valeur pour chaque enregistrement — plausible, uniforme, mais erronée.
Utiliser le CSS lorsque cela suffit. La grille et la sélection des cartes, le titre, l’image. Les écrire en XPath alourdirait le code et nuirait à la clarté.
N’utiliser XPath qu’en cas de nécessité. Le prix est identifié par son étiquette, et l’état des stocks par son texte. Aucun de ces éléments ne peut être exprimé en CSS, et ce sont ces deux éléments qui justifient la présence d’XPath dans le fichier.
Le résultat est un scraper où chaque expression est aussi courte que possible, et où le lecteur peut voir d’un seul coup d’œil quelles parties dépendent de la structure et lesquelles dépendent du contenu — ce qui constitue également une carte des éléments qui cesseront de fonctionner lorsque le site changera.
Performances
Généralement sans importance, parfois décisives.
Le CSS est généralement plus rapide dans les navigateurs, car les moteurs optimisent fortement la correspondance des sélecteurs — celle-ci se situe sur le chemin critique du rendu. XPath suit un modèle d’évaluation plus général.
Cette différence a rarement de l’importance à l’échelle à laquelle la plupart des gens travaillent. Effectuer une sélection sur une seule page quelques centaines de fois est négligeable dans les deux cas ; la requête réseau domine de plusieurs ordres de grandeur.
Dans quels cas cela a-t-il de l’importance ?
Les axes preceding et following sont véritablement coûteux en ressources. Ils parcourent l’intégralité du document avant ou après le nœud de contexte. À l’intérieur d’une boucle portant sur de nombreux nœuds, cela devient une opération quadratique. Si une expression XPath est sensiblement lente, vérifiez si elle utilise l’un de ces axes — preceding-sibling et following-sibling sont limités par le nombre de nœuds frères et ne posent pas de problème.
// au début d’une expression imbriquée relance la recherche depuis la racine. //div//span est plus coûteux qu’il n’y paraît, et .//span à l’intérieur d’une boucle sur des divs correspond généralement à ce que vous vouliez dire.
:has() peut s’avérer coûteux dans les documents volumineux, car le moteur doit évaluer le sélecteur interne pour chaque candidat. Cela convient pour l’extraction ; c’est un point à surveiller dans une feuille de style appliquée à une page volumineuse.
Pour l’analyse côté serveur avec lxml ou un outil similaire, la différence est suffisamment faible pour que ce soit la lisibilité qui soit déterminante.
Problèmes liés à la disponibilité et aux versions
Le piège qui explique pourquoi « ça marche dans l'outil de test en ligne, mais pas dans mon code ».
Les navigateurs implémentent XPath 1.0 via document.evaluate, et Selenium utilise le moteur du navigateur. Les fonctionnalités de XPath 2.0 et 3.1 — matches(), replace(), lower-case(), ends-with(), les séquences — ne sont pas disponibles dans ce contexte. Tout extrait de code utilisant l’une d’entre elles échouera.
lxml implémente XPath 1.0 pour son API standard, ainsi que les extensions EXSLT, notamment re:test() pour les expressions régulières. Ainsi, une expression basée sur des expressions régulières qui fonctionne en Python ne fonctionnera pas dans Selenium.
La prise en charge du CSS dans les bibliothèques d’analyse s’effectue généralement via une couche de traduction qui convertit le CSS en XPath en interne. Cela fonctionne bien pour les sélecteurs courants, mais moins bien pour les plus récents — la prise en charge de :has() varie considérablement d’un analyseur côté serveur à l’autre, et un sélecteur qui fonctionne dans un navigateur peut ne pas fonctionner dans votre scraper.
Règle pratique : testez vos sélecteurs dans l’environnement où ils seront exécutés, et non dans la console d’un navigateur ou un outil en ligne. Les deux surprises les plus courantes sont l’échec des fonctions XPath 2.0 dans Selenium et celui des sélecteurs de type « :has() » dans un analyseur Python.
Comment choisir dans la pratique
Une méthode de décision qui permet de résoudre pratiquement tous les cas.
Commencez par le CSS. Il est plus lisible, mieux pris en charge par les outils et convient parfaitement à la sélection par classe, identifiant, attribut et structure — ce qui représente la grande majorité des cas.
Passez à XPath lorsque vous devez effectuer une correspondance sur du texte. C’est la raison principale, et elle est déterminante : aucune construction CSS ne lit le contenu textuel.
Passez à XPath pour la navigation vers les ancêtres au-delà de ce que couvre :has(), en particulier lorsque vous avez besoin d’un ancêtre spécifique situé plusieurs niveaux plus haut plutôt que d’une correspondance conditionnelle sur un parent connu.
Passez à XPath pour le XML. Les espaces de noms et les opérations sur l’ordre des documents sont précisément ce pour quoi il a été conçu.
Utilisez les deux dans une même base de code. C’est une pratique normale et sensée, bien plus qu’un simple compromis. Un scraper utilisant le CSS pour les 90 % de cas simples et XPath pour les 10 % difficiles est plus facile à maintenir qu’un scraper qui s’en tient à l’un ou à l’autre.
Et une habitude qui vaut mieux que l’un ou l’autre choix : vérifiez la présence de données structurées intégrées avant même d’écrire un sélecteur. De nombreuses pages contiennent du JSON-LD dans un bloc <script type="application/ld+json">, car cela alimente les fonctionnalités de recherche, et l’analyse de ce format est nettement plus stable que celle du balisage affiché. Elle résiste aux refontes qui cassent tous les sélecteurs de la page.
Ce que ni l'un ni l'autre ne résout
Les deux présentent la même fragilité, et c'est justement cela qui fait perdre du temps.
Les sélecteurs structurels se cassent en silence. Quelqu’un insère une colonne, et td[3] correspond à une cellule différente. Aucune erreur n’est signalée ; vos données sont discrètement erronées. Ancrez-vous sur du texte ou des identifiants lorsque c’est possible, et vérifiez la structure de ce que vous extrayez — si un prix doit ressembler à un prix, vérifiez-le.
Aucun des deux ne gère le contenu rendu côté client. Si le balisage est assemblé par JavaScript après le chargement, les deux ne trouvent rien dans le code HTML initial. Il s’agit d’un problème de récupération qui relève du navigateur ou de l’API sous-jacente, et non d’un problème lié aux sélecteurs.
Aucun des deux ne résiste à lui seul à une refonte. Pour y remédier, regroupez les sélecteurs au même endroit, afin que la mise à jour après une modification ne prenne qu’une heure au lieu d’une journée, et stockez le code HTML que vous avez analysé pour pouvoir comparer l’ancien et le nouveau lorsque quelque chose ne fonctionne plus.
Et aucun des deux n’est affecté par la manière dont la page a été récupérée. C’est là que nous en revenons à notre point de départ : si le document est intact et que votre expression ne correspond à rien, aucune modification de l’infrastructure ne changera le résultat.
Questions fréquentes
L'XPath est-il meilleur que les sélecteurs CSS ?
En général, aucun des deux n'est meilleur que l'autre. L'XPath offre davantage de possibilités : il permet de rechercher du texte, d'accéder aux ancêtres et d'utiliser des fonctions de chaîne de caractères. Le CSS est plus lisible et mieux pris en charge par les outils. Utilisez le CSS par défaut et l'XPath pour les cas spécifiques que le CSS ne permet pas d'exprimer.
Les sélecteurs CSS permettent-ils de sélectionner du texte ?
Non. Il n’existe pas de sélecteur CSS standard pour le contenu textuel : la proposition « :contains() » n’a jamais été adoptée et les navigateurs ne l’implémentent pas. Il s’agit là de la principale lacune en termes de fonctionnalités et de la raison principale pour laquelle XPath reste utilisé dans les tâches d’extraction.
L’:has() remplace-t-il XPath ?
En partie. Il permet la sélection des éléments parents en CSS et la correspondance conditionnelle entre éléments frères, qui étaient auparavant l’apanage exclusif de XPath. Il n’ajoute pas de correspondance de texte ; le modèle « étiquette-valeur » nécessite donc toujours XPath. Il ne peut pas non plus être imbriqué, et sa prise en charge varie selon les bibliothèques d’analyse côté serveur.
Qu’est-ce qui est le plus rapide, XPath ou le CSS ?
Le CSS est généralement plus rapide dans les navigateurs, car la correspondance des sélecteurs est optimisée pour le rendu. La différence est généralement négligeable par rapport au temps de réseau. Là où XPath devient véritablement lent, c’est au niveau des axes « preceding » et « following », qui parcourent l’intégralité du document.
Puis-je utiliser XPath 2.0 dans Selenium ?
Non. Les navigateurs implémentent XPath 1.0 via document.evaluate, et Selenium utilise le moteur du navigateur. Les fonctions telles que matches(), lower-case() et ends-with() ne sont pas disponibles. C’est généralement la raison pour laquelle un extrait de code fonctionne dans un testeur en ligne mais échoue dans une suite de tests.
Comment sélectionner un élément parent ?
En XPath, parent:: ou ... En CSS, :has() propose une forme conditionnelle : div:has(> span.price) sélectionne la balise div plutôt que la balise span. Pour un ancêtre spécifique situé plusieurs niveaux plus haut, l’axe ancestor:: de XPath est plus direct.
Dois-je utiliser le CSS ou XPath pour le web scraping ?
Les deux, dans la même base de code. Le CSS pour la sélection des classes, des identifiants et des attributs, ce qui représente l’essentiel. XPath lorsque vous devez faire correspondre du texte — trouver une valeur par son libellé est le cas d’école et n’a pas d’équivalent en CSS.
Lequel est le plus stable en cas de modification d’un site ?
Aucun des deux, par nature — la stabilité dépend de ce sur quoi vous vous ancrez plutôt que du langage utilisé. L’ancrage sur du texte ou un attribut « data-* » résiste à une refonte ; l’ancrage sur la position ne résiste à rien. Les deux langages vous permettent de faire l’un ou l’autre.
En conclusion
La comparaison se résume à deux asymétries : le CSS ne peut pas « lire » le texte, et le XPath est plus difficile à lire. Tout le reste n’est que détail.
La règle à suivre est donc simple : privilégiez le CSS par défaut, car la plupart des sélections s’effectuent par classe, identifiant ou attribut, et le CSS les exprime clairement. Recourez à XPath uniquement lorsque vous avez besoin d’une fonctionnalité qu’il offre en exclusivité — la correspondance de texte avant tout, puis la navigation par ancêtres et les fonctions de chaînes de caractères. Il est normal de les mélanger, et une base de code qui utilise chacun d’eux pour ce qu’il fait le mieux est plus facile à maintenir qu’une base de code qui aurait choisi un camp. La spécification «
:has()» a véritablement réduit l’écart et mérite d’être adoptée là où elle s’applique : la sélection des parents et les conditions de frères et sœurs en CSS pur, largement disponibles depuis fin 2023. Notez simplement qu’il ne traite pas le texte, et que la prise en charge par les analyseurs côté serveur est moins homogène que celle des navigateurs.
Vérifiez donc votre environnement avant de vous fier à une expression. XPath 1.0 dans les navigateurs et Selenium, les fonctionnalités supplémentaires d’EXSLT dans lxml, la prise en charge de l’:has() de variables dans les bibliothèques d’analyse syntaxique… La surprise la plus courante en matière de sélecteurs n’est pas une erreur de syntaxe, mais une fonctionnalité qui existe ailleurs que là où vous l’exécutez.
