# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/cent4.py : connecteur Terrasse Cent4 (terrassecent4.com — # condos locatifs neufs 3½/4½ au 1700, chemin Saint-Jean, La Prairie, # promoteur Vestric). WordPress (thème Voyou) rendu serveur : la page # /les-unites liste les unités disponibles par bâtiment (Terrasse 2/3) # dans des
  • — numéro, étage, typologie, # pi², loyer, mois de disponibilité + lien « fiche de l'unité ». # La fiche (via self.detail, cache BD) ajoute inclusions et options. # Granularité = UNITÉ. external_id = slug de la fiche (terrasse-2-unite-202). # ----------------------------------------------------------------------------- from __future__ import annotations import html as _html import re from ..schema import Listing from .base import BaseConnector BASE = "https://terrassecent4.com" LIST_URL = f"{BASE}/les-unites" ADDRESS = "1700, chemin Saint-Jean, La Prairie" CITY = "La Prairie" BUILDING_RE = re.compile( r"
    ') FIELD_RE = re.compile( r'vy_units_listing_(unit|floor|type|square|price|availability)[\s\S]*?' r'([\s\S]*?)') FICHE_RE = re.compile(r'href="(https://terrassecent4\.com/units/[^"]+)"') AVAILABLE_RE = re.compile(r'data-filter-available="1"') INCL_RE = re.compile(r"Inclusions([\s\S]*?)(?:Options|Imprimez)", re.I) OPT_RE = re.compile(r"Options([\s\S]*?)Imprimez", re.I) SCRIPT_RE = re.compile(r"<(script|style)[\s\S]*?", re.I) TAG_RE = re.compile(r"<[^>]+>") IMG_RE = re.compile( # Bedrock : médias sous /app/uploads/ (data-src) r'https://terrassecent4\.com/app/uploads/[^"\s)]+' r"\.(?:jpe?g|png|webp)", re.I) def _flat(html: str) -> str: t = _html.unescape(SCRIPT_RE.sub(" ", html)) return re.sub(r"[\s ]+", " ", TAG_RE.sub(" | ", t)) def _clean(txt: str) -> str: return re.sub(r"\s+", " ", _html.unescape(TAG_RE.sub(" ", txt))).strip() class Cent4Connector(BaseConnector): source_id = "cent4" request_delay = 0.8 def fetch(self) -> list[Listing]: try: html = self.get(LIST_URL).text except Exception: return [] listings: list[Listing] = [] seen: set[str] = set() # les items sont regroupés par bloc bâtiment ; on suit le bâtiment # courant en balayant le HTML dans l'ordre pos_buildings = [(m.start(), m.group(1)) for m in BUILDING_RE.finditer(html)] for m in ITEM_RE.finditer(html): chunk = m.group(0) if not AVAILABLE_RE.search(chunk): continue building = "" for pos, b in pos_buildings: if pos < m.start(): building = b.replace("-", " ").title() fields = {k: _clean(v) for k, v in FIELD_RE.findall(chunk)} unit = fields.get("unit", "") fm = FICHE_RE.search(chunk) url = fm.group(1) if fm else LIST_URL ext_id = (url.rsplit("/", 1)[-1] if fm else f"{building.lower().replace(' ', '-')}-{unit}") if not unit or ext_id in seen: continue seen.add(ext_id) price = None pm = re.search(r"(\d[\d\s ]{2,8})\$", fields.get("price", "")) if pm: price = float(re.sub(r"[\s ]", "", pm.group(1))) area = None am = re.search(r"(\d{3,5})\s*pi", fields.get("square", "")) if am: area = float(am.group(1)) availability = re.sub(r"^Disponibilité\s*\|?\s*", "", fields.get("availability", "")).strip() detail = self.detail(ext_id, f"{price}|{availability}", lambda u=url: self._detail(u)) listings.append(Listing( source=self.source_id, external_id=ext_id, url=url, title=f"Terrasse Cent4 ({building}) — unité {unit}", address=ADDRESS, city=CITY, unit_type=fields.get("type", ""), price=price, price_label=f"{int(price)} $ par mois" if price else "", availability=(f"Disponibilité : {availability}" if availability else ""), area_sqft=area, description=detail.get("description", ""), amenities=detail.get("amenities", []), details={"building": building, "floor": fields.get("floor", "")}, images=detail.get("images", []), )) return listings def _detail(self, url: str) -> dict: """Fiche d'unité : inclusions, options, photos.""" try: html = self.get(url).text except Exception: return {} flat = _flat(html) amenities: list[str] = [] for rx in (INCL_RE, OPT_RE): fm = rx.search(flat) if fm: for part in fm.group(1).split("|"): part = part.strip(" .") if 3 <= len(part) <= 120 and part not in amenities: amenities.append(part) images = [u for u in dict.fromkeys(IMG_RE.findall(html)) if not re.search(r"logo|icon|favicon|hummingbird|-\d+x\d+\.", u, re.I)][:10] desc = ("Condo locatif neuf à la Terrasse Cent4, complexe de " f"Vestric à La Prairie ({ADDRESS}).") return {"description": desc, "amenities": amenities, "images": images}