← Tous les articles

Scraping d'inventaire de concessionnaires au-delà des agrégateurs

Le scraping d'inventaire de concessionnaires échoue sur les sites finaux, pas sur les agrégateurs. Prix côté client, poignée de templates de plateformes et blocages déguisés en données.

Tout le monde commence par concevoir le scraper d'agrégateurs. Cars.com, CarGurus, AutoTrader: trois sites, un parser chacun, et à la fin de la semaine, vous disposez d'un flux qui ressemble au marché des véhicules d'occasion.

Puis quelqu'un demande où se trouve le reste.

Le Défi

La NADA recense 16 972 concessionnaires franchisés de véhicules légers aux États-Unis selon son rapport de mi-2025, sans compter les parcs indépendants que personne ne mesure de manière cohérente. Les analyses secondaires de ce même chiffre de la NADA oscillent entre 15 720 et 16 990, ce qui en dit long sur la précision des mesures de ce marché.

Chacune de ces concessions exploite son propre site web. L'inventaire qui s'y trouve correspond au stock réel du concessionnaire, avec les prix du jour, des jours avant qu'ils n'apparaissent sur un site d'annonces tiers. Si vous calculez le prix de véhicules d'occasion, prévoyez des valeurs résiduelles ou vendez un outil concurrentiel aux concessionnaires, le site de la concession est la donnée dont vous avez besoin. Les agrégateurs n'en sont qu'une copie filtrée et différée.

Les équipes s'attaquent donc aux sites des concessions et découvrent trois choses dans l'ordre.

Ils ne sont pas uniques. La quasi-totalité repose sur un nombre restreint de plateformes spécialisées: Dealer.com, DealerOn, Dealer Inspire, CDK, Reynolds, Sincro, Lotlinx (la liste exacte dépend des sources). Quiconque vend ces données conçoit un parser par plateforme, et non un par concession. Le scraper d'inventaire de sites de concessionnaires d'Apify l'affiche clairement: détecter d'abord la plateforme, puis extraire. Une poignée de modèles couvre des dizaines de milliers de concessions. C'est la bonne nouvelle.

Le prix ne figure généralement pas dans le HTML que vous avez récupéré. Ces plateformes effectuent le rendu du bloc de prix côté client, et les estimations de paiement ou les promotions arrivent souvent lors d'un second appel ultérieur. Une simple requête HTTP vous renvoie l'année, la marque, le modèle, le kilométrage et le VIN. Le champ du prix reste vide.

Vient ensuite la partie qui corrompt silencieusement les jeux de données: sur le site d'un concessionnaire, un échec ressemble trait pour trait à une donnée valide. Un service de détection de bots répond à une requête suspecte par un code HTTP 200 et une page intermédiaire. Un bloc de prix qui ne s'est jamais affiché laisse la mention "Call for Price" dans le DOM, ce que les concessionnaires écrivent aussi délibérément. Les deux lignes arrivent dans votre entrepôt de données avec une structure tout aussi propre.

Nous avons abordé ce cas de figure dans un autre secteur, où un blocage ressemble à un point de données. Le secteur automobile représente la version complexe, car l'absence de prix est un état opérationnel légitime et non une anomalie flagrante.

Ce qui Change Lorsqu'on Sépare par Coût

Les pipelines robustes ne s'organisent pas par site. Ils s'organisent selon le coût d'une requête.

La requête coûteuse est la première exécutée contre une concession: celle qui doit lancer un navigateur, franchir les protections mises en place par la plateforme du concessionnaire et revenir avec une session. Tout ce qui suit est un appel HTTP peu coûteux qui réutilise ce que la première requête a obtenu.

import requests

FOURA = "https://api.foura.ai/api"
AUTH = {"Authorization": "Bearer pk_live_..."}

# First page of a rooftop: render it, clear the defense, keep the session.
first = requests.post(f"{FOURA}/browser", headers=AUTH, json={
    "url": "https://example-motors.com/used-inventory/index.htm",
    "unblocker": True,
    "timeout_ms": 45000,
}).json()

listings = first["body"]
jar      = "; ".join(f'{c["name"]}={c["value"]}' for c in first["cookies"])
agent    = first["userAgent"]
exit_id  = first["proxy"]     # opaque proxy ID, send it back to stay on the same exit

Parcourez ensuite le reste de l'inventaire de ce concessionnaire sans payer à nouveau pour un navigateur :

