# ----------------------------------------------------------------------------- # Immo-Ka — Agrégateur de maisons à vendre (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/kw_prestige.py : Keller Williams Prestige (kwprestige.com) # Bannière KW du Grand Montréal. La grille /fr/proprietes/ et les fiches détail # sont rendues par un widget JS (pagination AJAX, pas d'URL exploitable). Mais # le sitemap.xml expose TOUTES les fiches : URL # « {type}-a-vendre-{ville-secteur}-{no-centris}-detail-Fr ». On énumère donc # par le sitemap (rendu serveur, fiable) ; type/ville/secteur/n° Centris depuis # le slug. external_id = n° Centris → déduplication. # ----------------------------------------------------------------------------- from __future__ import annotations import re from .base import BaseConnector from ..schema import PropertyListing SITEMAP = "https://www.kwprestige.com/sitemap.xml" # {type}-a-vendre-{ville-secteur}-{centris}-detail-Fr _URL_RE = re.compile( r'https://www\.kwprestige\.com/([a-z0-9-]+)-a-vendre-([a-z0-9-]+?)-(\d{7,8})-detail-[A-Za-z]+', re.I) _TYPE = [ ("location-d-espace", "Commercial"), ("location-de", "Commercial"), ("location", "Commercial"), ("maison-etages", "Maison"), ("maison-paliers", "Maison"), ("maison-plain-pied", "Maison"), ("maison-mobile", "Maison mobile"), ("maison", "Maison"), ("appartement", "Condo"), ("condo", "Condo"), ("loft", "Condo"), ("duplex", "Duplex"), ("triplex", "Triplex"), ("quadruplex", "Multiplex"), ("quintuplex", "Multiplex"), ("propriete-revenu", "Multiplex"), ("terrain", "Terrain"), ("chalet", "Chalet"), ("fermette", "Fermette/Agricole"), ("ferme", "Fermette/Agricole"), ("local-commercial", "Commercial"), ("espace-commercial", "Commercial"), ("espace-industriel", "Commercial"), ("espace-bureau", "Commercial"), ("commerce", "Commercial"), ("bureau", "Commercial"), ("batisse", "Commercial"), ("industriel", "Commercial"), ] # villes composées fréquentes (sinon 1er token = ville) _CITY_FIX = {"montreal": "Montréal", "quebec": "Québec", "levis": "Lévis", "trois": "Trois-Rivières", "saint": "Saint", "sainte": "Sainte"} class KwPrestigeConnector(BaseConnector): source_id = "kw_prestige" request_delay = 0.4 def fetch(self) -> list[PropertyListing]: try: xml = self.get(SITEMAP).text except Exception: return [] by_id: dict[str, PropertyListing] = {} for m in _URL_RE.finditer(xml): typ_slug, loc_slug, centris = m.group(1), m.group(2), m.group(3) if centris in by_id: continue by_id[centris] = self._to_listing(m.group(0), typ_slug, loc_slug, centris) return list(by_id.values()) def _to_listing(self, url, typ_slug, loc_slug, centris) -> PropertyListing: ptype = "" for key, canon in _TYPE: if typ_slug.startswith(key): ptype = canon break parts = loc_slug.split("-") city = _CITY_FIX.get(parts[0], parts[0].title()) if parts else "" sector = " ".join(parts[1:]).title() if len(parts) > 1 else "" return PropertyListing( source=self.source_id, external_id=centris, url=url, title=_deslug(loc_slug), address="", city=city, sector=sector, region="", property_type=ptype, mls=centris, broker_name="Keller Williams Prestige", agency="Keller Williams Prestige", ) def _deslug(s: str) -> str: return re.sub(r"\s+", " ", s.replace("-", " ")).strip().title()