Scraping

Déplacer les données de l'API de scraping web vers des bases de données SQL à grande échelle

La récupération n'est que la moitié facile. Comment intégrer les données d'une API de scraping dans SQL avec des chargements idempotents, des champs typés, des alertes de dérive de schéma et une provenance intacte.

Chris Collins

Chris Collins

14 septembre 2026 · 11 min de lecture

Une API de web scraping élimine la partie la plus difficile de la collecte : les proxies, le rendu, les tentatives et les blocages. Ce qu’elle n’élimine pas, c’est tout ce qui se passe après l’arrivée de la réponse, et c’est là que la plupart des pipelines échouent réellement.

Les échecs sont silencieux. Des lignes en double provenant d’une nouvelle tentative que personne n’a dédupliquée. Une colonne de prix remplie de chaînes comme "$16.99" que quelqu’un a converties en nombre dans un tableau de bord. Une refonte de site qui a transformé un champ en null il y a trois semaines. Un bug d’extraction qui ne peut être corrigé sans payer pour tout récupérer à nouveau.

Ce guide traite du côté chargement : faire entrer la sortie d’une API de scraping dans une base de données SQL de manière fiable, à volume, sans perdre la capacité d’expliquer ou de rejouer ce que vous avez stocké. Les exemples utilisent PostgreSQL et la Shifter Web Scraping API, et les schémas s’appliquent à toute base de données SQL.

Partez de la réponse que vous obtenez réellement

Avec la Shifter Web Scraping API, vous choisissez entre deux formes de réponse.

Du HTML brut, que vous analysez de votre côté. Ou du JSON structuré, en passant des extract_rules qui associent des sélecteurs CSS à des champs :

curl "https://scrape.shifter.io/v1?api_key=YOUR_API_KEY&url=https://shop.example.com/p/42&render_js=1&extract_rules=%7B%22title%22%3A%7B%22selector%22%3A%22h1%22%2C%22output%22%3A%22text%22%7D%2C%22price%22%3A%7B%22selector%22%3A%22.price%22%2C%22output%22%3A%22text%22%7D%7D"

# {"title": "Example Product", "price": "$19.99"}

Deux propriétés de cette sortie façonnent votre schéma. Un champ dont le sélecteur ne correspond à rien revient sous forme de null plutôt que de faire échouer la requête, si bien qu’un élément manquant et un sélecteur cassé se ressemblent dans la réponse. Et les sorties de type texte sont des chaînes d’affichage, donc les prix, notes et dates arrivent formatés pour des humains, pas typés pour une base de données. La syntaxe des règles, y compris l’extraction de listes pour les pages de résultats, se trouve dans la documentation des règles d’extraction.

Trois couches, pas une seule table

La conception qui survit au contact avec la production sépare ce que vous avez reçu de ce que vous en avez conclu.

CoucheContenuPourquoi elle existe
Atterrissage brutChaque réponse réussie, telle que reçue, avec les métadonnées de récupérationRejouer l’extraction sans re-récupérer
Observations typéesValeurs analysées, typées, validées avec un statut d’analyseCe que les analystes et les applications interrogent
État actuelLa dernière valeur par entité, dérivée des observationsLectures rapides pour les produits et tableaux de bord

La couche brute est celle que les équipes sautent et regrettent. Les crédits sont dépensés sur des requêtes réussies, donc un bug dans votre analyse qui n’est corrigible qu’en récupérant à nouveau coûte le crawl entier deux fois. Atterrissez la réponse d’abord, analysez ensuite, et une correction d’analyse devient une requête de rejeu.

La table d’atterrissage

CREATE TABLE scrape_raw (
  job_id            text        PRIMARY KEY,
  source_url        text        NOT NULL,
  market            text        NOT NULL,
  fetched_at        timestamptz NOT NULL,
  http_status       smallint    NOT NULL,
  body              jsonb       NOT NULL,
  body_hash         text        NOT NULL,
  extractor_version text        NOT NULL
);

CREATE INDEX scrape_raw_url_time ON scrape_raw (source_url, fetched_at DESC);

Quelques choix délibérés là-dedans.

job_id est la clé d’idempotence, calculée avant la requête à partir de l’URL et de la fenêtre de planification, de sorte qu’une nouvelle tentative du même job logique atterrit sur la même clé au lieu de créer une deuxième ligne. extractor_version enregistre quel ensemble de règles d’extraction a produit le corps, ce qui permet de distinguer plus tard un changement de site d’un changement de règles. market enregistre où l’observation a été faite, car la même URL peut retourner un contenu différent selon le pays. Et la clé API n’est jamais stockée dans les métadonnées de requête que vous persistez, car des identifiants dans une base de données sont des identifiants dans chaque sauvegarde.

Charger de manière idempotente

import hashlib
import json
import os

import psycopg
import requests
from psycopg.types.json import Jsonb

