La plupart des équipes qui surveillent un marché, construisent un index de recherche ou alimentent un jeu de données commencent de la même façon : elles crawlent des données depuis un ensemble de pages web publiques et les transforment en enregistrements propres. La partie difficile n'est rarement une seule page. C'est le faire sur des centaines de pages sans que vos requêtes soient throttlées, bloquées ou renvoient silencieusement du HTML à moitié vide.

Ce guide vous montre comment construire un petit crawler web exécutable en Python. Il récupère une page de départ via la Crawling API, extrait les liens qu'elle contient, suit ceux qui restent dans la portée de votre cible, analyse les champs que vous voulez sur chaque page, supprime les doublons, et exporte du JSON et du CSV propres. Le tutoriel reste sur un site d'exemple neutre pour que vous puissiez l'exécuter tel quel, puis le pointer vers votre propre source publique.

Crawling et scraping en un paragraphe

Ces deux mots sont utilisés indifféremment, mais ils désignent des tâches différentes. Le crawling est la découverte : partir d'une ou plusieurs URLs, suivre des liens, et avancer vers l'extérieur pour trouver des pages qui valent la peine d'être visitées. Le scraping est l'extraction : prendre le HTML d'une page et en extraire les champs spécifiques qui vous intéressent, comme un titre, un prix ou une date. Un vrai pipeline fait les deux. Le crawler décide quelles pages visiter et le scraper décide quoi garder de chacune. Le script dans ce guide est un crawler avec un scraper attaché à chaque page qu'il visite.

Ce que vous allez construire

Un seul script Python qui prend une URL de départ, découvre des liens d'articles en suivant des liens dans la portée, récupère chaque page via la Crawling API, et extrait un enregistrement structuré par page. L'exemple courant utilise https://example.com comme substitut d'un index public ou d'un blog. Chaque enregistrement porte ces champs :

  • Titre le titre principal de la page.
  • URL le lien canonique depuis lequel l'enregistrement a été scraté.
  • Résumé le paragraphe principal ou la méta description.
  • Date la date de publication ou de mise à jour quand la page en expose une.
  • Liens le nombre de liens dans la portée découverts sur la page.

Pourquoi une requête ordinaire échoue souvent

La version naïve de cela est une boucle autour d'un simple client HTTP : récupérer une URL, l'analyser, mettre en file les liens, répéter. Ça fonctionne sur un site jouet et s'effondre sur un site réel pour deux raisons.

Premièrement, le rendu. De nombreuses pages modernes livrent une coquille HTML mince et chargent leur vrai contenu dans le navigateur via JavaScript et Ajax. Demandez cette coquille avec un client ordinaire et les liens et champs que vous voulez ne sont pas encore dans le corps, donc votre crawler ne découvre rien et n'analyse rien. Deuxièmement, le blocage. Les sites surveillent le trafic automatisé : les plages d'IP de datacenter, les en-têtes de navigateur manquants et les schémas de requêtes qui s'exécutent plus vite qu'un humain sont throttlés, bloqués par IP ou reçoivent un CAPTCHA avant d'atteindre le contenu.

Ainsi, un crawler qui tient à l'échelle a besoin de deux choses dans chaque requête : un navigateur qui rend la page, et une IP que le site lit comme un vrai visiteur. Vous pouvez assembler cela vous-même depuis un navigateur sans interface plus un pool de proxies résidentiels tournants, mais maintenir cette pile en bonne santé est l'essentiel du travail. La Crawling API regroupe les deux en un seul appel : vous lui envoyez une URL, elle rend la page derrière une IP de confiance, et vous renvoie le HTML fini à analyser.

Prérequis

Quelques éléments doivent être en place d'abord. Aucun ne prend longtemps.

Python de base. Vous devez être à l'aise pour écrire et exécuter un script et installer des packages avec pip. Si la partie analyse est nouvelle pour vous, le guide BeautifulSoup se complète bien avec ce tutoriel.

