Geonode logo
Geonode Team

Geonode Team

Mis à jour : 7 octobre 2026

Publié : 2 septembre 2026

XPath par classe : guide avec des exemples

La sélection par classe est l'utilisation la plus courante d'un sélecteur, et c'est justement ce que l'XPath fait le moins bien. L'expression que tout le monde écrit en premier — `//div[@class='card']` — ne correspond qu'aux éléments dont l'attribut « class » correspond exactement à cette chaîne de caractères ; elle ne prend donc pas en compte `class="card featured"`. La solution qui semble évidente, `contains(@class, 'card')`, correspond également à `card-large` et `discard`. Aucune de ces deux solutions n'est correcte. Voici la bonne méthode, ainsi que les cas où il vaut mieux utiliser un sélecteur CSS à la place.

Notre position est simple : nous sommes Geonode et nous vendons des proxys, ce qui n'a rien à voir avec la sélection de classes. La seule remarque pertinente est que les sélecteurs basés sur les classes sont les plus fragiles, et que cette fragilité ressemble en tout point à un problème de réseau vu de l’extérieur : la requête aboutit, la page s’affiche, mais votre extraction ne renvoie rien ou renvoie des informations erronées. Si vous déboguez un scraper qui a cessé de fonctionner, vérifiez si les noms de classes ont changé avant de vous pencher sur la manière dont la page a été récupérée. Sur un site utilisant un pipeline de build moderne, ils peuvent changer à chaque déploiement.

Le problème avec l'attribut « class »

class contient une liste de tokens séparés par des espaces, mais XPath ne reconnaît pas ce type de liste. Il ne voit qu'une seule chaîne de caractères.

<div class="card featured large">...</div>

Pour XPath, « @class » correspond à la chaîne « "card featured large" ». Il n’existe aucun moyen intégré de demander « card est-il l’un des tokens ? », ce qui est précisément la question à laquelle un sélecteur CSS répond nativement avec « .card ».

Cette lacune entraîne deux types d’échecs.

La correspondance exacte est trop stricte :

//div[@class='card']

Ne correspond qu’à class="card", sans rien d’autre ni espace supplémentaire. Elle ne prend pas en compte class="card featured", class="featured card" et class=" card ". Sur une page réelle, elle ne trouvera pas la plupart des éléments que vous recherchiez.

La correspondance de sous-chaîne est trop large :

//div[contains(@class, 'card')]

Correspond correctement à class="card", mais aussi à class="card-large", class="postcard", class="discard" et class="card-footer". Sur une page contenant des noms apparentés, il renvoie un sur-ensemble de ce que vous avez demandé — et votre code prend le premier résultat, sans rien signaler.

La deuxième erreur est la plus dangereuse, car elle produit des résultats. Quelque chose est renvoyé, cela semble raisonnable, mais c’est le mauvais élément.

L'expression idiomatique correcte

La solution standard ajoute des espaces de part et d'autre afin que seuls des tokens entiers puissent correspondre :

//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')]

Lisez-la étape par étape :

**normalize-space(@class)

** supprime les espaces en début et en fin de chaîne et réduit les séries d'espaces internes à un seul espace. " card featured "

devient "card featured"

.

**concat(' ', ..., ' ')

** encadre le résultat d’espaces, ce qui donne " card featured "

. Désormais, chaque token est délimité par un espace de chaque côté.

**contains(..., ' card ')

** recherche la cible entourée d’espaces.

Vérifiez cela par rapport aux cas qui ont fait échouer les versions naïves :

| Attribut de classe | Chaîne complétée | Contient-il « ' card '

» ? | |---|---|---| | card

| " card "

| Oui | | card featured

| " card featured "

| Oui | | featured card

| " featured card "

| Oui | | card-large

| " card-large "

| Non | | discard

| " discard "

| Non | | postcard footer

| " postcard footer "

| Non | | card

| " card "

| Oui |

C'est correct dans tous les cas. L'étape normalize-space()

n'est pas superflue : sans elle, class="card featured"

