Beaucoup de pages méritent d'être surveillées pour détecter des changements : la page de tarification d'un concurrent, le statut de stock d'un produit, un document de politique ou de conditions d'utilisation, une offre d'emploi, une page de notes de version. L'information est publique, mais le changement est le signal qui vous intéresse réellement, et actualiser un onglet manuellement ne passe pas à l'échelle au-delà d'une ou deux pages. Ce qu'il vous faut, c'est un script qui vérifie pour vous et vous avertit uniquement quand quelque chose est différent.

Ce guide vous montre comment construire un tracker de changements de sites web en Python de façon fiable. Vous construisez un petit outil exécutable qui récupère une page via la Crawling API, extrait le texte significatif, calcule une empreinte SHA-256, stocke chaque instantané sur le disque, compare la nouvelle empreinte à la dernière pour détecter un changement, et exécute l'ensemble de la vérification selon un calendrier. L'approche est générique : elle fonctionne sur n'importe quelle page publique que vous lui indiquez, pas sur un site spécifique.

Ce que vous allez construire

Un script Python qui prend une ou plusieurs URLs publiques, récupère chaque page via la Crawling API, la réduit à du texte comparable, prend une empreinte de ce texte et signale si la page a changé depuis la dernière exécution. Chaque composant est une petite fonction pour que vous puissiez la lire et la réutiliser. Les pièces sont :

  • Fetcher récupère le HTML de la page via la Crawling API afin que les blocages et le rendu JavaScript soient gérés pour vous.
  • Extractor supprime les scripts, les styles, la navigation et les pieds de page, laissant le corps de texte lisible.
  • Fingerprint un hash SHA-256 du texte nettoyé, afin qu'un seul mot modifié produise une valeur complètement différente.
  • Store un fichier JSON associant chaque URL à sa dernière empreinte, plus le dernier texte pour le diffing.
  • Comparator charge l'empreinte précédente, compare, et signale changed ou unchanged.
  • Scheduler une boucle avec un intervalle de sleep, ou une entrée cron, pour que la vérification s'exécute seule.

Pourquoi une requête classique ne suffit pas

Vous pourriez écrire cela avec un client HTTP nu et ignorer entièrement une API, et sur une page statique simple cela fonctionnerait même. Les ennuis commencent sur les cibles réelles. Beaucoup de sites limitent ou bloquent carrément les requêtes automatisées : une adresse IP de datacenter frappant la même URL à intervalle fixe est un schéma facile à signaler, et un moniteur est du trafic automatisé par définition. D'autres pages construisent leur contenu dans le navigateur avec JavaScript, donc le HTML brut qu'une requête ordinaire renvoie est une coquille presque vide, et votre empreinte finit par suivre la coquille plutôt que le contenu que vous vouliez surveiller.

Un tracker fiable a donc besoin de deux choses de chaque récupération : une adresse IP que le site lit comme un vrai visiteur, et, quand la page est rendue côté client, un navigateur qui exécute les scripts avant que le HTML ne revienne. Vous pouvez assembler cela vous-même avec un navigateur sans tête et un pool de proxies résidentiels rotatifs, mais maintenir cette stack en bonne santé représente la majeure partie du travail. La Crawling API regroupe les deux en un seul appel : envoyez-lui l'URL, optionnellement un token JavaScript, et elle renvoie du HTML fini à fingerprinter.

Il y a une deuxième raison pour laquelle la comparaison elle-même doit être prudente, indépendamment de la façon dont vous récupérez. Le HTML brut change constamment de façons qui ne vous intéressent pas : scripts inline, emplacements publicitaires, horodatages intégrés, tokens CSRF, widgets dynamiques. Si vous hashèz la réponse brute, vous obtenez un faux positif à presque chaque exécution. Réduire d'abord la page à son texte lisible est ce qui fait de l'empreinte un vrai signal plutôt que du bruit.

Prérequis

Quelques éléments doivent être en place au préalable. Aucun ne prend longtemps.

Bases de Python. Vous devez être à l'aise pour écrire et exécuter un script et installer des packages avec pip. Si BeautifulSoup est nouveau pour vous, notre guide sur l'utilisation de BeautifulSoup en Python couvre le parsing que ce tutoriel suppose acquis.

Python 3.10 ou version ultérieure. Confirmez votre version avec python --version. Le code utilise la syntaxe d'indication de type str | None, qui nécessite 3.10. Si vous ne l'avez pas, installez-le depuis python.org.

