# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/station_horizon.py : connecteur Station Horizon # (stationhorizon.com) — condos locatifs neufs au 550-650-700, boulevard # de la Gare, Mont-Saint-Hilaire (Village de la Gare), 2 phases. WordPress # rendu serveur : les pages « Plans des unités » (phase 1 et 2) exposent # les plans par typologie dans les noms de fichiers webp # (« …Phase-1-3.1-2_TypeC.webp » = 3½ type C). Les prix ne sont pas dans # les pages de plans : ils viennent de l'accueil (« À partir de : 3 ½ # 1 775$ / mois | 4 ½ 2 600$ | 5 ½ 2 625$ », tout inclus). Une annonce par # phase × typologie (pas d'inventaire unité par unité public), avec les # plans de la typologie en images. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing from .base import BaseConnector BASE = "https://stationhorizon.com" HOME_URL = f"{BASE}/" PHASES = { 1: f"{BASE}/plans-des-unites-phase-1/", 2: f"{BASE}/plans-des-unites-phase-2/", } ADDRESS = "550-650-700, boulevard de la Gare, Mont-Saint-Hilaire" CITY = "Mont-Saint-Hilaire" PHONE = "514-900-3339" # prix plancher par typologie sur l'accueil : « 3 ½ 1 775$ / mois » PRICE_RE = re.compile(r"(\d)\s*½\s*([\d\s ]{3,8})\$\s*/\s*mois") # plan d'unité : « …Phase-1-3.1-2_TypeC.webp », « …Phase-2-4.1-2_Type-15.webp » PLAN_RE = re.compile( r"https://(?:www\.)?stationhorizon\.com/wp-content/uploads/" r"[^\"\s\\']*Phase-(\d)-(\d)\.1-2_Type-?([\w]+?)(?:-scaled)?" r"\.(?:webp|png|jpe?g)", re.I) # inclusions : segment « Ce qui est inclus … Écrivez-nous » de l'accueil INCLUS_RE = re.compile(r"Ce qui est inclus(.{10,500}?)(?:Écrivez-nous|$)") class StationHorizonConnector(BaseConnector): source_id = "station_horizon" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: list[Listing] = [] # accueil : prix par typologie + inclusions prices: dict[str, tuple[float, str]] = {} amenities: list[str] = [] try: home = self.get(HOME_URL).text text = re.sub(r"\s+", " ", BeautifulSoup(home, "html.parser") .get_text(" ", strip=True)) for m in PRICE_RE.finditer(text): try: val = float(re.sub(r"[^\d]", "", m.group(2))) except ValueError: continue label = re.sub(r"[ ]", " ", m.group(2)).strip() prices[f"{m.group(1)}½"] = \ (val, f"À partir de {label}$ / mois") m = INCLUS_RE.search(text) if m: amenities = [a.strip() for a in re.split( r"(?<=[a-zé)])\s+(?=[A-ZÉ5])", m.group(1).strip()) if len(a.strip()) > 2][:10] except Exception: pass # pages de plans : typologies offertes par phase for phase, url in PHASES.items(): try: html = self.get(url).text except Exception: continue plans: dict[str, list[str]] = {} for m in PLAN_RE.finditer(html): if int(m.group(1)) != phase: continue unit_type = f"{m.group(2)}½" if m.group(0) not in plans.setdefault(unit_type, []): plans[unit_type].append(m.group(0)) for unit_type, imgs in sorted(plans.items()): price, price_label = prices.get(unit_type, (None, "")) listings.append(Listing( source=self.source_id, external_id=f"phase{phase}-" f"{unit_type.replace('½', '.5')}", url=url, title=f"{unit_type} — Station Horizon, phase {phase}", address=ADDRESS, city=CITY, unit_type=unit_type, price=price, price_label=price_label, amenities=list(amenities), details={"contact": {"phone": PHONE}, "nb_plans": len(imgs)}, images=imgs[:12], )) return listings