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/station_horizon.py : connecteur Station Horizon5# (stationhorizon.com) — condos locatifs neufs au 550-650-700, boulevard6# de la Gare, Mont-Saint-Hilaire (Village de la Gare), 2 phases. WordPress7# rendu serveur : les pages « Plans des unités » (phase 1 et 2) exposent8# les plans par typologie dans les noms de fichiers webp9# (« …Phase-1-3.1-2_TypeC.webp » = 3½ type C). Les prix ne sont pas dans10# les pages de plans : ils viennent de l'accueil (« À partir de : 3 ½11# 1 775$ / mois | 4 ½ 2 600$ | 5 ½ 2 625$ », tout inclus). Une annonce par12# phase × typologie (pas d'inventaire unité par unité public), avec les13# plans de la typologie en images.14# -----------------------------------------------------------------------------15from __future__ import annotations1617import re1819from bs4 import BeautifulSoup2021from ..schema import Listing22from .base import BaseConnector2324BASE = "https://stationhorizon.com"25HOME_URL = f"{BASE}/"26PHASES = {27 1: f"{BASE}/plans-des-unites-phase-1/",28 2: f"{BASE}/plans-des-unites-phase-2/",29}3031ADDRESS = "550-650-700, boulevard de la Gare, Mont-Saint-Hilaire"32CITY = "Mont-Saint-Hilaire"33PHONE = "514-900-3339"3435# prix plancher par typologie sur l'accueil : « 3 ½ 1 775$ / mois »36PRICE_RE = re.compile(r"(\d)\s*½\s*([\d\s ]{3,8})\$\s*/\s*mois")37# plan d'unité : « …Phase-1-3.1-2_TypeC.webp », « …Phase-2-4.1-2_Type-15.webp »38PLAN_RE = re.compile(39 r"https://(?:www\.)?stationhorizon\.com/wp-content/uploads/"40 r"[^\"\s\\']*Phase-(\d)-(\d)\.1-2_Type-?([\w]+?)(?:-scaled)?"41 r"\.(?:webp|png|jpe?g)", re.I)42# inclusions : segment « Ce qui est inclus … Écrivez-nous » de l'accueil43INCLUS_RE = re.compile(r"Ce qui est inclus(.{10,500}?)(?:Écrivez-nous|$)")444546class StationHorizonConnector(BaseConnector):47 source_id = "station_horizon"48 request_delay = 0.64950 def fetch(self) -> list[Listing]:51 listings: list[Listing] = []5253 # accueil : prix par typologie + inclusions54 prices: dict[str, tuple[float, str]] = {}55 amenities: list[str] = []56 try:57 home = self.get(HOME_URL).text58 text = re.sub(r"\s+", " ",59 BeautifulSoup(home, "html.parser")60 .get_text(" ", strip=True))61 for m in PRICE_RE.finditer(text):62 try:63 val = float(re.sub(r"[^\d]", "", m.group(2)))64 except ValueError:65 continue66 label = re.sub(r"[ ]", " ", m.group(2)).strip()67 prices[f"{m.group(1)}½"] = \68 (val, f"À partir de {label}$ / mois")69 m = INCLUS_RE.search(text)70 if m:71 amenities = [a.strip() for a in re.split(72 r"(?<=[a-zé)])\s+(?=[A-ZÉ5])", m.group(1).strip())73 if len(a.strip()) > 2][:10]74 except Exception:75 pass7677 # pages de plans : typologies offertes par phase78 for phase, url in PHASES.items():79 try:80 html = self.get(url).text81 except Exception:82 continue83 plans: dict[str, list[str]] = {}84 for m in PLAN_RE.finditer(html):85 if int(m.group(1)) != phase:86 continue87 unit_type = f"{m.group(2)}½"88 if m.group(0) not in plans.setdefault(unit_type, []):89 plans[unit_type].append(m.group(0))90 for unit_type, imgs in sorted(plans.items()):91 price, price_label = prices.get(unit_type, (None, ""))92 listings.append(Listing(93 source=self.source_id,94 external_id=f"phase{phase}-"95 f"{unit_type.replace('½', '.5')}",96 url=url,97 title=f"{unit_type} — Station Horizon, phase {phase}",98 address=ADDRESS,99 city=CITY,100 unit_type=unit_type,101 price=price,102 price_label=price_label,103 amenities=list(amenities),104 details={"contact": {"phone": PHONE},105 "nb_plans": len(imgs)},106 images=imgs[:12],107 ))108 return listings109