API = "https://scrape.shifter.io/v1"
RULES = {
    "title": {"selector": "h1", "output": "text"},
    "price": {"selector": ".price", "output": "text"},
}
EXTRACTOR_VERSION = "product-v3"

INSERT_RAW = """
INSERT INTO scrape_raw
  (job_id, source_url, market, fetched_at, http_status, body, body_hash, extractor_version)
VALUES (%s, %s, %s, now(), %s, %s, %s, %s)
ON CONFLICT (job_id) DO NOTHING
"""


def job_id(url: str, market: str, window: str) -> str:
    return hashlib.sha256(f"{url}|{market}|{window}".encode()).hexdigest()


def fetch(url: str, market: str) -> requests.Response:
    params = {
        "api_key": os.environ["SHIFTER_API_KEY"],
        "url": url,
        "render_js": 1,
        "country": market,
        "extract_rules": json.dumps(RULES),
    }
    return requests.get(API, params=params, timeout=90)


def land(conn: psycopg.Connection, url: str, market: str, window: str) -> None:
    resp = fetch(url, market)
    if resp.status_code != 200:
        raise RuntimeError(f"{resp.status_code} for {url}")
    body = resp.text
    with conn.cursor() as cur:
        cur.execute(
            INSERT_RAW,
            (
                job_id(url, market, window),
                url,
                market,
                resp.status_code,
                Jsonb(json.loads(body)),
                hashlib.sha256(body.encode()).hexdigest(),
                EXTRACTOR_VERSION,
            ),
        )

ON CONFLICT (job_id) DO NOTHING est ce qui rend le chargement sûr à réessayer. Que l’échec vienne du réseau, de votre worker ou de la base de données, relancer le job ne peut pas produire de doublon. Pour MySQL, l’équivalent est une clé unique avec INSERT IGNORE ou ON DUPLICATE KEY UPDATE.

Débit : découpler la récupération du chargement

Le côté récupération a un plafond strict fixé par la limite de concurrence de votre plan, et les requêtes au-delà de celui-ci retournent 429. La base de données a son propre plafond, et c’est généralement celui que les équipes atteignent en premier en ouvrant une connexion et une transaction par page scrapée.

Placez une file d’attente entre les deux. Les workers de récupération, dimensionnés selon votre limite de concurrence, écrivent les réponses dans la file d’attente. Un petit nombre de workers de chargement la vident par lots. Pour des volumes réguliers, executemany de psycopg par lots de quelques centaines de lignes suffit. Pour de grands rechargements rétroactifs, faites un COPY du lot dans une table de staging non journalisée et fusionnez-la en une seule instruction :

INSERT INTO scrape_raw
SELECT * FROM scrape_raw_staging
ON CONFLICT (job_id) DO NOTHING;

Cela transforme des milliers d’allers-retours en un seul, et maintient intacte la garantie d’idempotence.

Pour les rendus longs, l’API peut livrer de manière asynchrone : passez webhook=<URL> et la réponse est postée à votre endpoint dès qu’elle est prête. Rendez ce récepteur également idempotent sur job_id, car toute livraison HTTP peut finir par être retentée par l’une ou l’autre partie.

Faire correspondre les erreurs de l’API au comportement du pipeline

Les codes de statut ne sont pas tous des candidats à une nouvelle tentative, et un loader qui les traite uniformément soit spamme une configuration cassée, soit abandonne face à des échecs transitoires. Le tableau complet se trouve dans erreurs et limites.

StatutComportement du pipeline
408, 422, 500Réessayer avec un backoff exponentiel
429Ralentir et réduire la concurrence des workers
400, 401, 403Erreur de configuration : envoyer vers une dead-letter queue et alerter, ne jamais réessayer
509Crédits épuisés : arrêter l’étape de récupération et alerter, réessayer ne peut pas aider

Les requêtes échouées et les réponses cibles 4xx ou 5xx ne sont pas facturées, et l’API réessaie déjà les échecs transitoires jusqu’à trois fois avant de répondre, donc vos propres tentatives coûtent du temps plutôt que des crédits. Elles coûtent quand même du temps, ce qui explique l’importance du backoff.

Typer les observations

C’est ici que les chaînes d’affichage deviennent des données, et où la plupart des erreurs silencieuses sont introduites.

CREATE TABLE price_observation (
  source_url    text          NOT NULL,
  market        text          NOT NULL,
  observed_at   timestamptz   NOT NULL,
  price_amount  numeric(12,2),
  currency      char(3),
  raw_price     text,
  parse_status  text          NOT NULL,
  job_id        text          NOT NULL REFERENCES scrape_raw (job_id),
  PRIMARY KEY (source_url, market, observed_at)
);

Trois règles maintiennent l’honnêteté.

Gardez la chaîne brute à côté de la valeur analysée. raw_price est ce qui vous permet d’auditer un nombre suspect sans re-récupérer.

