# ----------------------------------------------------------------------------- # Immo-Ka — Agrégateur de maisons à vendre (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/kw_distinction.py : Keller Williams Distinction (Gatineau/Outaouais) # Site ASP.NET (plateforme tonikwebstudio) : la grille de résultats et les # fiches détail sont rendues via AJAX/UpdatePanel (prix et photos absents du # HTML statique, et non rendus de façon fiable même par Firecrawl). En revanche # le sitemap.xml énumère TOUTES les propriétés, et l'URL encode ville, type, # nombre de chambres, rue et numéro Centris — de quoi bâtir une fiche solide # au niveau liste. Le numéro Centris sert de clé de déduplication. # ----------------------------------------------------------------------------- from __future__ import annotations import re from .base import BaseConnector from ..schema import PropertyListing BASE = "https://www.kwdistinction.com" SITEMAP = f"{BASE}/sitemap.xml" # /maisons-a-vendre-{ville}/{type}/{maison-N-chambres}/{rue}/{no-centris} _URL_RE = re.compile( r'/maisons?-a-vendre-([a-z0-9-]+)/([a-z0-9-]+)/(?:maison-(\d+)-chambres?|[a-z0-9-]+)' r'/([a-z0-9-]+)/(\d{6,})', re.I) _TYPE_MAP = { "maison-de-plain-pied": "Maison", "maison-a-etages": "Maison", "maison-a-paliers": "Maison", "maison-a-paliers-multiples": "Maison", "maison-mobile": "Maison mobile", "appartement": "Condo", "condo": "Condo", "duplex": "Duplex", "triplex": "Triplex", "quadruplex": "Multiplex", "quintuplex": "Multiplex", "terrain": "Terrain", "fermette": "Fermette/Agricole", "chalet": "Chalet", "commercial": "Commercial", "commerciale": "Commercial", } def _deslug(s: str) -> str: return re.sub(r"\s+", " ", s.replace("--", " ").replace("-", " ")).strip().title() def _split_city(slug: str) -> tuple[str, str]: """« aylmer--gatineau- » -> (Gatineau, Aylmer) ; « cayamant » -> (Cayamant, '').""" slug = slug.strip("-") fixes = {"Gatineau": "Gatineau", "Quebec": "Québec", "Montreal": "Montréal"} if "--" in slug: sector, city = slug.split("--", 1) city = _deslug(city) return fixes.get(city, city), _deslug(sector) city = _deslug(slug) return fixes.get(city, city), "" class KwDistinctionConnector(BaseConnector): source_id = "kw_distinction" request_delay = 0.5 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): ville, ptype, beds, rue, centris = m.groups() if centris in by_id: continue city, sector = _split_city(ville) path = m.group(0) by_id[centris] = PropertyListing( source=self.source_id, external_id=centris, url=f"{BASE}{path}", title=_deslug(rue), address=_deslug(rue), city=city, sector=sector, region="Outaouais", property_type=_TYPE_MAP.get(ptype.lower(), ""), bedrooms=int(beds) if beds else None, mls=centris, broker_name="Keller Williams Distinction", ) return list(by_id.values())