📍 Guide principal : Web scraping en Python : le guide complet.
Deuxième étape de la série. Elle suppose que vous savez déjà extraire les données d’une page avec requests et BeautifulSoup ; on part de la fonction
extraire_livresécrite précédemment.
Aminata a maintenant les vingt premiers livres dans son terminal. Mais son grossiste publie 1000 références réparties sur 50 pages, et un affichage qui disparaît à la fermeture du terminal ne lui sert à rien. Ce qu’elle veut, c’est un fichier qu’elle ouvre dans son tableur, trie par prix, et compare à celui de la semaine dernière pour repérer les hausses. Le travail d’aujourd’hui : parcourir tout le catalogue, transformer les données brutes en données fiables, et les ranger là où on les retrouvera.
C’est souvent l’étape négligée des tutoriels — on s’arrête à « j’ai récupéré quelque chose ». Or des données sales (prix en texte, doublons, lignes vides) sont inutilisables pour décider. Le scraping ne vaut que par ce qu’on en fait ensuite.
🎯 Ce que vous allez apprendre
- Suivre automatiquement le lien « page suivante » jusqu’à la dernière page, sans coder le nombre de pages en dur.
- Espacer vos requêtes pour parcourir un site entier sans le surcharger.
- Nettoyer un jeu de données avec
pandas: typer les colonnes, retirer les doublons, gérer les valeurs manquantes. - Exporter le résultat en CSV lisible par Excel et le stocker en base SQLite pour comparer dans le temps.
🛠️ Ce que vous allez construire
Un script collecte.py qui ramène les 1000 références du catalogue, les nettoie, supprime les doublons, puis écrit deux sorties : livres.csv (pour Aminata et son tableur) et une table dans veille.db (pour l’historique). À chaque exécution hebdomadaire, une nouvelle photo du catalogue vient s’ajouter à la base.
Prérequis
- Avoir terminé le premier tutoriel (environnement,
requests,BeautifulSoup). - Un paquet supplémentaire :
pip install pandas.sqlite3, lui, est livré avec Python. - ⏱️ Temps estimé : ~40 minutes.
Étape 1 — Trouver le lien « page suivante »
La pagination d’un site suit presque toujours le même principe : un bouton « suivant » pointe vers la page d’après. Plutôt que de deviner les URL (page-1.html, page-2.html…), la méthode robuste consiste à lire, sur chaque page, le lien vers la suivante — et à s’arrêter quand il n’y en a plus. Le code s’adapte ainsi tout seul si le catalogue grandit.
Sur ce catalogue, le bouton vit dans <li class="next"><a href="page-2.html">. Le href est relatif : il faut le recombiner avec l’adresse de la page courante. C’est le rôle de urljoin.
from urllib.parse import urljoin
def lien_suivant(soup, url_courante):
bouton = soup.select_one("li.next a")
if bouton is None:
return None # on est sur la dernière page
return urljoin(url_courante, bouton["href"])
La fonction renvoie l’URL complète de la page suivante, ou None sur la dernière page. Ce None est notre signal d’arrêt : il évite la boucle infinie, l’erreur la plus banale du scraping de pagination.
Étape 2 — Parcourir toutes les pages, poliment
On enchaîne maintenant : télécharger une page, en extraire les livres, trouver la suivante, recommencer. Deux ajouts par rapport au premier tutoriel. D’abord une Session requests, qui réutilise la même connexion réseau d’une page à l’autre — plus rapide et plus léger pour le serveur. Ensuite un time.sleep entre chaque page : rien ne presse, et marteler un serveur à pleine vitesse est à la fois impoli et le meilleur moyen de se faire bloquer.
import time
import requests
from bs4 import BeautifulSoup
DEPART = "https://books.toscrape.com/catalogue/page-1.html"
EN_TETES = {"User-Agent": "VeilleManuels/1.0 (contact@exemple.sn)"}
def collecter_tout():
session = requests.Session()
session.headers.update(EN_TETES)
url = DEPART
tous = []
page = 0
while url:
page += 1
reponse = session.get(url, timeout=10)
reponse.raise_for_status()
soup = BeautifulSoup(reponse.text, "lxml")
tous.extend(extraire_livres(soup)) # fonction du tutoriel précédent
print(f"Page {page} : {len(tous)} livres cumulés")
url = lien_suivant(soup, url)
time.sleep(1) # une seconde de respiration
return tous
livres = collecter_tout()
La boucle while url tourne tant qu’il reste une page suivante. Chaque tour ajoute ses vingt livres à la liste tous. L’affichage cumulé sert de jauge de progression — utile pour ne pas croire le script planté alors qu’il travaille. Au bout d’une cinquantaine de secondes, vous devez lire Page 50 : 1000 livres cumulés.
✅ Point d’étape — Vous devez obtenir exactement 1000 livres. Un nombre inférieur signale une page tombée en erreur (vérifiez les statuts) ; un nombre qui ne s’arrête jamais signale que
lien_suivantne renvoie pasNoneà la fin.
Étape 2 bis — Encaisser les erreurs réseau
Sur cinquante requêtes successives, la probabilité qu’au moins une échoue n’est pas négligeable : un délai d’attente dépassé, une coupure d’une seconde, un 503 passager pendant que le serveur respire. Sans protection, votre collecte meurt brutalement à la page 37 et vous perdez les trente-six précédentes. La parade n’est pas d’espérer que le réseau tienne, mais de prévoir l’échec : on réessaie quelques fois, en patientant un peu plus à chaque tentative.
def telecharger(session, url, essais=3):
for tentative in range(1, essais + 1):
try:
reponse = session.get(url, timeout=10)
reponse.raise_for_status()
return reponse
except requests.RequestException as erreur:
attente = 2 ** tentative # 2 s, puis 4 s, puis 8 s
print(f" Echec ({erreur}) - nouvel essai dans {attente} s")
time.sleep(attente)
raise RuntimeError(f"Abandon apres {essais} tentatives : {url}")
On attrape requests.RequestException, la classe mère de toutes les erreurs de la bibliothèque (délai dépassé, connexion refusée, statut d’erreur). Entre deux tentatives, l’attente double : c’est le backoff exponentiel, qui laisse au serveur le temps de se remettre au lieu de le harceler. Au bout de trois échecs, on abandonne franchement plutôt que de boucler sans fin. Dans collecter_tout, il suffit alors de remplacer les deux lignes session.get et raise_for_status par un simple reponse = telecharger(session, url).
Écrire cette résilience à la main pour un script d’apprentissage est instructif. Mais la maintenir — gérer en plus les redirections, la concurrence, la reprise après interruption — devient vite un projet en soi. C’est précisément ce qu’un cadre comme Scrapy fournit déjà, réglé et éprouvé ; on y vient dans les deux tutoriels suivants.
✅ Point d’étape — Coupez volontairement votre connexion une seconde pendant l’exécution : le script doit afficher un message de nouvel essai puis repartir, sans planter. C’est le signe que votre collecte survit aux aléas du réseau.
Étape 3 — Charger les données dans pandas
Une liste de 1000 dictionnaires est exploitable, mais malcommode pour nettoyer et analyser. pandas la transforme en DataFrame : un tableau en mémoire, avec des colonnes typées, sur lequel filtrer, trier et corriger devient trivial. C’est l’outil standard de la donnée en Python.
import pandas as pd
df = pd.DataFrame(livres)
print(df.shape) # (1000, 3) : 1000 lignes, 3 colonnes
print(df.head())
print(df.dtypes) # vérifier que "prix" est bien un nombre
df.shape confirme les dimensions, df.head() montre les premières lignes, et df.dtypes révèle le type de chaque colonne. C’est ce dernier qu’on surveille : si prix apparaît comme object (du texte) au lieu de float64, les calculs et les tris seront faux. Comme on a déjà converti le prix en float à l’extraction, il devrait être correct — mais on ne fait jamais confiance sur parole à des données.
Étape 4 — Nettoyer le jeu de données
Le nettoyage répond à trois questions simples : y a-t-il des doublons ? des trous ? des valeurs aberrantes ? On les traite dans cet ordre. Un même livre peut apparaître deux fois si une page a été visitée deux fois lors d’une reprise ; une valeur peut manquer si un élément était absent du HTML ; un prix à zéro ou négatif trahit une erreur d’extraction.
# 1. Doublons : deux livres de même titre sont la même référence ici
avant = len(df)
df = df.drop_duplicates(subset=["titre"]).reset_index(drop=True)
print(f"{avant - len(df)} doublon(s) retiré(s)")
# 2. Valeurs manquantes : on écarte les lignes sans titre ou sans prix
df = df.dropna(subset=["titre", "prix"])
# 3. Valeurs aberrantes : un prix doit être strictement positif
df = df[df["prix"] > 0]
# 4. Normaliser le texte : retirer les espaces parasites des titres
df["titre"] = df["titre"].str.strip()
print(f"Jeu nettoyé : {len(df)} références")
Chaque opération pandas renvoie un nouveau DataFrame filtré, qu’on réassigne à df. Le reset_index(drop=True) après suppression de doublons renumérote proprement les lignes, sans quoi les index gardent des trous. Affichez systématiquement le nombre de lignes avant/après : c’est votre garde-fou contre un filtre trop agressif qui viderait le tableau sans prévenir.
Étape 5 — Exporter en CSV lisible par un tableur
Aminata travaille dans un tableur. Le CSV est le pont universel — mais il a un piège bien connu en contexte francophone : ouvert dans Excel, un fichier UTF-8 « ordinaire » affiche les accents en charabia. La parade tient en un paramètre : l’encodage utf-8-sig, qui ajoute la marque que les tableurs attendent.
from datetime import date
df["date_releve"] = date.today().isoformat() # tracer la date de la collecte
df.to_csv("livres.csv", index=False, encoding="utf-8-sig")
print("livres.csv écrit")
On ajoute une colonne date_releve avant d’exporter : sans elle, impossible de savoir de quand date la photo du catalogue. Le index=False évite d’écrire la colonne d’index technique de pandas, inutile pour Aminata. Ouvrez livres.csv dans votre tableur : les accents sont corrects et les colonnes bien séparées.
Étape 6 — Stocker l’historique en SQLite et vérifier
Le CSV est parfait pour une photo, mais Aminata veut comparer les semaines. Réécraser le même fichier perd l’historique. SQLite — une base de données contenue dans un seul fichier, sans serveur à installer — répond exactement à ce besoin : on y ajoute chaque collecte, et on interroge l’évolution avec du SQL.
import sqlite3
con = sqlite3.connect("veille.db")
df.to_sql("releves", con, if_exists="append", index=False)
# Vérification : combien de relevés, sur combien de dates ?
verif = pd.read_sql(
"SELECT date_releve, COUNT(*) AS n FROM releves GROUP BY date_releve",
con,
)
print(verif)
con.close()
Le if_exists="append" est la clé : chaque exécution ajoute ses lignes au lieu de remplacer la table. La requête de vérification regroupe par date et compte : après une première exécution, vous voyez une ligne (la date du jour, 1000 relevés) ; la semaine suivante, une seconde apparaîtra. Aminata pourra alors écrire une requête comparant les prix entre deux dates — mais ça, c’est le métier de la base, pas du scraper. La collecte, elle, est terminée et fiable.
✅ Point d’étape —
veille.dbexiste dans votre dossier et la requête affiche une ligne par date de collecte. Relancez le script : une seule date doit voir son compteur monter si vous relancez le même jour (les doublons de titre étant retirés en amont, c’est attendu).
🐞 Pièges fréquents
| Symptôme / erreur | Cause probable | Correctif |
|---|---|---|
| La boucle ne s’arrête jamais | lien_suivant renvoie toujours une URL |
Vérifier le sélecteur li.next a ; s’assurer qu’il renvoie None sur la dernière page |
| Accents en charabia dans Excel | CSV exporté en UTF-8 sans marque | Utiliser encoding="utf-8-sig" dans to_csv |
prix de type object |
Conversion en nombre oubliée | df["prix"] = pd.to_numeric(df["prix"], errors="coerce") |
| Le fichier SQLite gonfle à chaque essai | if_exists="append" pendant la mise au point |
Pendant les tests, utiliser "replace" ; passer à "append" en production |
🌍 Adaptation au contexte ouest-africain
Parcourir 50 pages, c’est 50 requêtes réseau — et autant de data consommée à chaque essai. Pendant la mise au point, limitez-vous volontairement aux trois premières pages (ajoutez if page >= 3: break dans la boucle) : vous validez toute la logique de pagination et de nettoyage sur 60 livres avant de lâcher le script sur les 1000. C’est aussi une bonne discipline tout court : on ne lance une collecte complète qu’une fois sûr du résultat. Pensez enfin à exécuter ce type de tâche aux heures creuses, quand la bande passante partagée d’un bureau à Cotonou ou Lomé n’est pas saturée.
✅ Récapitulatif
Vous avez transformé un extracteur d’une page en un collecteur de catalogue entier : suivi automatique de la pagination jusqu’au signal d’arrêt, requêtes espacées par politesse, puis un pipeline de nettoyage pandas (doublons, trous, aberrations) et deux sorties complémentaires — CSV pour l’usage immédiat, SQLite pour l’historique. « VeilleManuels » sait désormais photographier tout le catalogue et garder la mémoire de ses photos. La limite de l’approche artisanale commence pourtant à poindre : gérer les erreurs, la reprise, la concurrence, tout cela à la main devient lourd. C’est là qu’un cadre dédié entre en jeu.
🧾 Aide-mémoire
| Élément | Rôle |
|---|---|
urljoin(base, href) |
Reconstruire une URL absolue depuis un lien relatif |
requests.Session() |
Réutiliser la connexion sur plusieurs requêtes |
time.sleep(1) |
Espacer les requêtes (politesse) |
pd.DataFrame(liste) |
Créer un tableau depuis une liste de dictionnaires |
df.drop_duplicates(subset=) |
Retirer les doublons |
df.to_csv(path, encoding="utf-8-sig") |
Exporter pour un tableur |
df.to_sql(table, con, if_exists="append") |
Ajouter à une base SQLite |
💪 À vous de jouer
Aminata veut sa « top liste » : les dix livres en stock les moins chers, triés par prix croissant, exportés à part. Écrivez la requête pandas correspondante.
Voir une solution
top10 = (
df[df["en_stock"]]
.sort_values("prix")
.head(10)
)
top10.to_csv("moins_chers.csv", index=False, encoding="utf-8-sig")
On filtre d’abord les livres en stock, on trie par prix croissant, on garde les dix premiers. Le chaînage des opérations se lit comme une phrase — c’est la force de pandas.
Tutoriels frères
- Extraire des données avec requests et BeautifulSoup — la brique d’extraction réutilisée ici.
- Scrapy avancé : pagination, throttling et pipelines — le même travail, mais industrialisé par un framework.
Pour aller plus loin
- 🔝 Retour au guide complet du web scraping en Python.
- Documentation officielle : pandas et sqlite3.
- Quand le script artisanal montre ses limites : passer à Scrapy.
FAQ
Pourquoi suivre le lien « suivant » plutôt que construire les URL ?
Parce que c’est robuste : si le nombre de pages change ou si le motif d’URL évolue, votre code continue de fonctionner sans modification. Construire les URL en dur casse au premier changement.
CSV ou SQLite, lequel choisir ?
Les deux, pour des usages différents. Le CSV se partage et s’ouvre dans un tableur. SQLite garde l’historique et permet des requêtes (évolution des prix, ruptures récurrentes) que le CSV rend pénibles.
Une seconde d’attente par page, n’est-ce pas trop lent ?
Cinquante secondes pour un catalogue entier, une fois par semaine, c’est négligeable. La vitesse n’est pas l’objectif ; ne pas nuire au serveur et ne pas se faire bloquer, si. Le scraping responsable développe ce point.