Analysez par marché, pas globalement. "1.299,00" et "1,299.00" sont le même prix sous des conventions différentes, et un symbole de devise n’est pas une devise : $ signifie dollar américain, canadien ou australien selon la boutique. Résolvez le code ISO à partir du symbole et du marché ensemble.

Enregistrez pourquoi une valeur est null. Un parse_status de missing, unparseable ou ok distingue “la page n’avait pas de prix” de “notre parseur a échoué”, ce que le null de l’API ne peut pas vous indiquer seul.

Pour l’état actuel, dérivez plutôt que maintenez. Dans PostgreSQL :

CREATE VIEW price_current AS
SELECT DISTINCT ON (source_url, market) *
FROM price_observation
WHERE parse_status = 'ok'
ORDER BY source_url, market, observed_at DESC;

Une vue dérivée ne peut pas dériver hors de synchronisation avec l’historique qu’elle résume.

Détecter la dérive de schéma avant vos utilisateurs

Les sites changent leur balisage, et un sélecteur modifié ne lève pas d’erreur. Il retourne null, la requête réussit, un crédit est dépensé, et la ligne atterrit en semblant valide.

La défense est un moniteur de taux de null par champ, par source, par version d’extracteur. Calculez la part de statuts d’analyse missing pour chaque champ sur une fenêtre glissante et alertez lorsqu’elle s’écarte fortement de sa référence. Un champ de prix qui passe de 2% de manquants à 60% de manquants du jour au lendemain, c’est une refonte, et la détecter le jour même fait la différence entre un sélecteur corrigé et trois semaines d’historique inutilisable.

Quand vous le corrigez, incrémentez extractor_version et rejouez les lignes brutes concernées à travers le nouveau parseur. C’est le bénéfice d’avoir atterri les réponses brutes.

Rétention et partitionnement

Les tables d’atterrissage brut croissent le plus vite et sont le moins lues. Partitionnez-les par date de récupération, conservez-les assez longtemps pour couvrir votre fenêtre de rejeu réaliste, et supprimez les anciennes partitions plutôt que de supprimer des lignes. Les tables d’observations constituent l’enregistrement historique et méritent généralement une rétention plus longue, partitionnée de la même manière.

Que surveiller

MétriqueCe qu’elle détecte
Récupérations réussies par rapport aux lignes atterriesPertes du loader entre l’API et la base de données
Conflits de job_id en doubleTempêtes de nouvelles tentatives ou chevauchements de planification
Taux de null par champ et version d’extracteurChangements de balisage et sélecteurs cassés
Profondeur de file d’attente et retard de chargementUn loader qui prend du retard sur l’étape de récupération
Crédits consommés par rapport aux lignes analysées okArgent dépensé sur des réponses inutilisables

Cette dernière métrique est la vue de coût qui compte : crédits par ligne utilisable, pas crédits par requête. L’usage et le taux d’erreur de l’API elle-même sont visibles dans le panneau sous Web Scraping API.

FAQ

Dois-je stocker le HTML ou le JSON extrait dans la couche brute ?

Le JSON extrait est bien plus petit et généralement suffisant. Ne stockez le HTML que pour les sources où vous prévoyez de changer souvent la logique d’extraction, et donnez à cette table une rétention courte.

Le JSONB est-il suffisant pour être interrogé directement ?

Pour l’exploration, oui. Pour tout ce dont dépend un produit ou un tableau de bord, promouvez les champs en colonnes typées, où la base de données peut imposer les types et utiliser des index ordinaires.

Comment éviter de payer pour des pages qui n’ont pas changé ?

Chaque requête réussie coûte un crédit, donc l’économie doit venir du fait de récupérer moins, pas d’écrire moins. Utilisez des signaux peu coûteux comme une page de listing ou de sitemap pour décider quelles pages de détail doivent effectivement être récupérées.

Cela fonctionne-t-il aussi avec l’API Amazon ?

Oui. Ses réponses sont déjà du JSON structuré, donc les règles d’extraction disparaissent, mais les prix arrivent toujours sous forme de chaînes d’affichage et les schémas d’atterrissage, de typage et de dérive s’appliquent sans changement.

L’essentiel

Une API de scraping résout la collecte. La conception de votre base de données décide si ce que vous avez collecté reste fiable. Atterrissez chaque réponse réussie avec une clé d’idempotence et une version d’extracteur, typez les valeurs par marché tout en conservant la chaîne brute, enregistrez pourquoi une valeur est null, dérivez l’état actuel plutôt que de le maintenir, et surveillez les taux de null par champ pour que les changements de balisage remontent le jour même.

Pour Amazon spécifiquement, les options sont comparées dans les meilleures API de web scraping pour la surveillance Amazon, et une version immobilière du même pipeline se trouve dans comment les entreprises immobilières utilisent les API de web scraping. Le produit se trouve sur la page Web Scraping API, avec les plans sur la page tarifaire.

Prêt à commencer ?

Essayez les proxies résidentiels de Shifter, 205M+ IPs, 195+ pays, à partir de 0,75 $/GB.

Commencer