SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
4.5 KB · 109 lines python
Raw Blame History
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