Un compte Crawlbase et un token. Inscrivez-vous, ouvrez votre tableau de bord et copiez votre token depuis la page de documentation du compte. L'offre gratuite inclut 1 000 requêtes, ce qui est largement suffisant pour tester un tracker. Traitez le token comme un mot de passe et gardez-le hors du contrôle de version : le code ci-dessous le lit depuis la variable d'environnement CRAWLBASE_TOKEN.

Mettre en place le projet

Créez un environnement virtuel pour que les dépendances restent isolées, puis installez les deux bibliothèques tierces. Le hashing, le stockage, la planification et le diffing viennent tous de la bibliothèque standard Python (hashlib, json, time et difflib), il n'y a donc rien de plus à installer pour eux.

bash
python --version

python -m venv tracker_env
source tracker_env/bin/activate

pip install requests beautifulsoup4

Sur Windows, activez l'environnement avec tracker_env\Scripts\activate au lieu de la ligne source. Deux dépendances font le travail : requests envoie l'appel HTTP à la Crawling API, et beautifulsoup4 parse le HTML renvoyé pour que vous puissiez en extraire le texte lisible.

Étape 1 : récupérer la page via la Crawling API

Commencez par confirmer que vous pouvez récupérer la page. La fonction ci-dessous lit votre token, construit l'URL de requête Crawling API avec l'URL de la page cible encodée dedans, envoie la requête et renvoie le HTML. Vérifier le statut de la réponse maintient les échecs bruyants plutôt que silencieux.

python
import os
from urllib.parse import quote
import requests

CRAWLBASE_API_URL = "https://api.crawlbase.com"

def fetch_page(url: str, token: str | None = None) -> str:
    api_token = token or os.environ.get("CRAWLBASE_TOKEN", "")
    if not api_token:
        raise ValueError("Set CRAWLBASE_TOKEN or pass token=")
    api_url = f"{CRAWLBASE_API_URL}/?token={api_token}&url={quote(url)}"
    response = requests.get(api_url, timeout=30)
    response.raise_for_status()
    return response.text

if __name__ == "__main__":
    html = fetch_page("https://example.com")
    print(html[:300])

Exécutez cela avec votre token défini (export CRAWLBASE_TOKEN="your_token") et vous devriez voir les premiers centaines de caractères du vrai HTML de la page. Cette confirmation initiale compte : elle prouve que la requête atteint la page et revient avec du contenu avant que vous ne construisiez quoi que ce soit dessus. Les appels timeout=30 et raise_for_status() sont délibérés, et la section sur la gestion des erreurs plus loin s'appuie sur les deux. Si la cible rend son contenu avec JavaScript, ajoutez un token JavaScript à la place du standard pour que la page soit rendue avant que le HTML ne soit renvoyé.

Crawlbase Crawling API

Ce premier appel fetch_page a renvoyé du vrai HTML sans que vous gériez un seul proxy. La Crawling API gère le blocage, le throttling et les défis CAPTCHA qui font couler une boucle de requêtes nue, et fait tourner des adresses IP de confiance côté serveur, afin qu'un moniteur de longue durée continue d'obtenir du HTML propre à fingerprinter plutôt que d'être signalé. Ajoutez un token JavaScript et elle rend les pages côté client avant de les renvoyer. Pointez-la vers une page publique sur l'offre gratuite en premier.

Étape 2 : extraire et fingerprinter le contenu

Comparer le HTML brut n'est pas fiable, donc l'étape suivante réduit la page à du texte lisible puis le hashe. L'extracteur charge le HTML dans BeautifulSoup, supprime les éléments qui changent sans rien signifier (script, style, nav, footer), extrait le texte visible et réduit les espaces pour que les reformatages cosmétiques ne s'enregistrent pas comme des changements.

python
import hashlib
from bs4 import BeautifulSoup

def extract_monitorable_text(html: str) -> str:
    soup = BeautifulSoup(html, "html.parser")
    for tag in soup(["script", "style", "nav", "footer"]):
        tag.decompose()
    text = soup.get_text(separator=" ", strip=True)
    return " ".join(text.split())

def content_fingerprint(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).hexdigest()

L'empreinte est un hash SHA-256 de ce texte nettoyé. Un hash est une chaîne de longueur fixe dérivée de l'entrée, et tout changement dans l'entrée, même un seul caractère, produit une sortie complètement différente. C'est exactement la propriété que veut un tracker : au lieu de stocker et de comparer octet par octet des pages entières, vous stockez une chaîne de 64 caractères par URL et les comparez. La comparaison est rapide, le stockage est minuscule, et les petites modifications sont quand même détectées. Associez cela à notre guide général de scraping Python si vous souhaitez étendre l'extracteur pour cibler une région spécifique de la page plutôt que le corps entier.

