# ----------------------------------------------------------------------------- # Immo-Ka — Agrégateur de maisons à vendre (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/kw_urbain.py : Keller Williams Urbain (kwurbain.ca) # Page /inscriptions rendue serveur : toutes les inscriptions de l'agence en # cartes HTML (plateforme marketingwebsites.ca, commune aux bureaux KW Canada). # ----------------------------------------------------------------------------- from __future__ import annotations import html as _html import os import re from .base import BaseConnector from . import _detailutil as du from ..normalize import parse_price from ..schema import PropertyListing BASE = "https://www.kwurbain.ca" LISTINGS = f"{BASE}/inscriptions/" IMG_TMPL = "https://realestate.marketingwebsites.ca/property-images/{id}/{id}-{n:02d}.jpg" DETAIL_LIMIT = int(os.environ.get("IMMOKA_KW_DETAIL_LIMIT", "400")) _KW_IMG_RE = re.compile( r'https://realestate\.marketingwebsites\.ca/property-images/\d+/[^"\'\\ ]+\.(?:jpg|jpeg|png|webp)', re.I) _META_DESC_RE = re.compile(r'\s*\s*\s*|$)', re.S) _H5_RE = re.compile(r'
(.*?)
', re.S) _PRICE_RE = re.compile(r'(.*?)', re.S) _SUB_RE = re.compile(r'card-subtitle[^>]*>(.*?)', re.S) _BED_RE = re.compile(r'fa-bed">\s*(\d+)') _BATH_RE = re.compile(r'fa-bath">\s*(\d+)') _STATUS_RE = re.compile(r'card-status[^>]*>(.*?)', re.S) def _txt(s: str) -> str: return _html.unescape(re.sub(r"<[^>]+>", "", s or "")).strip() class KwUrbainConnector(BaseConnector): source_id = "kw_urbain" request_delay = 0.6 def fetch(self) -> list[PropertyListing]: html = self.get(LISTINGS).text listings: list[PropertyListing] = [] for pid, block in _CARD_RE.findall(html): lst = self._to_listing(pid, block) if lst is not None: listings.append(lst) # fiche détail : galerie complète + description du.enrich(self, listings, DETAIL_LIMIT, parse_kw_detail, key="v2") return listings def _to_listing(self, pid: str, block: str) -> PropertyListing | None: addr = _txt((_H5_RE.search(block) or [None, ""])[1] if _H5_RE.search(block) else "") m_addr = _H5_RE.search(block) addr = _txt(m_addr.group(1)) if m_addr else "" m_price = _PRICE_RE.search(block) price_label = _txt(m_price.group(1)) if m_price else "" m_sub = _SUB_RE.search(block) sub = _txt(m_sub.group(1)) if m_sub else "" # "Blainville, Laurentides J7B1M1" city = region = "" if sub: parts = [p.strip() for p in sub.split(",")] city = parts[0] if len(parts) > 1: # « Laurentides J7B1M1 » -> retirer le code postal region = re.sub(r"\s*[A-Z]\d[A-Z]\s?\d[A-Z]\d\s*$", "", parts[1]).strip() beds = _BED_RE.search(block) baths = _BATH_RE.search(block) return PropertyListing( source=self.source_id, external_id=pid, url=f"{BASE}/inscriptions/inscription/{pid}", title=addr, address=addr, city=city, region=region, price=parse_price(price_label), price_label=price_label, bedrooms=int(beds.group(1)) if beds else None, bathrooms=int(baths.group(1)) if baths else None, images=[IMG_TMPL.format(id=pid, n=1)], broker_name="Keller Williams Urbain", ) def parse_kw_detail(html: str) -> dict: """Galerie complète + description (balise meta) de la fiche KW.""" out: dict = {} seen, imgs = set(), [] for u in _KW_IMG_RE.findall(html): if u not in seen: seen.add(u) imgs.append(u) if imgs: out["images"] = imgs m = _META_DESC_RE.search(html) if m: d = _html.unescape(m.group(1)).strip() if d and not d.lower().startswith(("keller williams", "trouvez")): out["description"] = d _det = du.centris_details(du.flatten(html)) if _det: out.setdefault("details", {}).update(_det) return out