Katalogy autodílů nemají API. Mají HTML — a každá značka ho má jinak. Opel má dvě úrovně navigace a JSON subgroups. Volvo má čtyři úrovně a mixuje HTML s JSONem. Nissan má dvoufázový BOM, kde HTML stránka obsahuje jen kategorie dílů a teprve druhý request vrátí skutečná čísla.
Když potřebujete z takového katalogu tahat data programově, máte dvě možnosti: napsat jeden velký parser, který se větví podle značky, nebo navrhnout architekturu, kde každá značka řeší svoje specifika sama. První možnost funguje do třetí značky. Pak se z toho stane kód, který nikdo nechce číst.
Jedno rozhraní, každá značka vlastní parser
Základ je interface StrutsBrandParser. Definuje, co musí každá značka umět:
- naparsovat subgroups z JSONu
- naparsovat BOM (seznam dílů) z HTML
- poznat, jestli je URL validní VIN stránka
- vytáhnout session klíč z redirect parametru
Volitelně může značka přepsat parsování vehicle info, main groups a subgroups z HTML — to je klíčové pro značky, které nemají standardní layout.
Generic klient (struts-client.ts) řeší navigaci: follow redirectů, cookie auth, fetchování jednotlivých úrovní. Na parsování se ptá parseru konkrétní značky. Když Volvo vrátí HTML tam, kde Opel vrací JSON, klient to nemusí vědět — zavolá parseSubgroupsHtml a dostane normalizované VCfgEntry[], se kterými umí pracovat.
Volvo: čtyři úrovně, žádný JSON na začátku
Volvo je nejhlubší. Navigace vypadá takhle:
decode_vin
→ vin-group.action (level 1 — HTML, main groups)
→ vin-group.action?group1=X (level 2 — HTML, subgroups)
→ json-vin-groups3.action (level 3 — JSON, ilustrace)
→ vin-image-board.action (level 4 — HTML, BOM)
Level 1 a 2 jsou HTML tabulky renderované přímo do stránky. Žádný JSON endpoint pro main groups neexistuje — parser musí číst #nav-group1-table a #nav-group2-table přímo z HTML.
Level 2 je zajímavý: každý řádek tabulky má atribut jsonurl, který ukazuje na level 3. Parser vrátí VCfgEntry[] s vyplněným jsonUrl a generic klient pak udělá follow-up fetch, který projde parseSubgroupsJson. Stejná logika jako u značek, které mají JSON od začátku — jen první krok je navíc.
Level 4 (BOM) má svoje mouchy. Řádky s valid="false" jsou díly, které se nedají objednat samostatně — parser je přeskočí. Název dílu může obsahovat <br> mezi základním názvem a suffixem left/right, takže cheerio .text() nestačí — je potřeba přepsat <br> na mezeru před extrakcí textu.
Session token je hintstoken= (UUID), ne catId= jako u Opelu. Malý detail, ale bez něj nefunguje auth.
Nissan: BOM ve dvou fázích
Nissan má jiný problém. BOM stránka (x-vin-image-board.action) neobsahuje skutečná čísla dílů — jen kategorie (PNC, part-number category). Teprve druhý request na x-json-vin-bom-detail.action?pnc=X vrátí reálné díly pro danou kategorii.
Proto má interface volitelný parseBomPartsAsync — asynchronní verzi BOM parseru. Nissan ho implementuje, ostatní značky ne. Klient zjistí, jestli parser async verzi má, a podle toho se rozhodne, kterou cestou jít.
Detail fetchů je omezený na 6 souběžných requestů — jednak aby to bylo slušné k serveru, jednak aby to nespadlo pod rate limit.
Testy proti zachyceným fixturám
Parser se netestuje proti živému katalogu. Byl by to flaky test — katalog se mění, session expirují, síť je nespolehlivá.
Místo toho se používají zachycené HTML a JSON fixtury z reálných requestů. Volvo parser má 17 unit testů proti fixturám z VIN YV1UZK5VCM1743449 (XC60, brzdy, level 2 → 51 Wheel brake → 416001). Testy pokrývají všechny čtyři úrovně: level 2 HTML, level 3 JSON, level 4 BOM.
Když se změní struktura katalogu, test spadne s jasnou chybou — ne s timeoutem nebo prázdným výsledkem.
Co z toho plyne
Parser architektura pro katalogy autodílů není o tom, napsat co nejchytřejší scraper. Je o tom, navrhnout rozhraní, které unese rozdíly mezi značkami, aniž by se generic klient musel starat o to, jestli zrovna čte HTML nebo JSON.
Volvo má čtyři úrovně. Nissan má dvoufázový BOM. Opel má JSON od začátku. Všechny tři vrací stejné typy — a klient, který je volá, o rozdílech neví.
Dobrá abstrakce není ta, která skrývá složitost. Je to ta, která ji izoluje na jedno místo.