Étape 3 : stocker les instantanés et comparer

Pour détecter un changement, l'outil doit se souvenir de la dernière exécution. Deux petits fichiers JSON maintiennent l'état : snapshots.json associe chaque URL à sa dernière empreinte, et snapshots_text.json conserve le dernier texte extrait pour que vous puissiez afficher un diff lisible par un humain quand quelque chose bouge. La fonction de chargement renvoie un dict vide à la première exécution plutôt que d'échouer.

python
import json
from pathlib import Path

def load_json(path: str | Path) -> dict[str, str]:
    p = Path(path)
    if not p.exists():
        return {}
    with open(p, encoding="utf-8") as f:
        return json.load(f)

def save_json(data: dict[str, str], path: str | Path) -> None:
    with open(path, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2)

def check_for_change(url: str, current_hash: str, snapshots: dict[str, str]) -> bool:
    previous = snapshots.get(url)
    if previous is None:
        return True
    return previous != current_hash

La logique de comparaison est le coeur du tracker, et elle est délibérément simple. check_for_change recherche l'empreinte stockée de l'URL. S'il n'y en a pas, c'est la première fois que vous voyez la page, donc il signale un changement et la nouvelle empreinte est sauvegardée. S'il y en a une, il renvoie si les deux diffèrent. La première exécution sur n'importe quelle URL signale toujours changed pour exactement cette raison, ce qui est attendu, pas un bug.

Connectez maintenant les pièces ensemble en une seule exécution. La fonction ci-dessous boucle sur les URLs, récupère et fingerprinte chacune, décide si elle a changé, émet un diff unifié contre le texte stocké quand c'est le cas, et sauvegarde l'état mis à jour à la fin pour que la prochaine exécution ait quelque chose à comparer. Le diff utilise le module difflib de la bibliothèque standard, sans dépendance supplémentaire.

python
import difflib

def run_once(urls: list[str], hash_path="snapshots.json",
             text_path="snapshots_text.json") -> None:
    snapshots = load_json(hash_path)
    snapshot_texts = load_json(text_path)

    for url in urls:
        html = fetch_page(url)
        text = extract_monitorable_text(html)
        if not text:
            print(f"[warn] empty text, skipping {url}")
            continue
        fingerprint = content_fingerprint(text)

        if check_for_change(url, fingerprint, snapshots):
            print(f"[changed] {url}")
            old = snapshot_texts.get(url, "")
            diff = difflib.unified_diff(
                old.split(), text.split(),
                lineterm="", n=0)
            print(" ".join(diff)[:500])
        else:
            print(f"[no change] {url}")

        snapshots[url] = fingerprint
        snapshot_texts[url] = text

    save_json(snapshots, hash_path)
    save_json(snapshot_texts, text_path)

Sauvegarder à la fois l'empreinte et le texte est ce qui permet aux futures exécutions de détecter un changement et de l'expliquer. L'empreinte répond à « est-ce que quelque chose a changé », et le texte stocké permet à difflib de répondre à « qu'est-ce qui a changé ». Si vous n'avez jamais besoin que du signal oui/non, vous pouvez supprimer le fichier texte et garder uniquement la carte d'empreintes.

Storage beyond JSON

Les fichiers JSON sont parfaits pour une poignée d'URLs et faciles à inspecter manuellement. Une fois que vous surveillez des centaines de pages, remplacez les fonctions de chargement et de sauvegarde par SQLite via le module standard sqlite3 : il gère les lectures concurrentes, passe à l'échelle pour de grandes listes d'URLs et conserve tout l'état dans un seul fichier portable. Le reste du script ne change pas.

Étape 4 : exécuter le tracker selon un calendrier

Un tracker de changements n'est utile que s'il tourne seul. Il y a deux façons propres de faire cela. La première est intégrée au script : une boucle à intervalles optionnelle qui revérifie chaque URL toutes les N secondes jusqu'à ce que vous l'arrêtiez. La seconde est de laisser le système d'exploitation faire tourner une seule passe selon un minuteur avec cron. Ci-dessous se trouve le point d'entrée CLI avec les deux modes one-shot et à intervalle.

python
import argparse
import time

def main() -> None:
    parser = argparse.ArgumentParser(description="Website change tracker")
    parser.add_argument("urls", nargs="+", help="public URLs to monitor")
    parser.add_argument("--interval", type=float, metavar="SECONDS",
        help="re-check every SECONDS (e.g. 3600 for hourly); Ctrl+C to stop")
    args = parser.parse_args()

    while True:
        run_once(args.urls)
        if args.interval is None:
            break
        time.sleep(args.interval)