page = requests.post(f"{FOURA}/single", headers=AUTH, json={
    "method": "GET",
    "url": "https://example-motors.com/used-inventory/index.htm?start=20",
    "proxy": exit_id,
    "headers": [["Cookie", jar], ["User-Agent", agent]],
    "validate": {
        "status": {"accept": [200]},
        "data": {
            "accept": ["vehicle-card"],
            "fail": ["Just a moment", "Access Denied"]
        }
    }
}).json()

Deux détails ici ont bien plus d'importance qu'il n'y paraît.

La session voyage comme une seule unité. Un cookie d'autorisation est lié au proxy de sortie qui l'a obtenu et au User-Agent sous lequel il a été acquis. Rejouez-le depuis un autre endroit, ou avec un User-Agent différent, et le site vous renvoie au challenge initial. C'est pourquoi le jar, la chaîne de l'agent et l'ID du proxy se déplacent ensemble. C'est l'erreur que nous voyons commettre le plus souvent: les développeurs gardent le cookie, abandonnent le proxy de sortie, puis se demandent pourquoi la voie économique ne l'est plus.

Le bloc validate est ce qui élimine le problème des échecs silencieux. C'est la requête qui définit à quoi ressemble une vraie page: accepter un marqueur qui n'existe qu'une fois la grille d'annonces rendue, échouer sur les chaînes interstitielles. Une réponse qui ne respecte pas ces règles n'est pas une ligne avec un prix nul. C'est un échec, classé comme tel, et non comptabilisé comme un succès. Dans l'automobile, écrivez le marqueur positif ainsi que la liste négative, car "Appeler pour le prix" est réellement ambigu, alors que "la fiche du véhicule n'a jamais été rendue" ne l'est jamais.

Lorsque vous ne savez pas encore quelle voie nécessite une plateforme donnée, Auto le découvrira en un seul appel et vous renverra la session fonctionnelle. Considérez cela comme de la reconnaissance, pas comme la route de production. Une fois que vous savez qu'une plateforme nécessite le navigateur et que ses homologues ne l'exigent pas, assignez chacune au moteur direct et cessez de payer un orchestrateur pour redécouvrir la même réponse chaque nuit.

Résultats

Faites le calcul sur un projet de taille moyenne (scénario indicatif basé sur des références du secteur, sans référence à un client spécifique): 4 000 concessions, environ 180 véhicules d'occasion chacune, actualisés chaque nuit.

  • 4 000 pages rendues au lieu de 720 000. Un seul rendu par concession ouvre la session; les 716 000 autres pages passent par la voie économique sur la même session. C'est ce ratio, et non le parser, qui détermine si la couverture nocturne reste abordable.
  • Deux types de prix manquants, dans deux tables différentes. Avec les règles de validation en place, "le concessionnaire ne publie aucun prix" et "nous n'avons jamais reçu la page" cessent de partager la même structure de ligne. Votre modèle ne traite ainsi que le premier cas.
  • Un parser par plateforme, non par concession. Détectez la plateforme à partir de la réponse, transmettez l'HTML au parser dédié. L'intégration d'une nouvelle concession sur une plateforme déjà prise en charge ne coûte rien.
  • Une concession qui change de plateforme échoue de manière explicite. La détection de la plateforme échoue, la ligne n'est jamais écrite, et un ticket est généré au lieu de propager des prix erronés pendant six semaines en silence.

Là où la situation devient moins nette: les grands groupes de concessionnaires exploitent de plus en plus des sites sur mesure hors plateforme, qui nécessitent toujours des parsers développés à la main avec toute la maintenance associée. La fraîcheur des données par concession n'est pas non plus uniforme. Certaines plateformes appliquent une mise en cache agressive sur leurs pages d'inventaire, de sorte que "le prix du jour" peut dater de la veille, peu importe votre fréquence de collecte. Si votre modèle considère chaque horodatage de concession comme étant en temps réel, il est biaisé d'une manière qu'aucune infrastructure de collecte ne pourra corriger.

Conclusion principale

La difficulté avec les données automobiles n'a jamais résidé dans les trois agrégateurs sur lesquels tout le monde se base. Elle réside dans les dix-sept mille petits sites qui ne justifiaient pas un scraper dédié à eux seuls, mais qui ont une immense valeur une fois rassemblés.

Cette configuration se retrouve bien au-delà de l'automobile. Pharmacies, distributeurs d'équipements, épiceries régionales, franchises en tout genre: la longue traîne ne semble coûteuse que si vous traitez chaque acteur comme un site unique. Ce n'est généralement pas le cas. Réussissez la première request, rendez la session réutilisable, et ce qui reste cesse d'être un problème de collecte pour devenir un problème de parsing, ce qui est de loin le moins coûteux des deux.