Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/central_rosemont.py : connecteur Central Rosemont5# (centralrosemont.com) — condos locatifs au 1295, rue des Carrières,6# Rosemont (Montréal), au bord de la piste des Carrières. WordPress rendu7# serveur : la page /appartements-disponibles/ affiche un bloc8# div.bloc_unite_dispo[data-unite] par unité disponible — numéro, étage,9# superficie (pi²), nombre de chambres, prix « 2465$/mois » (parfois vide),10# date de disponibilité (« juin 2026 ») et plan PDF. Une annonce par unité,11# id stable = numéro d'unité (data-unite).12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from bs4 import BeautifulSoup1819from ..schema import Listing20from .base import BaseConnector2122BASE = "https://www.centralrosemont.com"23LIST_URL = f"{BASE}/appartements-disponibles/"24ADDRESS = "1295, rue des Carrières, Montréal, QC H2S 0E1"2526# Nombre de chambres -> type d'unité (convention QC : n½ = n-2 chambres)27_BED_TYPE = {0: "Studio", 1: "3½", 2: "4½", 3: "5½", 4: "6½"}2829_PRICE_RE = re.compile(r"([\d\s,]{3,7})\s*\$")30_SQFT_RE = re.compile(r"([\d\s]{2,6})\s*pi", re.I)31_FLOOR_RE = re.compile(r"Étage\s*(\d+)", re.I)32_IMG_RE = re.compile(33 r'https://(?:www\.)?centralrosemont\.com/wp-content/uploads/'34 r'[^"\'\s\\)]+?\.(?:jpg|jpeg|png|webp)', re.I)35_SKIP_IMG = re.compile(r"logo|icon|favicon|-\d{2,4}x\d{2,4}\.", re.I)363738class CentralRosemontConnector(BaseConnector):39 source_id = "central_rosemont"40 request_delay = 0.641 max_images = 204243 def fetch(self) -> list[Listing]:44 html = self.get(LIST_URL).text45 soup = BeautifulSoup(html, "html.parser")4647 # galerie du projet (page disponibilités, sans logos/miniatures)48 images: list[str] = []49 for u in _IMG_RE.findall(html):50 if _SKIP_IMG.search(u):51 continue52 if u not in images:53 images.append(u)54 images = images[: self.max_images]5556 desc = ""57 meta = soup.find("meta", attrs={"name": "description"})58 if meta and meta.get("content"):59 desc = meta["content"].strip()[:600]6061 listings: list[Listing] = []62 for bloc in soup.select("div.bloc_unite_dispo[data-unite]"):63 try:64 lst = self._bloc_listing(bloc, desc, images)65 if lst is not None:66 listings.append(lst)67 except Exception:68 continue69 return listings7071 def _bloc_listing(self, bloc, desc: str,72 images: list[str]) -> Listing | None:73 unite = (bloc.get("data-unite") or "").strip()74 num_el = bloc.select_one(".numero_unite .value")75 number = (num_el.get_text(" ", strip=True) if num_el else unite)76 if not (unite or number):77 return None7879 # étage (« Étage 1 »)80 floor = ""81 fl_el = bloc.select_one(".etage_unite")82 if fl_el:83 fm = _FLOOR_RE.search(fl_el.get_text(" ", strip=True))84 if fm:85 floor = fm.group(1)8687 # superficie (« 800 pi² »)88 sqft = None89 sq_el = bloc.select_one(".superficie_unite p")90 if sq_el:91 sm = _SQFT_RE.search(sq_el.get_text(" ", strip=True))92 if sm:93 try:94 sqft = float(re.sub(r"\s", "", sm.group(1)))95 except ValueError:96 sqft = None9798 # chambres fermées -> type d'unité99 bedrooms = None100 unit_type = ""101 ch_el = bloc.select_one(".chambres_unite p")102 if ch_el:103 ct = re.sub(r"[^\d]", "", ch_el.get_text(" ", strip=True))104 if ct:105 bedrooms = float(ct)106 unit_type = _BED_TYPE.get(int(bedrooms), "")107108 # prix (« 2465$/mois », parfois vide « $/mois »)109 price = None110 price_label = ""111 px_el = bloc.select_one(".prix_unite .prix")112 if px_el:113 raw = px_el.get_text(" ", strip=True)114 pm = _PRICE_RE.search(raw)115 if pm:116 digits = re.sub(r"[^\d]", "", pm.group(1))117 if digits:118 price = float(digits)119 price_label = f"{int(price)} $/mois"120 if price is not None and not (100 <= price <= 20000):121 price = None122123 # date de disponibilité (« juin 2026 » ou vide = maintenant)124 availability = ""125 dt_el = bloc.select_one(".date_unite .date")126 if dt_el:127 availability = dt_el.get_text(" ", strip=True)128129 details: dict = {}130 if floor:131 details["floor"] = floor132 plan = bloc.select_one(".actions_unite a[href$='.pdf']")133 if plan and plan.get("href"):134 details["floor_plan_pdf"] = plan["href"]135136 return Listing(137 source=self.source_id,138 external_id=unite or number,139 url=LIST_URL,140 title=f"Central Rosemont — Unité {number}"141 + (f" ({unit_type})" if unit_type else ""),142 address=ADDRESS,143 sector="Rosemont",144 city="Montréal",145 unit_type=unit_type,146 bedrooms=bedrooms,147 price=price,148 price_label=price_label,149 availability=availability or "Disponible",150 area_sqft=sqft,151 description=desc,152 details=details,153 images=list(images),154 )155