Zum Inhalt springen
labkompass

Logbuch#homeos#selfhosting#essensplanung#scraping#api

850 filialgenaue Kaufland-Angebote statt Aggregator — und ein 403 ist eine Antwort

Von Niclas Hennrich · veröffentlicht · geschehen am


BuildIm Einsatz

Selbst gebaut und in Betrieb genommen. Gehört zum Vorhaben HomeOS — Control Center mit drei Säulen.

Kaufland-Angebote je Import
850
Importdauer
3.7s
davon mit Card-Zusatzpreis
55

HomeOS ist mein selbst gebautes Control Center fürs Homelab, aus dem Infrastruktur-NOC gewachsen, mit drei Säulen: Infrastruktur, Haus (Smart Home) und Family (Haushalt). Die Essensplanung vom Vortag hatte die komplette Angebots-Pipeline fertig — Provider-Abstraktion, Angebotstabelle mit Dedupe-Index und Preisverlauf, Normalisierung mit Review-Queue, Matching, Planner. Was fehlte, war eine belastbare Primärquelle. Ein inoffizieller Aggregator und manuelle Erfassung sind ein Anfang, aber kein Fundament.

Was ich wollte: Angebote direkt vom Händler, für unsere Region, ohne blindes Scraping und ohne einen Schutzmechanismus zu umgehen. Wenn eine Seite automatisierte Clients nicht will, ist das ihre Entscheidung — und mein Parser baut nicht auf Umgehung.

Die Recherche lief mit höflichen Einzel-Requests und transparentem User-Agent, die Urteile stehen in einer Quellen-Registry mit drei ehrlichen Stufen: APPROVED, REVIEW_REQUIRED, DISALLOWED. REWE, ALDI SÜD und Netto blocken automatisierte Clients per 403 oder Captcha — respektiert, DISALLOWED. Penny deklariert eine eigene REST-Schnittstelle, versteckt die Angebotsroute aber im minifizierten Bundle; geschützte Pfade antworten 401, und ohne Browser-Sitzung mit DevTools lässt sich das nicht seriös bestimmen — REVIEW_REQUIRED statt geraten. Lidl ist Bildprospekt-Gebiet, ebenfalls REVIEW_REQUIRED.

Und dann Kaufland. Die Hauptdomain blockt Bots, aber die Filial-Subdomain für den Prospekt rendert dasselbe Wochenangebot serverseitig als JSON-Block — aktuelle und Folgewoche, rund 2 100 Angebote — dazu je Filiale eine Liste der dort gültigen Angebotsnummern und einen Storefinder mit 788 Filialen. Die Prospekt-Subdomain ist die bessere API. Wer Quellen sucht, sollte die Zweit-Hosts einer Marke prüfen, nicht nur die Hauptdomain.

Der Provider braucht drei GETs je Import: das Gesamtangebot, die Filialliste, der Schnitt über Angebotsnummer und Gültigkeit. Die Filiale wird aus der Haushalts-PLZ aufgelöst. Den Mengen-Parser habe ich nicht gegen vermutete Formate gebaut, sondern gegen die reale Verteilung: 30 Minuten Frequenzanalyse über 2 096 Einheiten-Texte („je 500-g-Packg.", „je Ka. 20 x 0,5-l-Fl." und Verwandte) ersetzten jede Spekulation — und lieferten die Testfälle gleich mit. Ändert Kaufland die Struktur, scheitert der Import kontrolliert statt leer zu importieren.

Was der erste Prod-Import fand: In der Stichprobe stand als Produktname *Mit Kaufland Card. Bei Angeboten mit Kartenrabatt ist title die Marke, subtitle der Produktname und detailTitle nur der Sternchen-Hinweis — und der Kartenpreis ist ein Zusatzpreis neben dem Aktionspreis, der für alle gilt. Mein erstes Mapping hätte 55 von 850 Angeboten falsch benannt und den Kartenvorteil als „nur mit Karte" fehldeklariert. Der Fix ist ein eigenes Feld member_price; member_only bleibt echten Nur-Karte-Angeboten vorbehalten. Der zweite Lauf korrigierte die 850 Bestandszeilen in place: 0 neu, 850 aktualisiert — die Idempotenz über den Dedupe-Schlüssel damit live belegt.

Ein Feld namens detailTitle ist keine Zusage. Meine Fixtures kamen aus der „normalen" Mehrheit der Angebote; den Fehler hätten sie nie gefunden. Seitdem gehört eine Stichprobe nach dem ersten Live-Lauf für jeden neuen Provider zum Fertig-Kriterium.

Stand: 850 filialgenaue Angebote in 3,7 s, 55 davon mit Card-Zusatzpreis, Folgewoche bereits als „kommend" enthalten, 26 neue Tests, Provider-Gesundheit im bestehenden Import-Status. Und ein Satz, der mir wichtiger ist als die Zahl: Ein 403 ist eine Antwort. Lieber ein Händler weniger als ein Parser, der auf Umgehung gebaut ist.