avec un double espace produirait " card featured "

, qui contient toujours " card "

et fonctionne par hasard, mais class="card\nfeatured"

avec un saut de ligne ne fonctionnerait pas.

C’est redondant. Enveloppez-la :

def has_class(name):
    return f"contains(concat(' ', normalize-space(@class), ' '), ' {name} ')"

tree.xpath(f"//div[{has_class('card')}]")
const hasClass = name =>
  `contains(concat(' ', normalize-space(@class), ' '), ' ${name} ')`;

Toute base de code effectuant des extractions complexes avec XPath finit par utiliser une version ou une autre de cette fonction d’aide. Il vaut mieux l’écrire une seule fois plutôt que de se tromper dans le remplissage d’une expression sur vingt.

Classes multiples

À combiner avec and

:

//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')
      and contains(concat(' ', normalize-space(@class), ' '), ' featured ')]

C'est là que la verbosité devient vraiment pénible : il faut 140 caractères pour exprimer ce que le CSS dit en div.card.featured

.

Pour « l'une ou l'autre de deux classes », utilisez or

:

//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')
      or contains(concat(' ', normalize-space(@class), ' '), ' tile ')]

Pour « possède cette classe mais pas celle-là » :

//div[contains(concat(' ', normalize-space(@class), ' '), ' card ')
      and not(contains(concat(' ', normalize-space(@class), ' '), ' hidden '))]

En cas de deux conditions de classe ou plus, demandez-vous sérieusement si un sélecteur CSS ne ferait pas l’affaire. div.card.featured:not(.hidden)

utilise la même logique en un cinquième du nombre de caractères, et toutes les bibliothèques d’analyse courantes prennent en charge les sélecteurs CSS en plus d’XPath. Aucune règle ne vous oblige à choisir un seul langage pour l’ensemble du fichier.

Noms de classes générés et hachés

Une complication récente, qui modifie les recommandations.

De nombreux outils de build front-end génèrent des noms de classes avec un espace de noms pour éviter les conflits — CSS Modules, styled-components, diverses bibliothèques CSS-in-JS. Le résultat ressemble à ceci :

<div class="ProductCard_container__3xK9p">...</div>
<div class="css-1x9dj2k">...</div>

La partie hachée change chaque fois que les styles du composant changent, ce qui, dans la pratique, signifie à chaque déploiement. Un sélecteur ancré au nom complet cesse de fonctionner sans avertissement.

Voici trois façons de gérer cela, par ordre de préférence.

Ancrer sur le préfixe stable, lorsque l’outil en génère un :

//div[starts-with(@class, 'ProductCard_container__')]

Les modules CSS génèrent généralement des noms de type ComponentName_elementName__hash

, de sorte que la partie précédant le double trait de soulignement final reste stable d’une version à l’autre. Cela fonctionne bien lorsque c’est applicable.

**Rechercher plutôt un attribut « data-*

».** De nombreuses applications incluent des attributs tels que data-testid

, data-test

ou similaires, précisément pour que les outils automatisés disposent d’un point d’ancrage stable :

//div[@data-testid='product-card']

S’il en existe un, utilisez-le. Il est plus stable que n’importe quel nom de classe, car il a été choisi délibérément plutôt que généré.

Ancrer sur quelque chose de complètement différent. Structurez par rapport à un titre, un contenu textuel ou un type d’élément. //h2[normalize-space()='Featured']/following-sibling::div[1]

ne se soucie pas du nom des classes.

Et dans le cas le plus opaque — css-1x9dj2k

sans composant stable —, la sélection basée sur les classes n’est tout simplement pas viable, et prétendre le contraire aboutit à un scraper qui tombe en panne chaque semaine. Recherchez des données structurées sur la page, ou l’API que la page elle-même appelle.

Ordre des classes et espaces

Deux points qui posent souvent problème et sur lesquels il convient d’être précis.

