# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/central_rosemont.py : connecteur Central Rosemont # (centralrosemont.com) — condos locatifs au 1295, rue des Carrières, # Rosemont (Montréal), au bord de la piste des Carrières. WordPress rendu # serveur : la page /appartements-disponibles/ affiche un bloc # div.bloc_unite_dispo[data-unite] par unité disponible — numéro, étage, # superficie (pi²), nombre de chambres, prix « 2465$/mois » (parfois vide), # date de disponibilité (« juin 2026 ») et plan PDF. Une annonce par unité, # id stable = numéro d'unité (data-unite). # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing from .base import BaseConnector BASE = "https://www.centralrosemont.com" LIST_URL = f"{BASE}/appartements-disponibles/" ADDRESS = "1295, rue des Carrières, Montréal, QC H2S 0E1" # Nombre de chambres -> type d'unité (convention QC : n½ = n-2 chambres) _BED_TYPE = {0: "Studio", 1: "3½", 2: "4½", 3: "5½", 4: "6½"} _PRICE_RE = re.compile(r"([\d\s,]{3,7})\s*\$") _SQFT_RE = re.compile(r"([\d\s]{2,6})\s*pi", re.I) _FLOOR_RE = re.compile(r"Étage\s*(\d+)", re.I) _IMG_RE = re.compile( r'https://(?:www\.)?centralrosemont\.com/wp-content/uploads/' r'[^"\'\s\\)]+?\.(?:jpg|jpeg|png|webp)', re.I) _SKIP_IMG = re.compile(r"logo|icon|favicon|-\d{2,4}x\d{2,4}\.", re.I) class CentralRosemontConnector(BaseConnector): source_id = "central_rosemont" request_delay = 0.6 max_images = 20 def fetch(self) -> list[Listing]: html = self.get(LIST_URL).text soup = BeautifulSoup(html, "html.parser") # galerie du projet (page disponibilités, sans logos/miniatures) images: list[str] = [] for u in _IMG_RE.findall(html): if _SKIP_IMG.search(u): continue if u not in images: images.append(u) images = images[: self.max_images] desc = "" meta = soup.find("meta", attrs={"name": "description"}) if meta and meta.get("content"): desc = meta["content"].strip()[:600] listings: list[Listing] = [] for bloc in soup.select("div.bloc_unite_dispo[data-unite]"): try: lst = self._bloc_listing(bloc, desc, images) if lst is not None: listings.append(lst) except Exception: continue return listings def _bloc_listing(self, bloc, desc: str, images: list[str]) -> Listing | None: unite = (bloc.get("data-unite") or "").strip() num_el = bloc.select_one(".numero_unite .value") number = (num_el.get_text(" ", strip=True) if num_el else unite) if not (unite or number): return None # étage (« Étage 1 ») floor = "" fl_el = bloc.select_one(".etage_unite") if fl_el: fm = _FLOOR_RE.search(fl_el.get_text(" ", strip=True)) if fm: floor = fm.group(1) # superficie (« 800 pi² ») sqft = None sq_el = bloc.select_one(".superficie_unite p") if sq_el: sm = _SQFT_RE.search(sq_el.get_text(" ", strip=True)) if sm: try: sqft = float(re.sub(r"\s", "", sm.group(1))) except ValueError: sqft = None # chambres fermées -> type d'unité bedrooms = None unit_type = "" ch_el = bloc.select_one(".chambres_unite p") if ch_el: ct = re.sub(r"[^\d]", "", ch_el.get_text(" ", strip=True)) if ct: bedrooms = float(ct) unit_type = _BED_TYPE.get(int(bedrooms), "") # prix (« 2465$/mois », parfois vide « $/mois ») price = None price_label = "" px_el = bloc.select_one(".prix_unite .prix") if px_el: raw = px_el.get_text(" ", strip=True) pm = _PRICE_RE.search(raw) if pm: digits = re.sub(r"[^\d]", "", pm.group(1)) if digits: price = float(digits) price_label = f"{int(price)} $/mois" if price is not None and not (100 <= price <= 20000): price = None # date de disponibilité (« juin 2026 » ou vide = maintenant) availability = "" dt_el = bloc.select_one(".date_unite .date") if dt_el: availability = dt_el.get_text(" ", strip=True) details: dict = {} if floor: details["floor"] = floor plan = bloc.select_one(".actions_unite a[href$='.pdf']") if plan and plan.get("href"): details["floor_plan_pdf"] = plan["href"] return Listing( source=self.source_id, external_id=unite or number, url=LIST_URL, title=f"Central Rosemont — Unité {number}" + (f" ({unit_type})" if unit_type else ""), address=ADDRESS, sector="Rosemont", city="Montréal", unit_type=unit_type, bedrooms=bedrooms, price=price, price_label=price_label, availability=availability or "Disponible", area_sqft=sqft, description=desc, details=details, images=list(images), )