SPB Git

spb/immo-ka Public

Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)

Python 66.4% TypeScript 19.9% CSS 13.2% HTML 0.5%
4.3 KB · 109 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_urbain.py : Keller Williams Urbain (kwurbain.ca)5#   Page /inscriptions rendue serveur : toutes les inscriptions de l'agence en6#   cartes HTML (plateforme marketingwebsites.ca, commune aux bureaux KW Canada).7# -----------------------------------------------------------------------------8from __future__ import annotations910import html as _html11import os12import re1314from .base import BaseConnector15from . import _detailutil as du16from ..normalize import parse_price17from ..schema import PropertyListing1819BASE = "https://www.kwurbain.ca"20LISTINGS = f"{BASE}/inscriptions/"21IMG_TMPL = "https://realestate.marketingwebsites.ca/property-images/{id}/{id}-{n:02d}.jpg"22DETAIL_LIMIT = int(os.environ.get("IMMOKA_KW_DETAIL_LIMIT", "400"))23_KW_IMG_RE = re.compile(24    r'https://realestate\.marketingwebsites\.ca/property-images/\d+/[^"\'\\ ]+\.(?:jpg|jpeg|png|webp)',25    re.I)26_META_DESC_RE = re.compile(r'<meta\s+name="description"\s+content="([^"]{40,})"', re.I)2728_CARD_RE = re.compile(r'property-card mix property-(\d+)"(.*?)(?=property-card mix property-\d+"|</div>\s*</div>\s*</div>\s*</section>|$)', re.S)29_H5_RE = re.compile(r'<h5>(.*?)</h5>', re.S)30_PRICE_RE = re.compile(r'<span>(.*?)</span>', re.S)31_SUB_RE = re.compile(r'card-subtitle[^>]*>(.*?)</h6>', re.S)32_BED_RE = re.compile(r'fa-bed"></i>\s*(\d+)')33_BATH_RE = re.compile(r'fa-bath"></i>\s*(\d+)')34_STATUS_RE = re.compile(r'card-status[^>]*>(.*?)</div>', re.S)353637def _txt(s: str) -> str:38    return _html.unescape(re.sub(r"<[^>]+>", "", s or "")).strip()394041class KwUrbainConnector(BaseConnector):42    source_id = "kw_urbain"43    request_delay = 0.64445    def fetch(self) -> list[PropertyListing]:46        html = self.get(LISTINGS).text47        listings: list[PropertyListing] = []48        for pid, block in _CARD_RE.findall(html):49            lst = self._to_listing(pid, block)50            if lst is not None:51                listings.append(lst)52        # fiche détail : galerie complète + description53        du.enrich(self, listings, DETAIL_LIMIT, parse_kw_detail, key="v2")54        return listings5556    def _to_listing(self, pid: str, block: str) -> PropertyListing | None:57        addr = _txt((_H5_RE.search(block) or [None, ""])[1] if _H5_RE.search(block) else "")58        m_addr = _H5_RE.search(block)59        addr = _txt(m_addr.group(1)) if m_addr else ""60        m_price = _PRICE_RE.search(block)61        price_label = _txt(m_price.group(1)) if m_price else ""62        m_sub = _SUB_RE.search(block)63        sub = _txt(m_sub.group(1)) if m_sub else ""     # "Blainville, Laurentides J7B1M1"64        city = region = ""65        if sub:66            parts = [p.strip() for p in sub.split(",")]67            city = parts[0]68            if len(parts) > 1:69                # « Laurentides J7B1M1 » -> retirer le code postal70                region = re.sub(r"\s*[A-Z]\d[A-Z]\s?\d[A-Z]\d\s*$", "", parts[1]).strip()71        beds = _BED_RE.search(block)72        baths = _BATH_RE.search(block)73        return PropertyListing(74            source=self.source_id,75            external_id=pid,76            url=f"{BASE}/inscriptions/inscription/{pid}",77            title=addr,78            address=addr,79            city=city,80            region=region,81            price=parse_price(price_label),82            price_label=price_label,83            bedrooms=int(beds.group(1)) if beds else None,84            bathrooms=int(baths.group(1)) if baths else None,85            images=[IMG_TMPL.format(id=pid, n=1)],86            broker_name="Keller Williams Urbain",87        )888990def parse_kw_detail(html: str) -> dict:91    """Galerie complète + description (balise meta) de la fiche KW."""92    out: dict = {}93    seen, imgs = set(), []94    for u in _KW_IMG_RE.findall(html):95        if u not in seen:96            seen.add(u)97            imgs.append(u)98    if imgs:99        out["images"] = imgs100    m = _META_DESC_RE.search(html)101    if m:102        d = _html.unescape(m.group(1)).strip()103        if d and not d.lower().startswith(("keller williams", "trouvez")):104            out["description"] = d105    _det = du.centris_details(du.flatten(html))106    if _det:107        out.setdefault("details", {}).update(_det)108    return out109