L’ordre des classes dans l’attribut n’a aucune importance. class="card featured" et class="featured card" sont équivalents pour le navigateur et pour le CSS. La syntaxe « padded-concat » gère correctement les deux cas ; la correspondance exacte ne gère aucun des deux de manière fiable. Si vous vous retrouvez à écrire une expression qui dépend de l’ordre, c’est le signe que quelque chose ne va pas.

Les espaces peuvent être n’importe quoi. Les tabulations et les sauts de ligne sont des séparateurs valides dans un attribut « class », et ils apparaissent dans le HTML formaté à la main :

<div class="card
            featured">

normalize-space() regroupe tout cela, c’est pourquoi l’idiome les inclut. Une expression utilisant ``concat(' ', @class, ' ') sans normalisation échouera sur ce balisage, et cet échec est invisible dans une page affichée.

La casse est importante. Les noms de classes sont sensibles à la casse dans les documents HTML analysés en XHTML et traités sans distinction de casse en mode standard HTML pour la correspondance CSS — mais la comparaison de chaînes XPath est toujours sensible à la casse. Si une page mélange Card et card , la syntaxe avec complétion les traitera comme différents. XPath 1.0 ne dispose pas de fonction «lower-case() », la solution consiste donc à utiliser translate() avec des alphabets explicites, ce qui rend l’expression véritablement illisible et fait clairement pencher la balance en faveur d’un sélecteur CSS.

Obtenir l'ancêtre ou le descendant d'une classe

La sélection d'une classe est généralement un moyen plutôt qu'une fin en soi. Deux modèles couvrent la plupart des cas.

D’une classe à un élément qu’elle contient :

//div[contains(concat(' ',normalize-space(@class),' '),' card ')]//span[@class='price']

Ou, dans un code qui contient déjà l’élément « card », utilisez une expression relative — le point initial est indispensable :

for card in tree.xpath(f"//div[{has_class('card')}]"):
    price = card.xpath(".//span[@class='price']/text()")

.//span

effectue une recherche au sein de la carte. //span

effectue une recherche dans l’ensemble du document à partir de la racine, ce qui renvoie le premier prix de la page pour chaque carte. Cela produit un résultat uniforme, plausible, mais erroné, et c’est l’un des bugs les plus courants dans le code d’extraction.

D’un élément jusqu’à son conteneur :

//span[@class='price']/ancestor::div[contains(concat(' ',normalize-space(@class),' '),' card ')][1]

L’[1]

e est importante : ancestor::

correspond à un axe inversé, de sorte que la position 1 correspond à l’ancêtre correspondant le plus proche plutôt qu’au plus externe. Sans cela, vous obtenez tous les ancêtres correspondants, ce qui est rarement le résultat souhaité en présence de conteneurs imbriqués.

Remarques spécifiques aux bibliothèques

Le syntaxe est la même partout ; l'API qui l'entoure ne l'est pas, et certaines différences entre les bibliothèques sont source de confusion qui pourrait être évitée.

lxml (Python). Prend en charge les deux langages, et cssselect est le choix le plus pragmatique pour les travaux en classe :

from lxml import html
tree = html.fromstring(source)

tree.cssselect('div.card.featured')                  # clear
tree.xpath(f"//div[{has_class('card')}]")            # when inside a larger expression

cssselect convertit en interne le CSS en XPath ; les deux sont donc équivalents en termes de capacités pour les sélecteurs qu’ils prennent en charge. Notez qu’il s’agit d’un module distinct de lxml lui-même et qu’il doit être installé.

BeautifulSoup (Python). Ne prend absolument pas en charge XPath — ce qui surprend souvent les personnes issues d’autres écosystèmes. Il propose select() pour les sélecteurs CSS et sa propre API find_all(class_='card') , qui effectue nativement une correspondance correcte des tokens. Si vous avez spécifiquement besoin d’XPath, vous devez utiliser lxml.

Selenium. Accepte à la fois By.XPATH et By.CSS_SELECTOR , et utilise le moteur du navigateur pour chacun. Cela signifie uniquement XPath 1.0, et que la prise en charge du CSS correspond à ce que le navigateur prend en charge — y compris :has() .

