spb/auto-ka Public
Python 81.8%
TypeScript 12.4%
CSS 5.5%
1# -----------------------------------------------------------------------------2# Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/okaze.py : OKAZE (Saguenay) — site Webflow (CMS w-dyn-list).5#6# Stratégie : la page /inventaire est rendue côté serveur avec TOUTES les7# annonces (pas de pagination). Chaque carte embarque un <script> inline qui8# pose des attributs data-* dont `data-search` : une chaîne CSV complète —9# nom, année, carrosserie, catégorie, couleur ext., couleur int., marque,10# modèle, moteur, portes, km, prix, motricité, transmission, carburant.11# On parse ces cartes en HTML brut (regex) : une seule requête par sync.12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from ..schema import Vehicle18from .base import BaseConnector1920# OKAZE « vend tout » : écarter les catégories non automobiles21_EXCLUDE_RE = re.compile(22 r"motoneige|vtt|bateau|ponton|motomarine|\bmoto\b|roulotte|remorque|"23 r"\bvr\b|campeur|tracteur|souffleuse|scooter|spyder|side-by-side|atv",24 re.I)2526_CARD_SPLIT = re.compile(r'class="item-inventaire w-dyn-item"')27_HREF_RE = re.compile(r'href="(/vehicules/[^"]+)"')28_IMG_RE = re.compile(r'<img src="(https://[^"]+)"')29_ATTR_RE = re.compile(30 r"item\.setAttribute\('data-([a-z-]+)',\s*(?:formatDate)?\(\"(.*?)\"\)",31 re.S)323334class Okaze(BaseConnector):35 source_id = "okaze"36 base_url = "https://www.okaze.ca"37 dealer_name = "OKAZE"38 city = "Saguenay"39 request_delay: float = 1.04041 def fetch(self) -> list[Vehicle]:42 html = self.get(f"{self.base_url}/inventaire").text43 vehicles: list[Vehicle] = []44 for block in _CARD_SPLIT.split(html)[1:]:45 veh = self._parse_card(block)46 if veh is not None:47 vehicles.append(veh)48 return vehicles4950 def _parse_card(self, block: str) -> Vehicle | None:51 m = _HREF_RE.search(block)52 if not m:53 return None54 path = m.group(1)55 ext_id = path.rstrip("/").rsplit("/", 1)[-1]5657 attrs = {name: value for name, value in _ATTR_RE.findall(block)}58 title = " ".join((attrs.get("name") or "").split())59 search = attrs.get("search") or ""60 fields = [f.strip() for f in search.split(",")]6162 # data-search : positions fixes quand la fiche est complète (15 champs)63 body = category = ext_color = int_color = engine = ""64 drivetrain = transmission = fuel = ""65 doors = km = None66 if len(fields) >= 15:67 (_, _, body, category, ext_color, int_color, _, _,68 engine) = fields[:9]69 drivetrain, transmission, fuel = fields[-3:]70 try:71 doors = int(fields[9]) or None72 except ValueError:73 doors = None74 try:75 km = float(fields[10])76 except ValueError:77 km = None78 else: # fiche partielle : heuristiques79 for f in fields:80 if re.fullmatch(r"\d{2,6}", f) and km is None:81 km = float(f)82 elif "intégrale" in f.lower() or "traction" in f.lower() \83 or "propulsion" in f.lower():84 drivetrain = f85 elif f.lower() in ("automatique", "manuelle", "cvt"):86 transmission = f8788 if _EXCLUDE_RE.search(f"{title} {category} {body}"):89 return None90 if not title:91 return None9293 price_label = attrs.get("price", "")94 if not price_label:95 pm = re.search(r'class="inventory-price"[^>]*>([^<]+)<', block)96 price_label = pm.group(1).strip() if pm else ""9798 details = {}99 if category:100 details["category"] = category101 if attrs.get("added"):102 details["listed_date"] = attrs["added"]103104 img = _IMG_RE.search(block)105106 return Vehicle(107 source=self.source_id,108 external_id=ext_id,109 url=f"{self.base_url}{path}",110 title=title,111 make=attrs.get("brand", "").title(),112 model=attrs.get("model", ""),113 price_label=price_label,114 mileage_km=km,115 mileage_label=(f"{km:,.0f} km".replace(",", " ") if km else ""),116 transmission=transmission,117 fuel=fuel,118 drivetrain=drivetrain,119 body_type=body,120 exterior_color=ext_color.capitalize(),121 interior_color=int_color.capitalize(),122 engine=engine,123 doors=doors,124 dealer_name=self.dealer_name,125 city=self.city,126 details=details,127 images=[img.group(1)] if img else [],128 )129