Python 3.8 ou version ultérieure. Confirmez avec python --version. Si vous ne l'avez pas, installez-le depuis python.org ou via une distribution comme Anaconda, et assurez-vous que Python est dans votre PATH.

Un compte Crawlbase et un token. Inscrivez-vous, ouvrez votre tableau de bord et copiez votre token depuis la page du compte. Crawlbase inclut 1 000 requêtes gratuites pour commencer, ce qui est largement suffisant pour parcourir ce guide. Il y a deux types de tokens : le token normal récupère le HTML statique, et le token JavaScript rend d'abord la page dans un vrai navigateur. Utilisez le token normal pour les pages statiques et le token JavaScript quand le contenu se charge côté client. Traitez le token comme un mot de passe et gardez-le hors du contrôle de version.

Configurer le projet

Créez un environnement virtuel pour que les dépendances restent isolées, puis installez les deux bibliothèques dont le crawler a besoin.

bash
python --version

python -m venv crawler_env
source crawler_env/bin/activate

pip install crawlbase beautifulsoup4

Sur Windows, activez l'environnement avec crawler_env\Scripts\activate à la place de la ligne source. Deux dépendances font le travail : crawlbase est le client officiel pour la Crawling API, et beautifulsoup4 analyse le HTML renvoyé pour que vous puissiez extraire des champs et des liens par sélecteur CSS. json et csv sont livrés avec la bibliothèque standard, donc rien de plus n'est nécessaire pour l'étape d'export.

Étape 1 : Récupérer une page via Crawlbase

Commencez par obtenir une page de manière fiable. Importez la classe CrawlingAPI, initialisez-la avec votre token, et demandez l'URL de départ. Vérifier le pc_status Crawlbase avant d'analyser rend les échecs bruyants plutôt que silencieux, et vous donne un endroit propre pour réessayer.

python
import time
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

def fetch_html(page_url, max_retries=2):
    for attempt in range(max_retries + 1):
        response = api.get(page_url)
        if response["headers"]["pc_status"] == "200":
            return response["body"].decode("utf-8")
        if attempt < max_retries:
            print(f"Retrying ({attempt + 1}/{max_retries})...")
            time.sleep(1)
    print(f"Failed: {page_url} ({response['headers']['pc_status']})")
    return None

if __name__ == "__main__":
    html = fetch_html("https://example.com")
    print(html[:500] if html else "No HTML returned")

L'assistant fetch_html est la colonne vertébrale de tout le crawler. Il envoie l'URL via Crawlbase, réessaie jusqu'à deux fois avec une courte pause en cas d'échec de récupération, et renvoie le HTML décodé en cas de succès ou None une fois qu'il abandonne. Exécutez-le avec python crawler.py et vous devriez voir du vrai balisage s'afficher, ce qui confirme que le chemin de requête fonctionne avant d'écrire un seul sélecteur. Si votre cible charge du contenu côté client, initialisez avec le token JavaScript et passez {"ajax_wait": "true", "page_wait": 5000} comme deuxième argument à api.get pour que l'API attende le contenu dynamique avant de capturer la page.

Crawlbase Crawling API

L'assistant fetch_html ci-dessus repose sur une chose : chaque requête revient rendue et depuis une IP en laquelle le site a confiance. La Crawling API fait exactement cela. Elle exécute la page dans un vrai navigateur quand vous en avez besoin, fait tourner des IPs résidentielles côté serveur, et vous remet du HTML fini, pour que vous évitiez de monter une flotte de navigateurs sans interface et un pool de proxies vous-même. Pointez-la vers une page publique sur le forfait gratuit d'abord.

Étape 2 : Extraire les liens sur une page

La découverte n'est que l'extraction de liens effectuée en boucle. Chargez le HTML dans BeautifulSoup, extrayez chaque href d'ancre, et résolvez les chemins relatifs par rapport à la page sur laquelle ils ont été trouvés pour que vous travailliez toujours avec des URLs absolues.

python
from urllib.parse import urljoin, urldefrag
from bs4 import BeautifulSoup

