Dans l'article précédent — « Parser le DOM » — nous avons vu comment le navigateur et les bibliothèques transforment le HTML en arbre de nœuds, et comment parcourir cet arbre par programmation. Mais parcourir le DOM « à la main », en passant de parent en enfant, est fastidieux et fragile. En pratique, dans le web scraping, le nœud recherché est presque toujours adressé par une seule expression courte : un sélecteur CSS.
Les sélecteurs CSS ont été conçus à l'origine pour la mise en page : ils décrivent à quels éléments appliquer un style. Mais la même syntaxe convient parfaitement pour trouver des éléments. C'est pourquoi presque tous les outils de scraping — de BeautifulSoup à Scrapy et Playwright — prennent en charge la sélection par CSS. Passons les constructions en revue, dans l'ordre.
Pourquoi des sélecteurs CSS en scraping
Un sélecteur est une façon déclarative de dire « donne-moi tous les éléments qui ressemblent à ceci ». Au lieu d'une dizaine de lignes de parcours d'arbre, vous écrivez une seule ligne, et le moteur descend lui-même dans le DOM pour renvoyer les correspondances. C'est :
- compact — un sélecteur au lieu d'une boucle ;
- lisible — l'expression ressemble à une description de l'emplacement de l'élément sur la page ;
- portable — le même sélecteur fonctionne en Python, en JS, dans la console du navigateur (
document.querySelectorAll).
La compétence clé du scraper : examiner la structure de la page dans les DevTools et composer un sélecteur qui atteint précisément la cible tout en survivant aux petites modifications de la mise en page.
Sélecteurs de base
Ce sont les atomes à partir desquels tout le reste se construit.
| Sélecteur | Ce qu'il sélectionne | Exemple |
|---|---|---|
* |
n'importe quel élément | * |
div |
les éléments par nom de balise | a, span, tr |
.price |
les éléments avec la classe price |
.product-card |
#main |
l'élément avec id="main" |
#catalog |
La classe et la balise peuvent être accolées sans espace — c'est un « ET » logique : a.button signifie « un lien qui possède la classe button ». Et div.card.featured — un div avec deux classes à la fois.
span.price.current /* <span class="price current"> */
li#first-item /* <li id="first-item"> */
Regroupement
La virgule combine plusieurs sélecteurs en une seule liste — sont renvoyés les éléments correspondant à l'un quelconque d'entre eux :
h1, h2, h3 /* tous les titres de ces trois niveaux */
.price-new, .price-old /* le nouveau prix et l'ancien */
Pratique quand le produit porte la classe price-new sur certaines fiches et price-old sur d'autres, et qu'il faut collecter les deux variantes en une seule requête.
Combinateurs : les relations entre éléments
Les combinateurs décrivent comment les éléments sont positionnés les uns par rapport aux autres dans l'arbre. C'est la partie la plus importante pour un scraping robuste.
Descendant (espace)
A B — un élément B situé quelque part à l'intérieur de A, à n'importe quelle profondeur.
.product-card .price /* .price n'importe où dans la fiche produit */
Enfant direct (>)
A > B — B est un enfant immédiat de A. Il ne descend pas plus profond.
ul.menu > li /* uniquement les entrées de premier niveau du menu, sans les imbriquées */
La différence est fondamentale : .menu li capturera aussi les entrées des sous-menus, alors que .menu > li non.
Frère adjacent (+)
A + B — un élément B qui vient immédiatement après A et a le même parent.
h2 + p /* le premier paragraphe juste après le titre */
.label + .value /* la valeur placée juste après le libellé */
Frère général (~)
A ~ B — tous les B qui viennent après A chez le même parent (pas forcément juste après).
.in-stock ~ .price /* tous les prix qui suivent le badge « en stock » */
Sélecteurs d'attributs
Souvent, un élément n'a pas de classe pratique, mais possède un attribut caractéristique. Les sélecteurs d'attributs sont précieux quand les classes sont dynamiques ou bruitées.
| Construction | Signification |
|---|---|
[data-id] |
l'attribut data-id est présent (quelle que soit sa valeur) |
[type="submit"] |
correspondance exacte de la valeur |
[class~="price"] |
la valeur contient le mot price parmi des mots séparés par des espaces |
[href^="https"] |
la valeur commence par https |
[href$=".pdf"] |
la valeur se termine par .pdf |
[href*="product"] |
la valeur contient la sous-chaîne product |
[lang|="en"] |
la valeur est égale à en ou commence par en- |
[type="text" i] |
drapeau i — comparaison insensible à la casse |
a[href^="/catalog/"] /* liens internes au catalogue */
img[src$=".webp"] /* images au format webp */
div[data-testid*="product"] /* conteneurs avec un marqueur produit dans l'attribut data */
input[name="csrf"] /* champ caché du token */
Les sélecteurs d'attributs sont particulièrement précieux en veille tarifaire des boutiques en ligne : les grandes plateformes exposent souvent les données dans des attributs data-* (data-price, data-sku, data-currency), et s'y accrocher est plus fiable que de dépendre de classes qui changent à chaque release du front-end.
Pseudo-classes : sélection par position et par état
Les pseudo-classes filtrent les éléments selon leur place parmi leurs voisins ou selon des conditions supplémentaires.
Positionnelles
li:first-child /* premier enfant */
li:last-child /* dernier enfant */
tr:nth-child(2) /* deuxième ligne */
tr:nth-child(odd) /* lignes impaires */
tr:nth-child(even) /* lignes paires */
tr:nth-child(3n) /* une ligne sur trois */
li:nth-last-child(1) /* premier en partant de la fin */
La formule an+b de :nth-child() est flexible : nth-child(2n+1) — les impairs, nth-child(n+3) — tous à partir du troisième.
Par type de balise
:nth-child compte tous les voisins à la suite, tandis que :nth-of-type ne compte que les éléments de la même balise :
p:first-of-type /* premier paragraphe parmi les voisins */
img:nth-of-type(2) /* deuxième image dans l'ordre */
span:last-of-type /* dernier span */
Cela sauve la mise quand le <p> recherché arrive en deuxième paragraphe, mais que des balises d'un autre type se sont intercalées avant lui.
Négation et état
input:not([type="hidden"]) /* tous les champs, sauf les champs cachés */
li:not(.sold-out) /* les produits qui ne sont pas « épuisés » */
a:not([rel="nofollow"]) /* les liens sans nofollow */
:not() accepte un autre sélecteur à l'intérieur et l'inverse. Sont également pris en charge :empty (éléments vides), :checked, :disabled et d'autres — mais en scraping statique, ce sont surtout les pseudo-classes positionnelles et :not() qui servent.
Attention : les pseudo-éléments (
::before,::after,::first-line) ne sont pas utilisés en scraping — ils décrivent du contenu généré par les styles, absent du HTML source.
Composer un sélecteur complexe
Les expressions réelles combinent tout ce qui précède. Exemple — extraire le prix actuel d'une fiche produit dans un catalogue :
div.product-card a[href^="/product/"] span.price:not(.price--old)
Cela se lit ainsi : à l'intérieur de la fiche produit, dans le lien vers la page du produit, prendre le span avec la classe price qui n'est pas l'ancien prix (barré).
Outils : où cela fonctionne
Les sélecteurs CSS sont pris en charge par la quasi-totalité de la stack de scraping.
Python — BeautifulSoup (via la bibliothèque soupsieve) :
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
# .select() renvoie une liste, .select_one() — le premier élément
cards = soup.select("div.product-card")
for card in cards:
title = card.select_one("h3.title").get_text(strip=True)
price = card.select_one("span.price:not(.price--old)").get_text(strip=True)
print(title, price)
Python — parsel / Scrapy (surcouche de lxml, rapide) :
from parsel import Selector
sel = Selector(text=html)
prices = sel.css("span.price::text").getall() # ::text — extension spécifique de parsel
links = sel.css("a.product-link::attr(href)").getall()
Ici, ::text et ::attr(name) sont des pseudo-éléments non standard de Scrapy/parsel : ils extraient directement le texte ou l'attribut, sans appel supplémentaire.
JavaScript / navigateur — API native, même syntaxe :
const cards = document.querySelectorAll("div.product-card");
const price = document.querySelector("span.price:not(.price--old)")?.textContent.trim();
Playwright / Selenium — pour les pages dynamiques, où les données sont chargées par des scripts :
# Playwright
price = page.locator("div.product-card span.price").first.inner_text()
Autrement dit, une fois les sélecteurs maîtrisés, vous les utilisez aussi bien dans des scrapers HTTP légers que dans des navigateurs headless.
Application à la veille tarifaire
Dans une tâche typique de veille tarifaire des boutiques en ligne, le scénario est le suivant :
- Liste des produits du catalogue. Un sélecteur du type
div.catalog__itemtrouve toutes les fiches sur la page de résultats. - Lien vers le produit. Dans chaque fiche,
a[href^="/product/"]::attr(href)extrait l'URL. - Prix et disponibilité. Sur la page produit,
[data-price]::attr(data-price)ouspan.price--currentdonne le prix, et.availability:not(.out-of-stock)confirme que le produit est en vente. - Ancien prix et remise.
span.price--oldcapture le prix barré, afin de calculer le montant de la remise.
Comme la mise en page diffère d'un concurrent à l'autre, on écrit un jeu de sélecteurs propre à chaque plateforme, et le moteur d'extraction de données web les applique selon un calendrier et consolide le résultat dans un tableau commun de comparaison.
Erreurs fréquentes et conseils
- Ne vous accrochez pas aux classes « poubelle ». Les classes du type
.css-1ab2c3sont générées par les bundlers et changent à chaque déploiement. Préférez les attributsdata-*, desidsignificatifs ou des combinateurs structurels. - Utilisez
>là où la structure compte. Cela élimine les correspondances accidentelles dans les blocs imbriqués. - Testez le sélecteur dans les DevTools. La commande
$$("votre sélecteur")dans la console du navigateur montre immédiatement combien d'éléments ont été trouvés. - Prévoyez des variantes. Énumérez, séparées par des virgules, plusieurs classes de prix possibles — cela renforce la résistance aux tests A/B de la boutique.
- Gardez en tête les limites du CSS. Les sélecteurs ne savent pas « remonter au parent » selon le contenu, ni filtrer par le texte d'un nœud. Là où le CSS ne suffit plus, on passe à XPath ou aux expressions régulières — nous les verrons dans le prochain article.
Conclusion
Les sélecteurs CSS sont l'outil principal d'adressage dans le DOM. Les sélecteurs de base (balise, classe, id) définissent le « quoi », les combinateurs (, >, +, ~) le « où », et les sélecteurs d'attributs et pseudo-classes ajoutent des conditions précises. Ensemble, ils couvrent l'immense majorité des tâches de web scraping, et un sélecteur bien conçu rend le scraper résistant aux petites retouches de la mise en page — un point critique lorsqu'il s'agit d'une veille tarifaire des boutiques en ligne régulière.
Dans le prochain article, nous verrons comment extraire des données du texte « brut » à l'aide des expressions régulières — là où la structure du DOM ne suffit plus.