Le web scraping vous donne les données, mais les stocker et les analyser est tout aussi important. Un dossier de fichiers JSON ou CSV épars convient pour une extraction ponctuelle, mais dès que vous voulez suivre des prix dans le temps, comparer des enregistrements provenant de plusieurs sources, ou répondre à une vraie question, cette pile plate devient le goulot d'étranglement. Une base de données SQL offre aux données scrapées un foyer structuré que vous pouvez interroger, filtrer et agréger sans écrire du code d'analyse jetable à chaque fois.
Ce guide construit le chemin complet de bout en bout avec Python et SQL. Vous scrapez un petit ensemble d'enregistrements de type produit depuis une page d'exemple neutre via la Crawling API, concevez un schéma SQL, insérez les lignes analysées, et exécutez des requêtes analytiques pour en extraire des insights. Le périmètre reste limité aux données publiques et illustratives avec une URL de remplacement, de sorte que vous pouvez le suivre en toute sécurité et substituer votre propre cible plus tard.
Ce que vous allez construire
Un flux Python exécutable qui récupère une page de listing rendue via la Crawling API, analyse chaque carte produit avec BeautifulSoup, écrit les enregistrements dans une table SQLite, puis interroge cette table pour des tranches de prix, des moyennes et des comptages par catégorie. Chaque enregistrement scrapé contient ces champs :
- Name le titre du produit affiché sur la carte de listing.
- Price le prix affiché, stocké sous forme de nombre pour pouvoir le comparer et l'agréger.
- Category la section ou le type de produit auquel l'article appartient.
- URL le lien vers la page propre du produit.
- Scraped at un horodatage que la base de données remplit automatiquement, pour suivre quand chaque ligne a été collectée.
Pourquoi stocker les données scrapées en SQL
L'endroit où les données atterrissent décide de leur utilité ultérieure. Les fichiers fonctionnent jusqu'à ce que vous ayez besoin de poser une question qui couvre plusieurs lignes : quels articles sont sous un seuil de prix, quel est le prix moyen par catégorie, comment l'extraction de cette semaine se compare à celle de la semaine dernière. SQL a été conçu exactement pour cela, et une base de données relationnelle vous offre quelques choses qu'un dossier de fichiers ne donne pas.
- Stockage structuré. Les tables et colonnes imposent une forme cohérente, de sorte que chaque enregistrement a les mêmes champs dans les mêmes types.
-
Interrogation efficace. Un seul
SELECTfiltre, trie et agrège des milliers de lignes sans boucle manuelle. - Intégrité des données. Les clés primaires, types et contraintes gardent les données cohérentes et détectent les lignes malformées à l'entrée.
- Scalabilité. Que vous stockiez des milliers ou des millions d'enregistrements, un moteur SQL gère la croissance et reste rapide avec les bons index.
- Analyse reproductible. Les requêtes sont réutilisables. Une fois que vous en écrivez une utile, vous la réexécutez sur les données de demain sans rien reconstruire.
Si vous pesez plus largement les formats de stockage, la comparaison dans JSON vs CSV couvre quand un fichier plat reste le bon choix, et le guide de modélisation des données va plus loin sur la conception de schéma que nous n'avons pas la place de couvrir ici.
Pourquoi une requête simple échoue sur une page de listing moderne
Avant que le travail de stockage ait de l'importance, vous avez besoin d'enregistrements propres à stocker, et c'est là qu'une requête HTTP naïve tend à tomber. De nombreuses pages de listing modernes rendent leur contenu dans le navigateur via JavaScript : la première réponse HTML est un shell mince, et les cartes produits n'apparaissent qu'après l'exécution des scripts de la page. Extrayez des données de cette réponse initiale et vous capturez un fragment de la liste, ou rien du tout.
Le second problème est l'accès. Les sites qui publient des listings à valeur commerciale surveillent le trafic automatisé et bloquent les requêtes provenant d'IPs de datacenter ou de tout ce qui ne ressemble pas à un vrai navigateur. Un scraper fiable a donc besoin de deux choses en une seule requête : un navigateur qui rend la page, et une IP que le site lit comme un visiteur authentique. Vous pouvez construire cela avec un navigateur headless et un pool de proxies résidentiels rotatifs, mais maintenir cette pile en bonne santé représente l'essentiel du travail. La Crawling API plie les deux dans un seul appel et retourne du HTML fini que vous pouvez analyser. Pour le côté analyse, le guide de web scraping Python et le guide de structuration des données scrapées sont des compagnons utiles.
Prérequis
Quelques éléments doivent être en place avant d'écrire du code. Aucun ne prend longtemps.
Python et SQL de base. Vous devez être à l'aise pour exécuter un script Python et lire une instruction SELECT. Vous n'avez pas besoin d'être administrateur de base de données ; ce guide utilise SQLite, qui est livré avec Python, donc il n'y a pas de serveur à installer.
Python 3.8 ou supérieur. Confirmez votre version avec python --version. Si vous ne l'avez pas, installez-le depuis python.org et assurez-vous que Python est dans votre PATH.
Un compte et un token Crawlbase. Inscrivez-vous, ouvrez votre tableau de bord, et copiez votre token. Crawlbase inclut jusqu'à 20 000 requêtes gratuites pour commencer, ce qui est largement suffisant pour parcourir ce guide. Traitez le token comme un mot de passe et gardez-le hors du contrôle de version. Si votre cible rend le contenu côté client, utilisez le token JavaScript (JS) pour que la page soit rendue avant que vous la receviez.
Configurer le projet
Créez un environnement virtuel pour que les dépendances restent isolées, puis installez les deux bibliothèques dont le scraper a besoin.
python --version python -m venv scraper_env source scraper_env/bin/activate pip install crawlbase beautifulsoup4
Sur Windows, activez l'environnement avec scraper_env\Scripts\activate au lieu de la ligne source. Le package crawlbase est le client officiel pour la Crawling API, et beautifulsoup4 analyse le HTML retourné pour que vous puissiez extraire des champs individuels. Les modules sqlite3 et json sont livrés avec la bibliothèque standard Python, donc rien de plus à installer pour l'étape de stockage.
Étape 1 : Récupérer une page rendue
Commencez par obtenir une page finie. Importez la classe CrawlingAPI, initialisez-la avec votre token, et demandez l'URL de listing. Nous utilisons un espace réservé, https://example.com/products ; pointez-le vers n'importe quelle page de listing publique que vous avez le droit de collecter. Vérifier le cb_status (legacy pc_status) de Crawlbase avant d'analyser garde les échecs visibles plutôt que silencieux.
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) LISTING_URL = "https://example.com/products" def crawl(page_url): response = api.get(page_url) if response["headers"]["cb_status"] == "200": return response["body"].decode("utf-8") print(f"Request failed: {response['headers']['cb_status']}") return None if __name__ == "__main__": html = crawl(LISTING_URL) print(html[:500] if html else "No HTML returned")
Exécutez ceci et vous devriez voir le vrai balisage de la page s'afficher, ce qui confirme que la requête a fonctionné avant d'écrire un seul sélecteur. Si votre cible rend ses cartes côté client, passez le token JS à la place et ajoutez {"ajax_wait": "true", "page_wait": 5000} comme second argument à api.get pour que le contenu dynamique ait le temps de se charger.
La fonction crawl ci-dessus cache la partie difficile : obtenir du HTML fini depuis une page qui se rend dans le navigateur et bloque le trafic de datacenter. La Crawling API exécute la page dans un vrai navigateur, fait tourner des IPs résidentielles côté serveur, et vous donne le HTML rendu en un seul appel, pour vous éviter de gérer vous-même une flotte headless et un pool de proxies. Commencez sur le palier gratuit et pointez-la vers une page de listing publique d'abord.
Étape 2 : Analyser les enregistrements à stocker
Avec du HTML fini en main, chargez-le dans BeautifulSoup et extrayez un enregistrement de chaque carte produit. Les sélecteurs ci-dessous supposent une mise en page avec un titre, un prix, une balise de catégorie, et un lien, une forme courante pour les pages de listing. Chaque recherche est protégée pour qu'un champ manquant retourne une valeur par défaut sûre au lieu de faire planter l'exécution. Le détail qui compte pour le stockage est le prix : scrapez-le comme texte, puis supprimez le symbole monétaire et les virgules pour qu'il atterrisse dans la base de données comme un nombre que vous pouvez comparer et moyenner.
import re from bs4 import BeautifulSoup def parse_price(text): if not text: return None cleaned = re.sub(r"[^0-9.]", "", text) return float(cleaned) if cleaned else None def parse_products(html): soup = BeautifulSoup(html, "html.parser") records = [] for card in soup.select("div.product-card"): name = card.select_one("h2.product-title") price = card.select_one("span.price") category = card.select_one("span.category") link = card.select_one("a.product-link") records.append({ "name": name.get_text(strip=True) if name else None, "price": parse_price(price.get_text() if price else None), "category": category.get_text(strip=True) if category else "Uncategorized", "url": link["href"] if link and link.get("href") else None, }) return records
parse_price supprime tout ce qui n'est pas un chiffre ou un point décimal, de sorte que "$1,299.00" devient le float 1299.0. Cette seule étape rend possibles les requêtes de prix ultérieures : le texte se trie alphabétiquement et ne peut pas être moyenné, mais un nombre fait les deux correctement. Ajustez les sélecteurs (product-card, product-title, price, category, product-link) pour correspondre au balisage de votre vraie cible ; le reste du flux reste identique.
Étape 3 : Concevoir le schéma SQL
Concevez maintenant la table qui contient ces enregistrements. Un bon schéma reflète la forme de votre enregistrement, choisit le bon type par colonne, et ajoute une clé primaire plus un horodatage que la base de données remplit pour vous. Notez les choix ci-dessous : price est REAL (un nombre, pas du texte) pour pouvoir être comparé et agrégé, et scraped_at prend par défaut l'heure actuelle pour que chaque ligne enregistre quand elle a été collectée.
CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, price REAL, category TEXT, url TEXT UNIQUE, scraped_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Speed up the price filters and category rollups you will run later CREATE INDEX IF NOT EXISTS idx_price ON products(price); CREATE INDEX IF NOT EXISTS idx_category ON products(category);
Deux choix de conception méritent d'être signalés. La contrainte UNIQUE sur url empêche le même produit d'être inséré deux fois, ce qui compte parce que les scrapers se relancent et vous ne voulez pas que les lignes d'hier soient dupliquées aujourd'hui. Les index maintiennent les requêtes de prix et de catégorie rapides à mesure que la table grandit. Sur MySQL ou PostgreSQL le schéma se lit presque identiquement : écrivez AUTO_INCREMENT ou SERIAL à la place de AUTOINCREMENT, et DECIMAL(10,2) est un choix plus strict que REAL pour de l'argent. Le guide de modélisation des données couvre quand chaque type est le bon choix.
Étape 4 : Insérer les enregistrements analysés
La table conçue, connectez-vous depuis Python, créez-la si nécessaire, et insérez les enregistrements analysés. Utilisez toujours des requêtes paramétrées : les espaces réservés ? laissent le pilote gérer l'échappement, ce qui empêche le texte malformé de casser l'insertion et ferme la porte à l'injection. executemany écrit chaque enregistrement dans un appel groupé, bien plus rapide qu'une boucle d'insertions simples quand vous avez des centaines de lignes.
import sqlite3 SCHEMA = """ CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, price REAL, category TEXT, url TEXT UNIQUE, scraped_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """ def store_records(records, db_path="scraped_data.db"): conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute(SCHEMA) rows = [ (r["name"], r["price"], r["category"], r["url"]) for r in records if r["name"] ] cursor.executemany( "INSERT OR IGNORE INTO products (name, price, category, url) " "VALUES (?, ?, ?, ?)", rows, ) conn.commit() inserted = cursor.rowcount conn.close() return inserted
INSERT OR IGNORE s'associe avec la contrainte UNIQUE sur url de l'étape 3 : une ligne dont l'URL est déjà présente est ignorée silencieusement plutôt que de lever une erreur, ce qui permet de relancer le scraper pour compléter les nouveaux listings sans dupliquer les anciens. Effectuer un commit après le lot maintient l'écriture rapide. Pour MySQL, la même forme de code s'applique avec mysql.connector à la place de sqlite3 et des espaces réservés %s au lieu de ? ; tout ce qui est en aval est identique.
Ne construisez jamais un INSERT en collant du texte scrapé directement dans la chaîne SQL. Les valeurs scrapées sont des entrées non fiables et peuvent contenir des guillemets ou des caractères qui cassent l'instruction ou, pire, la modifient. Les espaces réservés ? transmettent ce texte au pilote comme données, jamais comme code, ce qui est à la fois plus sûr et moins sujet aux erreurs.
Étape 5 : Analyser les données avec des requêtes SQL
Les données sont maintenant sous une forme interrogeable, ce qui est tout l'intérêt de SQL. Voici trois types d'analyse que vous utiliserez constamment : filtrage et tri, agrégation pour des résumés, et regroupement pour des insights par catégorie. Chacun est un simple SELECT que vous pouvez exécuter depuis le shell SQLite, un GUI de base de données, ou depuis Python.
D'abord, filtrer et trier. Ceci extrait chaque produit sous un seuil de prix, le moins cher en premier :
SELECT name, price, category FROM products WHERE price < 500 ORDER BY price ASC;
Ensuite, agrégez toute la table en un résumé d'une ligne. Les fonctions comme COUNT, AVG, MIN, et MAX condensent de nombreuses lignes en les chiffres que vous voulez réellement rapporter :
SELECT COUNT(*) AS total_products, ROUND(AVG(price), 2) AS average_price, MIN(price) AS cheapest, MAX(price) AS most_expensive FROM products;
Enfin, regroupez par catégorie pour voir comment le catalogue se répartit. GROUP BY exécute les agrégats une fois par catégorie, et HAVING filtre ces groupes après coup :
SELECT category, COUNT(*) AS items, ROUND(AVG(price), 2) AS avg_price FROM products GROUP BY category HAVING COUNT(*) > 1 ORDER BY avg_price DESC;
Pour exécuter ces requêtes depuis Python plutôt que depuis un shell, ouvrez la même connexion et relisez les lignes. Ce snippet exécute le récapitulatif par catégorie et affiche chaque groupe :
def category_summary(db_path="scraped_data.db"): conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute(""" SELECT category, COUNT(*), ROUND(AVG(price), 2) FROM products GROUP BY category ORDER BY 3 DESC """) for category, count, avg_price in cursor.fetchall(): print(f"{category}: {count} items, avg {avg_price}") conn.close()
Si votre analyse dépasse ce que le SQL simple gère confortablement, chargez la table dans un DataFrame et continuez là ; le guide d'analyse pandas reprend exactement à ce point.
Assembler le tout en un flux
Les quatre fonctions s'enchaînent en un seul script exécutable. Scraper, analyser, stocker, puis analyser :
def main(): html = crawl(LISTING_URL) if not html: return records = parse_products(html) inserted = store_records(records) print(f"Parsed {len(records)} records, inserted {inserted} new rows") category_summary() if __name__ == "__main__": main()
Chaque exécution récupère la page, n'écrit que les nouvelles lignes grâce à INSERT OR IGNORE, et affiche le résumé par catégorie. Planifiez-le dans un cron job et la table devient un historique croissant que vous pouvez interroger dans le temps, ce qui est là que SQL mérite son avantage sur un dossier de fichiers CSV datés.
À quoi ressemble le résultat
Un enregistrement stocké porte les champs structurés plus l'id et l'horodatage assignés par la base de données. Sélectionner quelques lignes retourne quelque chose comme ceci :
[ { "id": 1, "name": "Wireless Headphones", "price": 129.99, "category": "Audio", "url": "https://example.com/products/wireless-headphones", "scraped_at": "2026-06-11 09:42:18" }, { "id": 2, "name": "Mechanical Keyboard", "price": 89.50, "category": "Accessories", "url": "https://example.com/products/mechanical-keyboard", "scraped_at": "2026-06-11 09:42:18" } ]
La requête de résumé par catégorie sur cette même table affiche un récapitulatif compact, une ligne par catégorie avec un compte et un prix moyen, ce qui est le type d'insight qu'un tas de fichiers bruts ne peut pas vous donner sans code supplémentaire.
Passer à l'échelle au-delà d'une seule page
Le flux ci-dessus gère une page. Les vrais jeux de données s'étendent sur plusieurs pages, et quelques ajustements transforment cette démonstration en quelque chose de forme production.
-
Paginez. La plupart des pages de listing exposent un paramètre de page ou un lien suivant. Bouclez sur les pages, appelez
crawletparse_productspour chacune, et passez les enregistrements combinés àstore_recordsen un seul lot. -
Groupez vos insertions.
executemanyet un seulcommitpar lot minimisent déjà les allers-retours. Pour les très grands travaux, committez toutes les quelques milliers de lignes plutôt que tout d'un coup pour qu'un échec ne perde pas toute l'exécution. - Indexez ce que vous interrogez. Les deux index de l'étape 3 couvrent les filtres de prix et de catégorie. Ajoutez un index sur toute autre colonne que vous filtrez ou triez beaucoup.
- Passez en async pour le volume. Pour des milliers de pages, le Crawler async met les requêtes en file d'attente et livre les résultats à un webhook, pour que vous ne mainteniez pas de connexions ouvertes pendant le crawl. Associez-le aux insertions groupées et la base de données suit.
Si vous concevez le système collect-parse-store-analyze plus large plutôt qu'un seul script, le guide d'architecture de pipeline de données couvre comment ces étapes s'assemblent à grande échelle.
Scraper de façon responsable
Gardez la collecte dans les limites. Ne scrapez que des données publiques qui ne se trouvent pas derrière une connexion, lisez et respectez les conditions d'utilisation du site et son robots.txt, et maintenez votre taux de requêtes raisonnable pour ne pas surcharger les serveurs de la cible. Quand les données touchent des individus identifiables, des règles de confidentialité comme le RGPD et le CCPA s'appliquent, donc évitez les données personnelles sauf si vous avez une base légale et un besoin clair. L'exemple ici utilise une URL de remplacement et des champs de produits illustratifs précisément pour rester hors de ces préoccupations ; appliquez le même jugement quand vous pointez le scraper vers un vrai site.
Points clés
- SQL bat les fichiers plats pour l'analyse. Une table structurée vous permet de filtrer, agréger et regrouper des milliers d'enregistrements avec une seule requête plutôt que du code d'analyse jetable.
- Nettoyez les données à l'entrée. Supprimez les symboles monétaires et les virgules pour que le prix atterrisse comme un nombre ; une colonne numérique trie et calcule des moyennes correctement là où le texte ne le fait pas.
-
Concevez le schéma délibérément. Choisissez de vrais types, ajoutez une clé primaire et une URL
UNIQUEpour bloquer les doublons, et indexez les colonnes sur lesquelles vous filtrez. -
Insérez de façon sûre et par lots. Les espaces réservés paramétrés
?etexecutemanyavec un commit par lot gardent les insertions à la fois sécurisées et rapides. -
Analysez avec des SELECT simples.
WHERE,ORDER BY, les fonctions d'agrégat, etGROUP BYtransforment les lignes stockées en tranches de prix, moyennes et insights par catégorie.
Foire aux questions
Pourquoi stocker les données scrapées en SQL plutôt que dans un fichier CSV ou JSON ?
Les fichiers conviennent pour une seule extraction, mais ils rendent les questions inter-lignes pénibles. Une base de données SQL impose une structure cohérente, vous permet de filtrer, trier et agréger en une requête, et passe à l'échelle à des millions de lignes avec des index. Elle gère aussi proprement les exécutions répétées : une contrainte UNIQUE arrête les doublons pour que vous puissiez compléter la même table dans le temps plutôt que de jongler avec des fichiers datés.
Quelle base de données devrais-je utiliser, SQLite, MySQL, ou PostgreSQL ?
SQLite est idéal pour les petits et moyens projets et le travail local car il est livré avec Python et n'a pas besoin de serveur, ce qui explique pourquoi ce guide l'utilise. MySQL convient aux applications web et aux grands jeux de données partagés, et PostgreSQL est puissant pour les requêtes complexes et l'analytique. Le flux Python ici ne change que la connexion et la syntaxe des espaces réservés quand vous passez de l'un à l'autre ; le schéma et les requêtes restent presque identiques.
Comment éviter d'insérer le même enregistrement deux fois ?
Mettez une contrainte UNIQUE sur une colonne qui identifie l'enregistrement, comme l'URL du produit, puis insérez avec INSERT OR IGNORE (ou ON CONFLICT sur PostgreSQL). La base de données ignore toute ligne dont la clé existe déjà, donc relancer le scraper n'ajoute que les nouveaux listings plutôt que de dupliquer les anciens.
Pourquoi convertir le prix en nombre avant de le stocker ?
Parce qu'un prix gardé comme texte tel que "$1,299.00" ne peut pas être comparé ou moyenné correctement : il se trie alphabétiquement et casse les fonctions d'agrégat. Supprimer le symbole et les virgules pour stocker un price numérique signifie que WHERE price < 500, AVG(price), et ORDER BY price se comportent tous comme prévu.
Ai-je besoin du token JavaScript pour cela ?
Seulement si votre cible rend son contenu côté client. Le token normal retourne du HTML statique, ce qui suffit pour les pages rendues côté serveur. Si le listing se remplit après l'exécution des scripts de la page, utilisez le token JS et ajoutez les options ajax_wait et page_wait pour que le contenu dynamique se charge avant que le HTML soit capturé.
Comment passer à l'échelle sur de nombreuses pages sans surcharger la base de données ?
Paginez le scrape et passez les enregistrements combinés à une insertion executemany groupée, en committant toutes les quelques milliers de lignes plutôt qu'après chacune. Pour les grands crawls, le Crawler async met les requêtes en file d'attente et poste les résultats vers un webhook pour que vous ne mainteniez pas de connexions ouvertes, et l'indexation des colonnes interrogées garde les lectures rapides à mesure que la table grandit.
Crawlez n'importe quel site à grande échelle, sans combattre l'infrastructure.
Crawlbase gère les proxies, les empreintes et les CAPTCHA afin que votre équipe livre des pipelines de données au lieu de maintenir la plomberie de crawl. 1 000 requêtes gratuites, sans carte requise.
