SPB Git

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%
3.6 KB · 83 lines python
Raw Blame History
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