def extract_links(html, base_url):
    soup = BeautifulSoup(html, "html.parser")
    links = set()
    for a in soup.select("a[href]"):
        href = a["href"].strip()
        if not href or href.startswith(("mailto:", "tel:", "javascript:")):
            continue
        absolute = urljoin(base_url, href)
        absolute, _ = urldefrag(absolute)
        links.add(absolute)
    return links

Trois petites décisions rendent cela robuste. La fonction ignore les ancres mailto:, tel: et javascript: qui ne sont pas de vraies pages. Elle utilise urljoin pour qu'un href relatif comme /articles/web-data devienne une URL complète par rapport à la page dont il provient. Et elle appelle urldefrag pour supprimer le fragment #section, car /page et /page#top sont le même document et vous ne voulez pas les visiter tous les deux. Renvoyer un set déduplique les liens trouvés sur cette seule page avant qu'ils n'atteignent jamais la file d'attente.

Étape 3 : Garder le crawl dans la portée

Sans contrainte, un crawler suit des liens hors de votre site cible et ne s'arrête jamais. La solution est une règle de portée : ne suivre que les liens qui partagent l'hôte de l'URL de départ et, optionnellement, se trouvent sous un préfixe de chemin qui vous intéresse. C'est l'équivalent crawler de rester dans la section produit au lieu de s'égarer dans le centre d'aide.

python
from urllib.parse import urlparse

def in_scope(url, root):
    root_parts = urlparse(root)
    url_parts = urlparse(url)
    if url_parts.scheme not in ("http", "https"):
        return False
    if url_parts.netloc != root_parts.netloc:
        return False
    return url_parts.path.startswith(root_parts.path)

in_scope compare chaque URL candidate par rapport à la racine depuis laquelle vous avez commencé. Il rejette tout ce qui n'est pas HTTP ou HTTPS, tout ce qui se trouve sur un hôte différent (netloc), et tout ce dont le chemin ne commence pas par le chemin racine. Définissez la racine sur https://example.com/ pour crawler tout l'hôte, ou sur https://example.com/blog/ pour rester dans une section. Rétrécir la portée ici est le levier unique le plus important sur la quantité que vous récupérez.

Étape 4 : Analyser les champs sur chaque page

La découverte vous dit quelles pages visiter ; l'analyse décide quoi garder. Extrayez un enregistrement petit et bien défini de chaque page et protégez chaque lookup pour qu'un champ manquant renvoie None au lieu de faire planter l'exécution.

python
def text_of(soup, selector):
    el = soup.select_one(selector)
    return el.get_text(strip=True) if el else None

def attr_of(soup, selector, attr):
    el = soup.select_one(selector)
    return el.get(attr) if el else None

def parse_page(html, url):
    soup = BeautifulSoup(html, "html.parser")
    summary = (
        attr_of(soup, 'meta[name="description"]', "content")
        or text_of(soup, "article p")
    )
    return {
        "url": url,
        "title": text_of(soup, "h1") or text_of(soup, "title"),
        "summary": summary,
        "date": attr_of(soup, "time[datetime]", "datetime"),
    }

Les deux assistants, text_of et attr_of, interrogent un seul élément et renvoient son texte ou un attribut, retombant sur None quand l'élément est absent. parse_page utilise une chaîne de replis : il préfère la balise meta[name="description"] pour le résumé et tombe sur le premier paragraphe article s'il n'y en a pas, et prend le h1 pour le titre mais utilise la balise <title> quand il n'y a pas de h1. Ces sélecteurs sont délibérément génériques pour que le script fonctionne sur le site d'exemple. Pour une vraie cible, ouvrez la page dans les outils de développement de votre navigateur et remplacez-les par des sélecteurs qui correspondent à son vrai balisage.

Étape 5 : Assembler la boucle de crawl

Maintenant, reliez les pièces en un seul crawler en largeur d'abord. Une file d'attente contient les URLs à visiter, un ensemble visited empêche de récupérer deux fois la même page, et un plafond max_pages arrête l'exécution pour qu'elle ne dure pas indéfiniment. Pour chaque page qu'il récupère, le crawler analyse un enregistrement, compte les liens dans la portée, et met en file les nouveaux.

