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/lsr.py : connecteur LSR GesDev (lsrgesdev.ca — anciennement5# lsrgesdev.com, redirige) — développeur-gestionnaire de Saint-Lambert.6# Le site corporatif ne publie aucun prix ; l'inventaire locatif vit sur7# lenovia.ca (WordPress) avec DEUX backends différents :8# - NOVIA centre-ville – Longueuil (300, place Charles-Le Moyne) : widget9# Livya (app.livya.com, client « lsr-gesdev », projet « novia ») — flux10# RSC Next.js parsé par la base commune LivyaConnector (_livya.py) ;11# - NOVIA de la Gare – Candiac (125, rue des Marronniers) : widget Planpoint12# (app.planpoint.io/125-marronniers/125-marronniers) — API JSON publique13# via le helper _planpoint.py.14# Granularité = UNITÉ pour les deux projets (prix, pi², chambres, étage).15# Roomî (espaceroomi.ca, résidence étudiante Trois-Rivières) exclu.16# -----------------------------------------------------------------------------17from __future__ import annotations1819import html as _html20import re2122from ..schema import Listing23from ._livya import LivyaConnector24from ._planpoint import planpoint_projects, unit_listing2526CANDIAC_PAGE = "https://lenovia.ca/novia-de-la-gare-candiac/plans/"27CANDIAC_NAMESPACE = "125-marronniers"28CANDIAC_HOST = "125-marronniers"2930# prix planchers par typologie publiés sur lenovia.ca (Livya n'expose aucun31# prix pour Longueuil) : « 3 ½ (de 541 pi2 à 769 pi2) à partir de 1 950$ /mois »32LONGUEUIL_APTS = "https://lenovia.ca/novia-centre-ville-longueuil/appartements/"33TYPE_TOKEN_RE = re.compile(r"Studio|\d\s*½|Maison de ville", re.I)34FROM_PRICE_RE = re.compile(35 r"à\s+partir\s+de\s+([\d\s ]{3,9})\$\s*/?\s*mois", re.I)36SCRIPT_RE = re.compile(r"<(script|style)[\s\S]*?</\1>", re.I)37TAG_RE = re.compile(r"<[^>]+>")383940class LSRConnector(LivyaConnector):41 source_id = "lsr"42 request_delay = 0.84344 # -- NOVIA centre-ville (Longueuil) via Livya --------------------------------45 client = "lsr-gesdev"46 brand = "NOVIA centre-ville"47 site_url = "https://www.lsrgesdev.ca"48 default_city = "Longueuil"49 default_projects = ["novia"]5051 def fetch(self) -> list[Listing]:52 listings = super().fetch() # Longueuil (Livya)53 floors = self._longueuil_floor_prices()54 for lst in listings:55 lst.city = re.sub(r"^Ville de\s+", "", lst.city or "")56 # Livya ne publie aucun loyer pour Longueuil : on rattache le prix57 # plancher « à partir de » publié par lenovia.ca pour la typologie58 if lst.price is None and not lst.price_label:59 base = floors.get(lst.unit_type)60 if base:61 lst.price_label = f"à partir de {int(base)} $ /mois"62 listings.extend(self._candiac()) # Candiac (Planpoint)63 return listings6465 def _longueuil_floor_prices(self) -> dict[str, float]:66 """{'3½': 1950.0, …} depuis la page publique « Appartements »."""67 try:68 html = self.get(LONGUEUIL_APTS).text69 except Exception:70 return {}71 flat = re.sub(r"[\s ]+", " ",72 TAG_RE.sub(" | ", _html.unescape(SCRIPT_RE.sub(" ", html))))73 # segment « typologie -> texte jusqu'à la typologie suivante » pour ne74 # jamais rattacher le prix d'un 3½ au Studio « Complet » qui le précède75 tokens = list(TYPE_TOKEN_RE.finditer(flat))76 prices: dict[str, float] = {}77 for i, tok in enumerate(tokens):78 end = tokens[i + 1].start() if i + 1 < len(tokens) else len(flat)79 seg = flat[tok.end():min(end, tok.end() + 200)]80 pm = FROM_PRICE_RE.search(seg)81 if not pm:82 continue83 label = re.sub(r"\s+", "", tok.group(0))84 key = "Studio" if label.lower() == "studio" else label85 if key.lower() == "maisondeville":86 continue # typologie non « n½ » : ignorée87 try:88 val = float(re.sub(r"[\s ]", "", pm.group(1)))89 except ValueError:90 continue91 if 500 <= val <= 8000:92 prices.setdefault(key, val)93 return prices9495 # -- NOVIA de la Gare (Candiac) via Planpoint --------------------------------96 def _candiac(self) -> list[Listing]:97 try:98 projects = planpoint_projects(self, "projects",99 CANDIAC_NAMESPACE, CANDIAC_HOST)100 except Exception:101 return []102 listings: list[Listing] = []103 for project in projects:104 for floor in project.get("floors") or []:105 for unit in floor.get("units") or []:106 lst = unit_listing(self.source_id, project, floor, unit,107 page_url=CANDIAC_PAGE,108 default_city="Candiac")109 if lst is not None:110 listings.append(lst)111 return listings112