if __name__ == "__main__":
    main()

Lancez une vérification unique, ou maintenez-la en boucle toutes les heures :

bash
export CRAWLBASE_TOKEN="your_token"

# one pass, then exit
python tracker.py https://example.com

# check every hour until you stop it
python tracker.py https://example.com --interval 3600

La boucle à intervalles est l'option la plus simple et garde le processus en un seul endroit, ce qui est pratique pendant les tests. Pour une configuration en production sans surveillance, cron est généralement plus adapté : il survit aux redémarrages et ne monopolise pas un terminal. Une entrée crontab qui fait tourner une seule passe chaque heure et ajoute la sortie à un journal ressemble à ceci :

bash
# run at the top of every hour
0 * * * * cd /path/to/project && \
  CRAWLBASE_TOKEN=your_token \
  ./tracker_env/bin/python tracker.py https://example.com >> tracker.log 2>&1

Sur Windows, l'équivalent est le Planificateur de tâches exécutant la même commande one-pass sur un déclencheur. Dans tous les cas, supprimez l'indicateur --interval quand cron ou le Planificateur de tâches gère la temporisation, puisque le planificateur gère déjà la répétition.

À quoi ressemble la sortie

Le script affiche une ligne par URL et par exécution, plus un diff tronqué quand une page a changé. La première fois que vous vérifiez une URL, elle signale toujours changed, car il n'y a pas encore d'empreinte stockée, et l'instantané est écrit pour la prochaine fois :

bash
# first run: no snapshot exists yet
[changed] https://example.com

# later run, content edited
[changed] https://example.com
--- +++ @@ -Old pricing copy +New pricing copy

# later run, nothing moved
[no change] https://example.com

L'état sur disque est tout aussi lisible. snapshots.json est une carte plate d'URL vers empreinte, ce qui est tout ce dont la comparaison a besoin :

json
{
  "https://example.com": "3e1f9c...a7d2",
  "https://example.com/pricing": "b04c88...11ef"
}

Gérer les échecs et monter en charge

Un moniteur de longue durée rencontrera des échecs, et la façon dont il les gère détermine s'il continue de tourner. Trois cas surviennent constamment. Timeouts : l'appel requests.get(timeout=30) lève une exception si l'API ne répond pas à temps, donc encapsulez la récupération et réessayez avec backoff exponentiel plutôt que de laisser une réponse lente tuer l'exécution. Erreurs HTTP : raise_for_status() transforme les réponses 4xx et 5xx en exceptions ; journalisez le statut et l'URL, puis ignorez cette URL et continuez avec les autres. Extractions vides : si extract_monitorable_text renvoie une chaîne vide, ignorez la comparaison et journalisez un avertissement au lieu d'enregistrer un faux changement, ce que le garde if not text dans run_once fait déjà.

python
def fetch_with_retry(url: str, retries: int = 3,
                     backoff: float = 2.0) -> str:
    for attempt in range(retries):
        try:
            return fetch_page(url)
        except requests.exceptions.RequestException:
            if attempt < retries - 1:
                time.sleep(backoff ** attempt)
            else:
                raise

Passer d'une page à plusieurs suit naturellement. Suivre plus d'URLs est simplement une liste plus longue passée à run_once. Pour accélérer les grandes listes, récupérez en parallèle avec concurrent.futures.ThreadPoolExecutor puisque le travail est lié à l'I/O. Pour suivre des centaines de pages, passez l'état de JSON à SQLite comme noté ci-dessus. Et si l'une de vos cibles rend le contenu côté client, passez la récupération à un token JavaScript pour que la page soit rendue avant que vous en extrayiez : nos notes sur le scraping de pages JavaScript avec Python couvrent quand c'est nécessaire.

Suivre les changements de façon responsable

Un tracker de changements est du trafic automatisé, donc faites-le tourner comme vous voudriez qu'on le fasse tourner contre votre propre site. Interrogez à une cadence qui correspond à la fréquence de changement réelle de la page : toutes les 15 à 60 minutes pour les actualités rapides ou les tableaux de bord, toutes les quelques heures pour les prix et les annonces, et quotidiennement ou hebdomadairement pour les pages de politique et de documentation. Vérifier une page statique chaque minute ajoute du coût de requête et de la charge sans améliorer la détection, donc choisissez l'intervalle le plus lent qui capture encore ce dont vous avez besoin.