Playwright. Détecte automatiquement le type de localisateur à partir de la chaîne de caractères ; ainsi, page.locator('div.card') et page.locator('//div[@id="x"]') fonctionnent tous deux sans préfixe. Il propose également des localisateurs basés sur du texte — page.getByText() , page.getByRole() — qui couvrent une grande partie des cas pour lesquels on avait auparavant besoin de la correspondance de texte d’XPath, et qui sont plus lisibles que l’un ou l’autre de ces langages.

Scrapy. Fournit les méthodes .css() et .xpath() sur les sélecteurs, et celles-ci s’enchaînent, ce qui est vraiment utile :

for card in response.css('div.card'):
    price = card.xpath(".//dt[normalize-space()='Price']/following-sibling::dd[1]/text()").get()

du CSS pour le passage structurel, de l’XPath pour la recherche d’étiquettes, dans la même chaîne d’expressions. Notez le . en début de l’XPath — les sélecteurs enchaînés de Scrapy présentent le même piège « racine vs relatif » que partout ailleurs.

Console du navigateur. $x('//div[@class="card"]') évalue les expressions XPath dans les outils de développement de Chrome et Firefox, tandis que document.querySelectorAll('div.card') gère le CSS. Tester une expression ici avant de l’intégrer au code vaut bien ces dix secondes, en gardant à l’esprit que le DOM du navigateur est post-JavaScript, contrairement à celui de votre parseur — une expression qui fonctionne dans la console peut ne rien trouver dans le code HTML brut.

Quand utiliser plutôt le CSS

Pour le dire simplement, parce que pour cette tâche spécifique, la réponse est généralement « oui ».

Utilisez le CSS lorsque vos critères sont des classes. div.card, div.card.featured, div.card:not(.hidden) — toutes ces expressions sont plus claires, plus courtes et correctes par défaut. Le CSS interprète l’attribut « class » comme une liste de tokens, ce qui est précisément ce qui manque à XPath.

Utilisez XPath lorsque la classe est accessoire et que le véritable critère est quelque chose que le CSS ne peut pas exprimer — le contenu textuel avant tout. //div[contains(@class,'card')][.//span[contains(., 'Sold out')]] nécessite XPath pour la condition relative au texte, et la partie « classe » est incluse par la même occasion.

Combinez les deux. Toutes les bibliothèques de parsing courantes prennent en charge les deux. lxml propose cssselect ainsi que xpath ; Selenium accepte les deux stratégies de localisation ; Playwright les accepte également. Utiliser le CSS pour la sélection structurelle et XPath pour les conditions textuelles n’est pas un compromis, c’est la combinaison qui produit le code le plus court et le plus lisible.

Le seul cas où la sélection de classes via XPath s’impose est lorsque vous en avez besoin au sein d’une expression XPath plus large et qu’il n’est pas possible de changer de langage en cours d’expression. Il s’agit là d’une véritable contrainte, et c’est la raison d’être de cette syntaxe — mais son champ d’application est plus restreint que le nombre de correspondances de classes via XPath que vous rencontrerez dans la pratique.

Questions fréquentes

Comment sélectionner un élément par classe dans XPath ?

Utilisez //div[contains(concat(' ', normalize-space(@class), ' '), ' card ')]. Le remplissage garantit que seuls les tokens de classe complets sont pris en compte. La requête simple @class='card' ne détecte pas les éléments comportant des classes supplémentaires, et contains(@class,'card') correspond également à card-large et discard.

Pourquoi contains(@class, 'name') sélectionne-t-il les mauvais éléments ?

Parce qu’il s’agit d’un simple test de sous-chaîne sans notion de limites de mots. L’attribut « class » est une liste séparée par des espaces, mais XPath le considère comme une seule chaîne ; ainsi, contains(@class,'card') correspond à tout nom de classe contenant ces quatre caractères, où qu’ils se trouvent.

Comment sélectionner un élément ayant deux classes en XPath ?

