Jeder baut zuerst den Aggregator-Scraper. Cars.com, CarGurus, AutoTrader: drei Websites, ein Parser pro Seite, und am Ende der Woche hast du einen Feed, der wie der Gebrauchtwagenmarkt aussieht.
Dann fragt jemand, wo der Rest ist.
Die Herausforderung
Laut NADA-Bericht von Mitte 2025 gibt es in den USA 16.972 Vertragshändler für PKW und leichte Nutzfahrzeuge. Unabhängige Händler sind da noch gar nicht mitgezählt, die erfasst ohnehin niemand einheitlich. Sekundärberichte zur selben NADA-Zahl schwanken zwischen 15.720 und 16.990, was einiges darüber aussagt, wie ungenau dieser Markt gemessen wird.
Jeder einzelne dieser Händlerstandorte betreibt eine eigene Website. Der dortige Fahrzeugbestand ist das aktuelle Angebot dieses Händlers, mit den Preisen von heute, Tage bevor irgendetwas davon auf Plattformen Dritter landet. Wenn du Gebrauchtwagenpreise kalkulierst, Restwerte prognostizierst oder ein Wettbewerbstool an Händler verkaufst, sind die Websites der Händler genau die Datenquelle, die du brauchst. Die Aggregatoren sind nur eine verzögerte, gefilterte Kopie davon.
Teams nehmen sich also die Händler-Websites vor und stellen der Reihe nach drei Dinge fest.
Sie sind nicht einzigartig. Fast alle laufen auf einer kleinen Auswahl von Händler-Plattformen: Dealer.com, DealerOn, Dealer Inspire, CDK, Reynolds, Sincro, Lotlinx (die genaue Liste variiert je nach Zählung). Wer diese Daten verkauft, baut einen Parser pro Plattform, nicht einen pro Händler. Beim dealer website inventory scraper von Apify steht es direkt in der Beschreibung: Zuerst die Plattform erkennen, dann extrahieren. Eine Handvoll Templates deckt Zehntausende von Standorten ab. Das ist die gute Nachricht.
Der Preis steht meistens nicht im abgerufenen HTML. Diese Plattformen rendern den Preisblock clientseitig. Ratenberechnungen und Rabatte kommen oft erst über einen zweiten Request danach. Ein einfacher HTTP-Abruf liefert dir Baujahr, Marke, Modell, Kilometerstand und Fahrgestellnummer (VIN). Das Preisfeld bleibt leer.
Und dann der Punkt, der Datensätze unbemerkt ruiniert: Auf einer Händlerseite sieht ein Fehler exakt wie ein valider Datensatz aus. Ein Bot-Erkennungsdienst beantwortet einen verdächtigen Request mit HTTP 200 und einer Zwischenseite. Ein Preisblock, der nie gerendert wurde, hinterlässt "Call for Price" im DOM, was Händler dort allerdings auch bewusst eintragen. Beide Zeilen landen in deinem Data Warehouse und sehen absolut sauber aus.
Wir haben dieses Muster bereits in einer anderen Branche beschrieben, in der eine Blockierung wie ein Datenpunkt aussieht. Die Autobranche ist die schwierigere Variante, weil "kein Preis" ein legitimer Geschäftszustand ist und keine offensichtliche Anomalie.
Was sich ändert, wenn du nach Kosten trennst
Stabile Pipelines strukturieren sich nicht nach Websites. Sie strukturieren sich danach, was ein Request kostet.
Der teure Request ist der erste gegen einen Standort: derjenige, der einen Browser starten, vorgeschaltete Hürden der Plattform überwinden und mit einer Session zurückkehren muss. Alles danach ist ein günstiger HTTP-Call, der die Ergebnisse des ersten Requests wiederverwendet.
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
Lies dann den Rest des Bestands dieses Händlers aus, ohne erneut für einen Browser zu zahlen:
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()
Zwei Details dort wiegen schwerer, als es auf den ersten Blick scheint.
Die Session reist als eine Einheit. Ein Clearance-Cookie ist an den Exit gebunden, der ihn erhalten hat, sowie an den User-Agent, unter dem er empfangen wurde. Wenn du ihn von einem anderen Ort oder unter einem anderen User-Agent erneut abspielst, schickt dich die Seite direkt zurück zur Challenge. Deshalb wandern Jar, Agent-String und Proxy-ID gemeinsam. Genau diesen Fehler sehen wir am häufigsten: Entwickler behalten den Cookie, verwerfen den Exit und wundern sich dann, warum der günstige Pfad nicht mehr günstig ist.
Der validate-Block eliminiert das Problem stiller Fehler. Hier definiert der Request, wie eine echte Seite aussieht: Akzeptiere einen Marker, der erst existiert, sobald das Listing-Grid gerendert ist, und brich bei Interstitial-Strings ab. Eine Response, die diese Regeln verfehlt, ist keine Zeile mit einem Null-Preis. Es ist ein Fehler, genau so klassifiziert, und wird nicht als Erfolg gewertet. Im Automotive-Bereich solltest du sowohl den positiven Marker als auch die Negativliste definieren. Denn "Preis auf Anfrage" ist tatsächlich uneindeutig, "die Fahrzeugkarte wurde nie gerendert" hingegen nie.
Wenn du noch nicht weißt, welchen Pfad eine bestimmte Plattform benötigt, findet Auto das in einem einzigen Call heraus und liefert die funktionierende Session zurück. Betrachte das als Aufklärung, nicht als Produktionsroute. Sobald du weißt, dass eine Plattform den Browser benötigt und benachbarte Plattformen nicht, pinne jede fest an die direkte Engine. Hör auf, einen Orchestrator dafür zu bezahlen, jede Nacht dieselbe Antwort neu zu ermitteln.
Ergebnisse
Rechne das Ganze für einen mittelgroßen Job durch (Beispielszenario basierend auf Branchen-Benchmarks, kein konkreter Kunde): 4.000 Standorte, jeweils etwa 180 Gebrauchtwagen, nächtlicher Refresh.
- 4.000 gerenderte Seiten statt 720.000. Ein Render pro Standort öffnet die Session. Die anderen 716.000 Seiten laufen über den günstigen Pfad auf derselben Session. Dieses Verhältnis, nicht der Parser, entscheidet darüber, ob die nächtliche Abdeckung bezahlbar bleibt.
- Zwei Arten fehlender Preise in zwei separaten Tabellen. Mit aktiven Validate-Regeln teilen sich "der Händler veröffentlicht keinen Preis" und "wir haben die Seite nie erhalten" nicht mehr dieselbe Zeilenstruktur. Dein Modell sieht ausschließlich den ersten Fall.
- Ein Parser pro Plattform, nicht pro Händler. Erkenne die Plattform anhand der Response und übergib das HTML an den zuständigen Parser. Einen neuen Standort auf einer bereits abgedeckten Plattform aufzuschalten, kostet dich nichts.
- Ein Händler, der die Plattform wechselt, schlägt sofort und sichtbar fehl. Die Plattformerkennung schlägt fehl, die Zeile wird gar nicht erst geschrieben, und jemand bekommt ein Ticket statt sechs Wochen unbemerkt falscher Preise.
Wo es komplexer wird: Große Händlergruppen betreiben zunehmend individuelle Seiten abseits der Standardplattformen. Diese erfordern weiterhin manuell gebaute Parser samt dem entsprechenden Wartungsaufwand. Auch die Aktualität der Standorte ist nicht einheitlich. Einige Plattformen cachen ihre Bestandsseiten aggressiv, sodass der "heutige Preis" bereits einen Tag alt sein kann, egal wie oft du scrapest. Wenn dein Modell jeden Standort-Zeitstempel als gleichermaßen aktuell behandelt, liegt es auf eine Weise falsch, die keine Scraping-Infrastruktur der Welt korrigieren kann.
Fazit
Der schwierige Teil bei Fahrzeugdaten waren nie die drei Aggregatoren, an denen sich alle messen. Es sind die siebzehntausend kleinen Websites, für die sich ein eigener Scraper allein nie gelohnt hat, die zusammen aber extrem wertvoll sind.
Dieses Muster zeigt sich weit über Autos hinaus. Apotheken, Baumaschinenhändler, regionale Lebensmittelhändler, Franchisebetriebe jeder Art: Der Long Tail wirkt nur so lange teuer, wie du jedes einzelne Ziel als einzigartige Website behandelst. Meistens ist es das nicht. Bring den ersten Request sauber durch, mach die Session wiederverwendbar, und schon ist das Ganze kein Erfassungsproblem mehr, sondern ein Parsing-Problem, und das ist mit großem Abstand die günstigere Variante.