Restez également du bon côté de la source. Ne suivez que les pages publiques, celles que n'importe qui peut charger sans compte, et lisez les conditions d'utilisation et le robots.txt du site avant de pointer un job récurrent dessus ; traitez les deux comme la frontière de ce que vous collectez. Maintenez votre volume de requêtes suffisamment bas pour ne pas surcharger le serveur, espacez les vérifications entre les cibles au lieu de bombarder une seule URL, et reculez quand vous voyez des erreurs ou des défis plutôt que de réessayer plus fort. Si un site propose une API officielle ou un flux de changements, préférez-la : c'est le chemin que le site prévoit pour cela, et il est généralement plus stable que le parsing HTML.

Récapitulatif

Points clés

  • Les empreintes surpassent la comparaison brute. Hasher le texte nettoyé avec SHA-256 transforme « est-ce que cette page a changé » en une comparaison rapide de deux chaînes de 64 caractères au lieu de pages entières.
  • Extraire avant de hasher. Supprimer scripts, styles, nav et pieds de page et réduire les espaces est ce qui empêche les horodatages et les emplacements publicitaires de déclencher des faux positifs.
  • Stocker le texte, pas seulement le hash. Garder le dernier texte extrait avec l'empreinte permet à difflib de montrer exactement ce qui a changé, pas seulement que quelque chose a changé.
  • La couche de récupération est là où les blocages se produisent. La Crawling API gère le rendu et la rotation des adresses IP de confiance, afin qu'un moniteur de longue durée continue d'obtenir du HTML propre au lieu d'être signalé.
  • Planifier et être poli. Une boucle sleep ou une entrée cron fait tourner la vérification seule ; adaptez l'intervalle à la fréquence de changement réelle de la page et respectez les conditions et le robots.txt de la source.

Foire aux questions

Pourquoi fingerprinter le texte plutôt que comparer le HTML brut ?

Le HTML brut change à presque chaque requête de façons qui ne vous intéressent pas : scripts inline, emplacements publicitaires, horodatages intégrés et tokens CSRF changent tous tandis que le contenu réel reste le même. Comparer le HTML brut vous donne un faux positif à presque chaque exécution. Réduire d'abord la page au texte lisible, puis hasher celui-ci, fait en sorte que l'empreinte suive le contenu que vous vouliez surveiller plutôt que le bruit autour.

Cela fonctionne-t-il sur les sites web lourds en JavaScript ?

Oui, avec un changement. Utilisez un token JavaScript avec la Crawling API au lieu du standard. Cela rend la page complète dans un vrai navigateur avant de renvoyer le HTML, afin que le contenu côté client soit présent quand BeautifulSoup extrait le texte. Sans cela, une page rendue côté client renvoie un cadre presque vide et votre empreinte finit par suivre le cadre plutôt que le contenu.

Peut-il surveiller plusieurs pages à la fois ?

Oui. Passez plusieurs URLs en ligne de commande et le script les traite à tour de rôle, gardant une empreinte par URL dans le fichier d'instantanés. Pour les grandes listes, récupérez en parallèle avec concurrent.futures.ThreadPoolExecutor puisque le travail est lié à l'I/O, et envisagez de passer le stockage à SQLite pour que l'état passe à l'échelle proprement.

Comment recevoir une alerte quand quelque chose change ?

Le script central signale les changements sur stdout, ce qui est suffisant si cron vous envoie sa sortie par email. Pour envoyer une vraie alerte, appelez depuis l'endroit où check_for_change renvoie True : postez vers un webhook Slack ou Discord, envoyez un email via une API transactionnelle, ou frappez n'importe quel endpoint HTTP. Ce sont quelques lignes ajoutées à la branche changed de run_once.

Quelle est la meilleure option de stockage pour suivre des centaines d'URLs ?

Remplacez les fichiers JSON par SQLite via le module standard sqlite3. Il gère les lectures concurrentes, passe à l'échelle pour de grandes listes d'URLs et conserve tout l'état dans un seul fichier portable. Les fonctions de chargement et de sauvegarde sont le seul code qui change ; la logique de récupération, d'extraction, de fingerprinting et de comparaison reste exactement la même.

À quelle fréquence le tracker devrait-il tourner ?

Adaptez l'intervalle à la fréquence de changement réelle de la page. Les pages d'actualités rapides et les tableaux de bord justifient toutes les 15 à 60 minutes ; les prix et les annonces de produits conviennent généralement à quelques heures ; les pages de politique et de documentation peuvent être vérifiées quotidiennement ou hebdomadairement. Tourner bien plus souvent que la page ne change ajoute uniquement du coût de requête et de la charge sur la source sans rien attraper que vous auriez autrement manqué.

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