Combinez deux conditions « padded-concat » avec and. Cela fonctionne et fait environ 140 caractères. À partir de deux conditions de classe, un sélecteur CSS — div.card.featured — est nettement plus clair, et la plupart des bibliothèques d’analyse syntaxique vous permettent d’en utiliser un.

L’ordre des classes a-t-il de l’importance en XPath ?

Pas avec l’idiome « padded-concat », qui correspond à un token où qu’il apparaisse dans l’attribut. En revanche, cela a de l’importance en cas de comparaison exacte, ce qui est l’une des nombreuses raisons pour lesquelles @class='card featured' est un mauvais choix. L’ordre des classes n’a aucune signification en HTML ; un sélecteur qui en dépend est donc un bug en puissance.

Comment gérer les noms de classe générés aléatoirement ?

Ancrer le sélecteur sur le préfixe stable avec starts-with() lorsque l’outil en génère un, utiliser un attribut data-testid si l’application en fournit un, ou s’ancrer sur un élément totalement différent, tel que du texte ou la structure. Pour les noms hachés totalement opaques sans partie stable, la sélection basée sur les classes n’est pas viable.

Quel est le meilleur choix entre XPath et CSS pour la sélection par classe ?

CSS, sans aucun doute. Il traite l’attribut « class » comme une liste de tokens, ce qu’il est effectivement, de sorte que div.card est à la fois plus court et correct. XPath nécessite une construction de soixante-dix caractères pour exprimer la même chose. Utilisez XPath lorsque vous avez également besoin d’une fonctionnalité que le CSS ne peut pas offrir, comme la correspondance de texte.

Comment trouver un élément parent par classe dans XPath ?

//span[@class='price']/ancestor::div[contains(concat(' ',normalize-space(@class),' '),' card ')][1]. L’[1] sélectionne l’ancêtre correspondant le plus proche, car ancestor:: est un axe inversé où la position 1 signifie « le plus proche » plutôt que « le plus extérieur ».

Pourquoi mon XPath relatif renvoie-t-il la même valeur pour chaque élément ?

Parce que vous avez utilisé // au lieu de .// à l’intérieur de la boucle. Un // en début de requête effectue une recherche à partir de la racine du document, quel que soit le contexte ; ainsi, chaque itération trouve la première correspondance sur l’ensemble de la page. .// effectue une recherche au sein de l’élément actuel, ce qui correspond à ce que vous souhaitiez.

Conclusion

C'est au niveau de la sélection des classes que XPath montre ses limites. L'attribut « class » est une liste de tokens et XPath ne dispose d'aucune opération sur les tokens ; par conséquent, pour exprimer « a cette classe », il faut ajouter des espaces à la chaîne et rechercher un token complété — une construction de soixante-dix caractères pour ce que le CSS exprime en neuf.

Apprenez cette syntaxe, car vous en aurez besoin dans des expressions XPath plus complexes, et encapsulez-la dans une fonction d’aide afin de ne l’écrire qu’une seule fois plutôt que vingt. L’étape de l’normalize-space()ation n’est pas facultative : le balisage réel contient des sauts de ligne et des tabulations dans les attributs de classe, et une expression non normalisée échouera de manière invisible face à ceux-ci.

Mais la conclusion la plus utile est celle qui évite cette expression. Si vos critères de sélection se limitent aux classes, écrivez un sélecteur CSS. Toutes les bibliothèques courantes prennent en charge les deux ; il est courant de les mélanger dans un même fichier, et choisir l’outil adapté à chaque expression permet d’obtenir un code plus court et lisible par un collègue.

Et considérez les noms de classes comme le point d’ancrage le moins stable qui soit. Les noms générés et hachés changent lors du déploiement ; même ceux écrits à la main changent chaque fois que quelqu’un redessine un composant. Une balise data-testid, le texte d’un titre ou la relation d’un élément avec quelque chose d’identifiable perdureront tous plus longtemps que le nom de classe — et le mode de défaillance lorsqu’une classe disparaît n’est pas une erreur, mais une sortie silencieuse, bien formée et vide.