python
import csv
import json
import time
from collections import deque
from urllib.parse import urljoin, urldefrag, urlparse
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

def fetch_html(page_url, max_retries=2):
    for attempt in range(max_retries + 1):
        response = api.get(page_url)
        if response["headers"]["pc_status"] == "200":
            return response["body"].decode("utf-8")
        if attempt < max_retries:
            time.sleep(1)
    return None

def extract_links(html, base_url):
    soup = BeautifulSoup(html, "html.parser")
    links = set()
    for a in soup.select("a[href]"):
        href = a["href"].strip()
        if not href or href.startswith(("mailto:", "tel:", "javascript:")):
            continue
        absolute, _ = urldefrag(urljoin(base_url, href))
        links.add(absolute)
    return links

def in_scope(url, root):
    r, u = urlparse(root), urlparse(url)
    return (
        u.scheme in ("http", "https")
        and u.netloc == r.netloc
        and u.path.startswith(r.path)
    )

def text_of(soup, selector):
    el = soup.select_one(selector)
    return el.get_text(strip=True) if el else None

def attr_of(soup, selector, attr):
    el = soup.select_one(selector)
    return el.get(attr) if el else None

def parse_page(html, url, link_count):
    soup = BeautifulSoup(html, "html.parser")
    summary = (
        attr_of(soup, 'meta[name="description"]', "content")
        or text_of(soup, "article p")
    )
    return {
        "url": url,
        "title": text_of(soup, "h1") or text_of(soup, "title"),
        "summary": summary,
        "date": attr_of(soup, "time[datetime]", "datetime"),
        "links": link_count,
    }

def crawl(start_url, max_pages=25):
    queue = deque([start_url])
    visited = set()
    records = []
    while queue and len(visited) < max_pages:
        url = queue.popleft()
        if url in visited:
            continue
        visited.add(url)
        html = fetch_html(url)
        if not html:
            continue
        found = {l for l in extract_links(html, url) if in_scope(l, start_url)}
        records.append(parse_page(html, url, len(found)))
        for link in found:
            if link not in visited:
                queue.append(link)
        print(f"[{len(visited)}/{max_pages}] {url}")
        time.sleep(2)
    return records

C'est un crawl en largeur d'abord classique. L'ensemble visited est le garde de déduplication au niveau du crawl : une URL est ajoutée avant d'être récupérée, donc même si trois pages pointent toutes vers le même article, il n'est demandé qu'une seule fois. max_pages plafonne le travail total, le filtre de portée empêche la file d'attente de se remplir de liens hors site, et le sommeil de deux secondes rythme l'exécution pour ne pas marteler le serveur. La ligne print vous donne une trace de progression en direct pendant qu'il fonctionne.

Étape 6 : Dédupliquer et exporter en JSON et CSV

L'ensemble visited empêche déjà de récupérer une URL deux fois, mais les redirections et les variantes avec barre oblique finale peuvent encore produire deux enregistrements décrivant la même page. Un passage final clé sur l'URL effondre ceux-ci avant l'export.

python
def dedupe(records):
    seen = {}
    for record in records:
        seen[record["url"].rstrip("/")] = record
    return list(seen.values())

