Une petite précision sur l'auteur de cet article et pourquoi cela n'a guère d'importance ici : nous sommes Geonode et nous vendons des proxys à des personnes qui effectuent des travaux d'extraction de données. Le choix du sélecteur n’a aucune incidence sur ce que nous vendons, et le seul point commun digne d’être mentionné est d’ordre diagnostique : un sélecteur qui ne correspond à rien et une requête bloquée semblent identiques vus de l’extérieur, alors qu’il s’agit de problèmes totalement distincts. Si la page s’est affichée et que votre sélecteur n’a rien trouvé, cette référence est la bonne et rien ne doit être modifié concernant la manière dont la page a été récupérée.
Sélecteurs de base
| Sélecteur | Correspondances |
|---|---|
* | Tous les éléments |
div | Tous les éléments de type « div » |
.card | Éléments ayant la classe « card » |
#main | L'élément ayant l'identifiant « main » |
.card.featured | Éléments ayant les deux classes |
div.card | Éléments de type « div » ayant la classe « card » |
div, p | Éléments de classe « div » et de classe « p » |
Deux d’entre eux méritent qu’on s’y attarde.
** «.card.featured » sans espace signifie que les deux classes s’appliquent à un seul élément.** « .card .featured » avec un espace signifie un élément de classe « featured » à l’intérieur d’un élément de classe « card ». Un seul caractère change complètement le sens, et c’est la faute de frappe la plus courante dans tout ce domaine.
** «div, p » est une liste de sélecteurs**, et non une relation. Elle correspond à tout ce qui se trouve dans l’un ou l’autre groupe, et chaque partie est évaluée indépendamment — ainsi, « div, p.note » correspond à toutes les balises div ainsi qu’aux paragraphes ayant la classe « note », et non aux balises div ayant la classe « note ».
Combinateurs
Ils expriment des relations entre les éléments.
| Sélecteur | Correspondances |
|---|---|
div p | p n'importe où à l'intérieur d'un div (descendant) |
div > p | p qui est un enfant direct d'un div |
h2 + p | L'p immédiatement après un h2 (frère adjacent) |
h2 ~ p | Tout élément p situé après un élément h2 ayant le même parent (frère général) |
Le combinateur de descendants est un espace, ce qui fait qu’il est facile de l’introduire par inadvertance et de le manquer à la lecture.
> est plus précis et généralement plus robuste. div > p ne correspondra pas à un paragraphe que quelqu’un encadre dans un <section> le mois prochain — ce qui peut sembler être un inconvénient, mais qui est souvent le contraire, car un sélecteur qui commence silencieusement à correspondre à plus d’éléments que prévu est pire qu’un sélecteur qui cesse de correspondre et vous le signale.
+ et ~ sont les combinateurs de frères et sœurs et ils ne regardent que vers l’avant. Il n’existe pas de combinateur « frère précédent » en CSS ; la propriété :has() permet d’obtenir cet effet de manière indirecte, comme expliqué ci-dessous.
Sélecteurs d'attributs
Il s'agit souvent de l'option la plus fiable, car les attributs sont généralement choisis de manière délibérée.
| Sélecteur | Correspond à |
|---|---|
[href] | Éléments comportant un attribut « href » |
[href="/about"] | Valeur exacte |
[href^="/docs"] | Valeur commençant par |
[href$=".pdf"] | Valeur se terminant par |
[href*="download"] | Valeur contenant |
[class~="card"] | Valeur sous forme de liste séparée par des espaces contenant le mot card |
[lang|="en"] | Valeur égale à en ou commençant par en- |
[data-state="open" i] | Correspondance insensible à la casse |
Ces deux-là sont sous-utilisés.
[class~="card"] est exactement équivalent à .card — l’opérateur ~= effectue une correspondance de mots séparés par des espaces, ce dont l’attribut class a besoin. Il est utile de savoir qu’il existe, principalement pour comprendre ce que fait réellement .card, et pour construire des sélecteurs par programmation lorsque vous disposez d’un nom d’attribut dans une variable.
Le drapeau i rend la correspondance insensible à la casse, ce qui est vraiment pratique pour les valeurs d’attribut dont la casse varie d’une page à l’autre sur un site.
Pour l’extraction en particulier, les attributs data-* sont nettement préférables aux classes. Un [data-testid="product-card"] a été choisi par un développeur pour servir de point d’ancrage stable ; un nom de classe peut être régénéré par un outil de build à chaque déploiement.
Pseudo-classes : structurelles
| Sélecteur | Correspondances |
|---|---|
:first-child | Un élément qui est le premier enfant de son parent |
:last-child | Le dernier enfant |
:only-child | Un élément sans frères et sœurs |
:nth-child(3) | Le troisième enfant |
:nth-child(2n) | Les enfants pairs |
:nth-child(2n+1) | Les enfants impairs |
:nth-child(-n+3) | Les trois premiers |
:nth-last-child(2) | L'avant-dernier |
:first-of-type | Le premier élément de son type parmi ses frères et sœurs |
:nth-of-type(2) | Le deuxième de son type |
:empty | Un élément sans enfants, y compris le texte |
:root | La racine du document, généralement html |
Les variantes de l’-of-type ont plus d’importance qu’on ne le pense. p:first-child ne correspond à un paragraphe que s’il s’agit du premier enfant de son parent — ainsi, si un titre précède, cela ne correspond à rien. p:first-of-type correspond au premier paragraphe, quel que soit ce qui le précède. Pour l’extraction, c’est presque toujours la deuxième variante qui correspond à ce que vous vouliez dire.
:nth-child(-n+3) pour « les trois premiers » est l’expression idiomatique à retenir de la syntaxe An+B, tout comme :nth-child(n+4) pour « à partir du quatrième ». En les combinant, on obtient une plage : :nth-child(n+2):nth-child(-n+5) sélectionne les enfants du deuxième au cinquième.
Pseudo-classes : Logiques
Le groupe qui a le plus évolué ces dernières années.
| Sélecteur | Correspondances |
|---|---|
:not(.hidden) | Éléments sans classe hidden |
:is(h1, h2, h3) | N'importe lequel des sélecteurs listés |
:where(h1, h2, h3) | Identique à :is() mais avec une spécificité nulle |
:has(> img) | Éléments contenant un descendant ou un frère correspondant |
:is() et :where() permettent de raccourcir les listes répétitives. :is(article, section) > h2 remplace article > h2, section > h2, et le gain de place s'accumule rapidement avec des listes plus longues. La seule différence entre eux réside dans la spécificité : :is() reprend la spécificité de son argument le plus spécifique, tandis que :where() apporte toujours une spécificité nulle.
:not() accepte une liste de sélecteurs dans les navigateurs modernes, ce qui fait que :not(.a, .b) fonctionne et signifie « aucun des deux ». L’ancien comportement à argument unique nécessitait d’enchaîner :not(.a):not(.b).
:has() est la plus importante. La documentation MDN la décrit 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 « 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 est « Baseline widely available » (standard de base largement disponible) ; il est pris en charge par tous les navigateurs depuis décembre 2023.
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 */
label:has(input:checked) /* a label wrapping a checked input */
Trois limites documentées : il « ne peut pas être imbriqué » à l’intérieur d’un autre :has(), les pseudo-éléments ne sont pas valides à l’intérieur de celui-ci ni en tant qu’ancrages pour celui-ci, et sa spécificité suit la même règle que :is() et :not() — l’argument le plus spécifique.
Pseudo-classes : état et formulaire
| Sélecteur | Correspondances |
|---|---|
:hover, :focus, :active | États d'interaction |
:focus-visible | Focus devant afficher un anneau visible |
:focus-within | Élément contenant un descendant en focus |
:checked | Case à cocher cochée, bouton radio activé ou option sélectionnée |
:disabled, :enabled | État des contrôles de formulaire |
:required, :optional | Attributs de validation de formulaire |
:valid, :invalid | État de validation des contraintes |
:placeholder-shown | Un champ de saisie affichant son texte de remplacement |
:target | L’élément correspondant au fragment d’URL |
:visited, :link | États des liens |
La plupart de ces éléments relèvent davantage de la mise en forme que de l’extraction, à deux exceptions près. :checked est véritablement utile pour lire l’état d’un formulaire dans le cadre de l’automatisation des tests, et :disabled vous indique si un contrôle est interactif — ce qui explique souvent pourquoi un clic n’a rien donné.
Notez que :hover et :focus ne peuvent pas faire l’objet d’une requête utile avec querySelectorAll dans un contexte de scraping, car aucun de ces états n’existe dans un document que vous venez simplement d’analyser.
La spécificité, en bref
Ce concept est pertinent lorsque des sélecteurs se font concurrence pour l'application d'un style, mais n'a aucune importance pour les requêtes — il est utile de le savoir, car il est fréquent de confondre ces deux contextes.
La spécificité se calcule à partir de trois chiffres :
| Composant | Source |
|---|---|
| Identifiants | #main |
| Classes, attributs, pseudo-classes | .card, [href], :hover |
| Éléments, pseudo-éléments | div, ::before |
En comparant de gauche à droite : aucun nombre de classes ne l’emporte jamais sur un seul identifiant. Les styles en ligne l’emportent sur tout cela, et !important l’emporte à son tour, ce qui explique pourquoi ces deux éléments constituent un dernier recours plutôt qu’un outil à part entière.
Deux remarques concernant les sélecteurs modernes. :where() n’apporte aucune spécificité, ce qui en fait le choix idéal pour les valeurs par défaut des bibliothèques que les utilisateurs doivent pouvoir remplacer sans difficulté. Et :is(), :not() et :has() reprennent tous la spécificité de leur argument le plus spécifique ; ainsi, :is(#main, div) est aussi spécifique que #main.
Pour les sélecteurs « querySelectorAll » et le scraping, la spécificité ne s’applique pas du tout. Le sélecteur correspond à un élément ou non ; il n’y a pas de concurrence.
Modèles d'extraction
Les sélecteurs qui effectuent le travail proprement dit dans un scraper.
Fiches produits dans une grille :
div.product-grid > article.product-card
Liens vers des PDF :
a[href$=".pdf"]
Liens externes :
a[href^="http"]:not([href*="example.com"])
Un hook de test stable plutôt qu'une classe :
[data-testid="price"]
Un conteneur qui contient quelque chose :
tr:has(td.error)
article:has(img)
form:has(input:invalid)
Le premier paragraphe d’un article, quel que soit ce qui le précède :
article p:first-of-type
Ignorer une ligne d’en-tête :
tbody tr:not(:first-child)
Prendre les lignes 2 à 11 :
tbody tr:nth-child(n+2):nth-child(-n+11)
Éléments comportant un identifiant de classe spécifique, créés par programmation :
[class~="card"]
Cellules vides que vous souhaitez exclure :
td:not(:empty)
Écrire des sélecteurs qui ne tombent pas en panne
Une référence vous indique ce qui est disponible. C’est dans le choix parmi ces options que réside la pérennité, et la différence entre un sélecteur qui survit à une refonte et un autre qui tombe en panne la semaine suivante tient entièrement à ce dont il dépend.
Classez vos ancres en fonction du degré d’intentionnalité de leur choix. Un « data-testid » existe parce qu’un développeur l’a placé là à des fins d’automatisation ; c’est donc l’élément le plus stable de la page. Vient ensuite l’« id » — généralement choisi intentionnellement, parfois généré. Un élément sémantique ou un rôle ARIA est stable car il véhicule du sens. Un nom de classe saisi manuellement est modérément stable. Un nom de classe généré, tel que css-1x9dj2k, change à chaque compilation et n’a aucune valeur. Les sélecteurs positionnels sont les moins stables de tous, car ils codent la forme exacte de la page à un instant donné.
Soyez aussi précis que l’exige le sens, et pas plus. body > div > div > div.content > p est précis mais fragile : tout élément d’encapsulation ajouté n’importe où dans cette chaîne le rend inopérant. .content p exprime l’exigence réelle et résiste à une restructuration. L’instinct qui pousse à ajouter de la spécificité par mesure de sécurité produit l’effet inverse de la sécurité.
Préférez les sélecteurs qui échouent de manière évidente. Un sélecteur ne correspondant à aucun élément soulève un problème évident. Un sélecteur correspondant à trop d’éléments renvoie discrètement le premier, ce qui est plausible mais peut s’avérer erroné. Si vous avez le choix, optez pour la version qui va échouer plutôt que celle qui va dériver — div > p plutôt que div p lorsque la relation est réellement directe.
Regroupez les sélecteurs au même endroit. Quarante chaînes de sélecteurs dispersées dans une base de code constituent quarante sources de problèmes indépendantes. Regroupées dans un seul module avec des noms descriptifs, elles deviennent une carte de vos dépendances, et leur mise à jour après une modification du site ne prend qu’une heure de travail au lieu d’une journée d’archéologie.
Vérifiez la structure de ce que vous extrayez. C’est cette habitude qui permet de détecter les dérives silencieuses. Si un prix doit correspondre à un modèle de devise, vérifiez-le. Si une liste n’a jamais compté moins de vingt éléments, considérez qu’un nombre inférieur à vingt est une erreur. Sans cela, un sélecteur qui commence à correspondre au mauvais élément produira indéfiniment des données incorrectes mais bien formées, et rien ne déclenchera d’exception à aucun moment.
Enregistrez le code HTML que vous avez analysé. Lorsqu’un sélecteur ne fonctionne plus, comparer l’ancien balisage au nouveau vous permet de voir exactement ce qui a changé en quelques secondes. Une nouvelle requête pour enquêter est plus lente, consomme de la bande passante et peut vous renvoyer une page différente de celle qui a réellement échoué.
Ce que le CSS ne peut toujours pas faire
Une liste succincte, dont chaque élément constitue une véritable raison de se tourner vers XPath.
Recherche sur le contenu textuel. Il n’existe pas d’:contains() standard. Elle a été proposée puis abandonnée, et aucun navigateur ne la met en œuvre. C’est la lacune la plus importante et la raison pour laquelle le modèle « trouver une valeur par son libellé » relève du domaine d’XPath — //dt[normalize-space()='Price']/following-sibling::dd[1] n’a pas d’équivalent en CSS.
Accéder à un ancêtre arbitraire. :has() permet la sélection conditionnelle d’un parent — « la balise div qui contient cette balise span » — mais il n’existe pas d’axe d’ancêtre général. Remonter plusieurs niveaux jusqu’à un conteneur spécifique nécessite l’utilisation d’XPath.
Fonctions de chaîne de caractères. Pas de troncature, pas de sous-chaîne, pas de conversion de casse, pas de concaténation. Tout ce que vous sélectionnez doit être traité par la suite dans votre langage hôte.
Interroger du XML avec des espaces de noms. Le CSS ne prend pratiquement pas en charge les espaces de noms. Pour les plans de site, les flux RSS ou SOAP, utilisez XPath.
Sélectionner directement les éléments frères précédents. La propriété :has() permet d’obtenir cet effet — h2:has(+ p) sélectionne l’en-tête — mais il n’existe pas de combinateur - permettant de reproduire le comportement de +.
Voilà la liste complète à des fins pratiques. Tout ce pour quoi on recourait auparavant à XPath, le CSS le fait désormais, et généralement de manière plus lisible.
Différences entre environnements à vérifier
La surprise la plus courante avec les sélecteurs, c’est qu’une fonctionnalité existe ailleurs que là où vous l’utilisez.
Les navigateurs prennent en charge tout ce qui est répertorié ici, y compris :has(), :is(), :where() et :not() (avec plusieurs arguments).
Les analyseurs côté serveur varient considérablement. cssselect de Python, utilisé par lxml et Scrapy, traduit le CSS en XPath — ce qui fonctionne bien pour les sélecteurs classiques, mais moins bien pour les plus récents. La prise en charge de :has(), en particulier, n’est pas universelle, et un sélecteur qui fonctionne dans la console d’un navigateur peut générer une erreur ou ne rien renvoyer sans avertissement dans un analyseur.
BeautifulSoup utilise soupsieve pour sa méthode select(), qui offre une bonne couverture mais pas exhaustive, et ne prend absolument pas en charge XPath.
Playwright et Selenium utilisent le moteur du navigateur ; leur prise en charge correspond donc à celle du navigateur. Playwright propose en outre des localisateurs de texte et de rôle qui couvrent une grande partie de ce que le CSS ne peut pas exprimer.
Conseil pratique : testez vos sélecteurs dans l’environnement où ils seront exécutés. La console d’un navigateur est certes pratique, mais elle teste une implémentation différente sur un DOM post-JavaScript, ce qui peut vous induire en erreur de deux façons.
Questions fréquentes
Quelle est la différence entre .card.featured et .card .featured ?
Le premier, sans espace, correspond à un élément portant les deux classes. Le second, avec un espace, correspond à un élément de classe « featured » situé à l’intérieur d’un élément de classe « card ». Un simple espace change complètement le sens et c’est l’erreur la plus courante en matière de sélecteurs CSS.
Comment sélectionner un élément parent en CSS ?
À l’aide de :has() — div:has(> span.price) sélectionne la balise div plutôt que la balise span. Cette fonctionnalité est largement prise en charge par tous les navigateurs depuis décembre 2023. Il n’existe pas de sélecteur d’ancêtre général ; pour accéder à un conteneur spécifique situé plusieurs niveaux plus haut, il faut donc toujours recourir à XPath.
Les sélecteurs CSS peuvent-ils correspondre à du texte ?
Non. Il n'existe pas de pseudo-classe standard « :contains() » ; elle a été proposée, mais n'a jamais été adoptée. La correspondance par contenu textuel nécessite l'utilisation d'XPath ou d'un localisateur spécifique à une bibliothèque, tel que « getByText() » de Playwright.
Quelle est la différence entre :nth-child et :nth-of-type ?
:nth-child compte tous les enfants du parent ; :nth-of-type ne compte que les éléments du même type. Ainsi, p:first-child ne correspond à rien si un titre apparaît en premier, tandis que p:first-of-type correspond au premier paragraphe quoi qu’il arrive.
Que fait :is() et en quoi diffère-t-il de :where() ?
Les deux acceptent une liste de sélecteurs et sélectionnent n’importe lequel d’entre eux, ce qui permet de raccourcir les sélecteurs répétitifs. La seule différence réside dans la spécificité : :is() adopte la spécificité de son argument le plus spécifique, tandis que :where() apporte toujours une spécificité nulle — ce qui rend :where() idéal pour les valeurs par défaut qui doivent pouvoir être facilement remplacées.
Comment fonctionne la spécificité CSS ?
Les identifiants (id) l’emportent sur les classes, les attributs et les pseudo-classes, qui l’emportent à leur tour sur les sélecteurs d’éléments et de pseudo-éléments, composant par composant. Les styles en ligne l’emportent sur les sélecteurs, et l’!important l’emporte sur ces derniers. Aucune de ces règles ne s’applique lorsque vous utilisez des sélecteurs pour interroger plutôt que pour styliser.
Les sélecteurs CSS sont-ils meilleurs que l’XPath pour le scraping ?
Pour la sélection par classe, identifiant, attribut et structure, oui — ils sont plus courts et plus clairs. Pour faire correspondre du contenu textuel ou remonter jusqu’à un ancêtre arbitraire, XPath est indispensable. La plupart des bibliothèques de scraping prennent en charge les deux, et il est courant d’utiliser chacune selon son domaine d’application.
Pourquoi mon sélecteur fonctionne-t-il dans le navigateur mais pas dans mon scraper ?
Il y a deux causes courantes. Le DOM du navigateur est généré après l’exécution du JavaScript, tandis que votre parseur traite le HTML brut ; il se peut donc que l’élément n’existe pas dans les données que vous avez récupérées. De plus, les implémentations CSS côté serveur ne prennent pas en charge tous les sélecteurs modernes — notamment :has(), dont la prise en charge est inégale en dehors des navigateurs.
En conclusion
Le cœur fonctionnel des sélecteurs CSS est restreint : le type, la classe et l’identifiant pour l’identité, les quatre combinateurs pour les relations, les sélecteurs d’attributs pour tout élément choisi délibérément, et une poignée de pseudo-classes pour la position et l’état.
Trois habitudes permettent d’améliorer davantage les sélecteurs concrets que la simple connaissance d’une syntaxe supplémentaire. Préférez > à un simple espace lorsque la relation est véritablement directe, car un sélecteur qui cesse de correspondre est plus utile que celui qui commence à correspondre à trop d’éléments. Préférez les attributs data-* aux noms de classes, car les classes sont générées automatiquement par les outils de compilation tandis que les attributs sont choisis par les utilisateurs. Et préférez :nth-of-type à :nth-child lorsque vous voulez désigner « le deuxième paragraphe » plutôt que « le deuxième enfant ».
:has() C’est une amélioration qui mérite d’être adoptée délibérément. La sélection des parents et les conditions de frères et sœurs en CSS standard éliminent une grande partie de ce qui obligeait auparavant les utilisateurs à recourir à XPath, et cette fonctionnalité est disponible depuis suffisamment longtemps pour être fiable dans un navigateur. En dehors des navigateurs, vérifiez d’abord votre analyseur syntaxique — c’est là que se cachent les dernières surprises.
