spb/ora-ka Public
Ora-Ka — cinq agrégateurs Ka, une barre de recherche hybride (exact + sémantique)
Python 80%
TypeScript 12.9%
CSS 6.8%
1# -----------------------------------------------------------------------------2# Immo-Ka — Agrégateur de maisons à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/kw_prestige.py : Keller Williams Prestige (kwprestige.com)5# Bannière KW du Grand Montréal. La grille /fr/proprietes/ et les fiches détail6# sont rendues par un widget JS (pagination AJAX, pas d'URL exploitable). Mais7# le sitemap.xml expose TOUTES les fiches : URL8# « {type}-a-vendre-{ville-secteur}-{no-centris}-detail-Fr ». On énumère donc9# par le sitemap (rendu serveur, fiable) ; type/ville/secteur/n° Centris depuis10# le slug. external_id = n° Centris → déduplication.11# -----------------------------------------------------------------------------12from __future__ import annotations1314import re1516from .base import BaseConnector17from ..schema import PropertyListing1819SITEMAP = "https://www.kwprestige.com/sitemap.xml"20# {type}-a-vendre-{ville-secteur}-{centris}-detail-Fr21_URL_RE = re.compile(22 r'https://www\.kwprestige\.com/([a-z0-9-]+)-a-vendre-([a-z0-9-]+?)-(\d{7,8})-detail-[A-Za-z]+',23 re.I)2425_TYPE = [26 ("location-d-espace", "Commercial"), ("location-de", "Commercial"),27 ("location", "Commercial"),28 ("maison-etages", "Maison"), ("maison-paliers", "Maison"),29 ("maison-plain-pied", "Maison"), ("maison-mobile", "Maison mobile"),30 ("maison", "Maison"), ("appartement", "Condo"), ("condo", "Condo"),31 ("loft", "Condo"), ("duplex", "Duplex"), ("triplex", "Triplex"),32 ("quadruplex", "Multiplex"), ("quintuplex", "Multiplex"),33 ("propriete-revenu", "Multiplex"), ("terrain", "Terrain"),34 ("chalet", "Chalet"), ("fermette", "Fermette/Agricole"),35 ("ferme", "Fermette/Agricole"),36 ("local-commercial", "Commercial"), ("espace-commercial", "Commercial"),37 ("espace-industriel", "Commercial"), ("espace-bureau", "Commercial"),38 ("commerce", "Commercial"), ("bureau", "Commercial"),39 ("batisse", "Commercial"), ("industriel", "Commercial"),40]41# villes composées fréquentes (sinon 1er token = ville)42_CITY_FIX = {"montreal": "Montréal", "quebec": "Québec", "levis": "Lévis",43 "trois": "Trois-Rivières", "saint": "Saint", "sainte": "Sainte"}444546class KwPrestigeConnector(BaseConnector):47 source_id = "kw_prestige"48 request_delay = 0.44950 def fetch(self) -> list[PropertyListing]:51 try:52 xml = self.get(SITEMAP).text53 except Exception:54 return []55 by_id: dict[str, PropertyListing] = {}56 for m in _URL_RE.finditer(xml):57 typ_slug, loc_slug, centris = m.group(1), m.group(2), m.group(3)58 if centris in by_id:59 continue60 by_id[centris] = self._to_listing(m.group(0), typ_slug, loc_slug, centris)61 return list(by_id.values())6263 def _to_listing(self, url, typ_slug, loc_slug, centris) -> PropertyListing:64 ptype = ""65 for key, canon in _TYPE:66 if typ_slug.startswith(key):67 ptype = canon68 break69 parts = loc_slug.split("-")70 city = _CITY_FIX.get(parts[0], parts[0].title()) if parts else ""71 sector = " ".join(parts[1:]).title() if len(parts) > 1 else ""72 return PropertyListing(73 source=self.source_id, external_id=centris, url=url,74 title=_deslug(loc_slug), address="", city=city, sector=sector,75 region="", property_type=ptype, mls=centris,76 broker_name="Keller Williams Prestige",77 agency="Keller Williams Prestige",78 )798081def _deslug(s: str) -> str:82 return re.sub(r"\s+", " ", s.replace("-", " ")).strip().title()83