def save_outputs(records):
    with open("crawl_results.json", "w") as f:
        json.dump(records, f, indent=2)
    if not records:
        return
    with open("crawl_results.csv", "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=records[0].keys())
        writer.writeheader()
        writer.writerows(records)

def main():
    records = crawl("https://example.com", max_pages=25)
    records = dedupe(records)
    save_outputs(records)
    print(f"Saved {len(records)} pages")

if __name__ == "__main__":
    main()

dedupe clé chaque enregistrement sur son URL avec la barre oblique finale supprimée, donc /article et /article/ se résolvent en une seule entrée, et le dernier enregistrement gagne. save_outputs écrit un fichier JSON et un CSV en utilisant les clés du premier enregistrement comme en-tête, vous donnant les données sous la forme que votre prochain outil préfère. Ajoutez ces deux fonctions en dessous de la boucle de crawl de l'étape 5 et le script s'exécute de bout en bout.

À quoi ressemble la sortie

Exécutez le script complet avec python crawler.py et vous obtenez un enregistrement structuré par page, prêt pour l'analyse, une base de données ou une feuille de calcul.

json
[
  {
    "url": "https://example.com/articles/web-data",
    "title": "A Practical Guide to Web Data",
    "summary": "How teams turn public pages into clean, structured records.",
    "date": "2024-09-18",
    "links": 12
  },
  {
    "url": "https://example.com/articles/crawling-basics",
    "title": "Crawling Basics",
    "summary": "Discovery, scope, and dedupe explained from first principles.",
    "date": "2024-08-02",
    "links": 9
  }
]

Le CSV correspondant porte les mêmes colonnes, une ligne par page, qui tombe directement dans pandas ou n'importe quelle feuille de calcul pour le tri, le filtrage ou la jointure avec un autre jeu de données. Si vous voulez aller plus loin dans l'étape de stockage, stocker des données scrapées sur le cloud et les charger dans SQL sont des étapes naturelles suivantes.

Mise à l'échelle du crawl

Le script ci-dessus est délibérément mono-thread pour être facile à lire et facile à maintenir poli. Quelques changements le font passer d'une démo à un travail que vous pouvez laisser tourner.

  • Augmentez le plafond avec précaution. max_pages est votre soupape de sécurité. Augmentez-le par étapes et regardez combien de liens dans la portée le crawl découvre avant de vous engager dans une grande exécution.
  • Persistez la frontière. Pour les longs crawls, écrivez la file d'attente et l'ensemble visited sur disque pour qu'une exécution interrompue reprenne au lieu de recommencer et de tout re-récupérer.
  • Passez à l'asynchrone pour le volume. Quand vous avez besoin de milliers de pages, le Crawler asynchrone met en file des requêtes et pousse les résultats vers un webhook, pour que vous ne gardiez pas de connexions ouvertes pendant que les pages se rendent.

Pour les cibles lourdes en JavaScript où les liens eux-mêmes se chargent côté client, la même boucle fonctionne une fois que vous passez au token JavaScript et aux options d'attente ; voir le crawling de sites JavaScript pour les détails.

Rester non bloqué

Même avec le rendu et les IPs de confiance gérés, quelques habitudes maintiennent un crawl plus long en bonne santé.

  • Rythmez vos requêtes. Le sommeil de deux secondes dans la boucle est un plancher, pas un plafond. Élargissez-le pour les travaux plus importants, et évitez de crawler un chemin aussi vite que le serveur répond.
  • Appuyez-vous sur la rotation. Un pool d'IPs résidentielles répartit les requêtes sur de nombreuses adresses d'utilisateurs réels afin qu'aucune ne déclenche une limite de débit. La Crawling API gère cela pour vous ; si vous construisez votre propre pile, c'est la partie à bien faire.
  • Lisez les codes de statut. Une exécution qui commence à renvoyer des valeurs pc_status non 200 vous indique que le débit ou le niveau d'IP actuel n'est plus suffisant. Traitez cela comme un signal de ralentissement, pas du bruit à ignorer.

Pour le guide plus complet, voir comment scraper des sites sans être bloqué.

Scraper de manière responsable

Ne crawlez que des données publiques, et respectez les règles des sites que vous visitez. Lisez les conditions d'utilisation de chaque cible et son robots.txt avant de commencer, maintenez votre débit de requêtes raisonnable pour ne pas surcharger les serveurs de qui que ce soit, et restez à l'écart de tout ce qui se trouve derrière un identifiant ou un paywall. Quand les pages que vous collectez contiennent des données personnelles, les lois sur la vie privée comme le RGPD et le CCPA s'appliquent à la façon dont vous les stockez et les utilisez, alors limitez vos champs à ce dont vous avez vraiment besoin et évitez de collecter des détails liés à des individus identifiables. Le code de ce guide fait fonctionner la partie technique ; maintenir le projet du bon côté de ces lignes vous incombe.

Récapitulatif

Points clés

  • Le crawling et le scraping sont deux tâches. Le crawler découvre quelles pages visiter en suivant des liens ; le scraper extrait les champs que vous gardez de chacune.
  • Rendez et routez via une IP de confiance. Un client ordinaire manque le contenu rendu côté client et est bloqué ; la Crawling API renvoie du HTML fini depuis une IP de confiance en un seul appel.
  • La portée et la déduplication maintiennent le crawl sain. Une vérification in_scope arrête l'exécution de s'égarer hors site, et un ensemble visited plus un passage clé sur l'URL suppriment le travail et les enregistrements en double.
  • Analysez défensivement. Protégez chaque sélecteur pour qu'un champ manquant renvoie None et qu'une page bizarre ne termine pas l'exécution.
  • Exportez une fois, utilisez partout. Écrire à la fois JSON et CSV permet au même jeu de données de s'écouler dans pandas, une base de données ou une feuille de calcul sans rework.

Foire aux questions

Quelle est la différence entre le crawling web et le scraping web ?

Le crawling est l'étape de découverte : partir d'une ou plusieurs URLs et suivre des liens pour trouver des pages qui valent la peine d'être visitées. Le scraping est l'étape d'extraction : prendre le HTML d'une seule page et en extraire des champs spécifiques comme un titre ou une date. La plupart des vrais pipelines font les deux en même temps, ce qui est exactement ce que fait le script de ce guide : crawler pour trouver des pages et scraper un enregistrement de chacune.

Pourquoi mon crawler renvoie-t-il du HTML vide ou partiel ?

Généralement parce que la page rend son contenu dans le navigateur avec JavaScript, donc le HTML initial est une coquille mince et vos liens et champs ne s'y trouvent pas encore. Récupérez la page via la Crawling API avec le token JavaScript et les options ajax_wait et page_wait, qui rendent d'abord la page et renvoient le balisage fini à analyser.

Comment empêcher le crawler de quitter le site que je cible ?

Utilisez une règle de portée. La fonction in_scope compare chaque lien candidat par rapport à l'hôte et au chemin de votre URL de départ et rejette tout ce qui ne correspond pas. Définissez étroitement le chemin racine, par exemple https://example.com/blog/, pour maintenir le crawl dans une section plutôt que tout le domaine.

Comment le crawler évite-t-il de visiter deux fois la même page ?

Deux couches. Un ensemble visited enregistre chaque URL avant qu'elle ne soit récupérée, donc une page liée depuis de nombreux endroits n'est encore demandée qu'une seule fois. Après le crawl, un passage de déduplication clé sur l'URL (avec la barre oblique finale normalisée) efface tous les enregistrements qui décrivent encore la même page avant qu'ils n'atteignent JSON et CSV.

Dois-je exporter en JSON ou CSV ?

Les deux, et laissez l'outil en aval décider. JSON conserve la forme imbriquée et typée que le code et les API préfèrent, tandis que CSV tombe directement dans les feuilles de calcul et pandas. La fonction save_outputs écrit les deux depuis les mêmes enregistrements, donc vous n'êtes pas enfermé dans un format. Pour plus sur les compromis, voir la différence entre JSON et CSV.

Combien de pages puis-je crawler sur le forfait gratuit ?

Crawlbase inclut 1 000 requêtes gratuites pour commencer, et vous ne payez que pour les requêtes réussies. Chaque page que le crawler récupère est une requête, donc le plafond max_pages dans le script correspond directement à votre utilisation. Pour les travaux plus importants ou récurrents, le Crawler asynchrone met à l'échelle la même approche sans garder de connexions ouvertes.

Commencer à construire

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.

En libre-service · Sans appel commercial requis · Volumes de crawl entreprise disponibles