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/signature_bois_franc.py : Signature Bois-Franc — condos locatifs5# (location.signatureboisfranc.com) au 2020, rue Lucien-Thimens, quartier6# Bois-Franc, Saint-Laurent (Montréal).7# Techno : WordPress + FacetWP, mais la liste /unites-disponibles/ est8# RENDUE CÔTÉ SERVEUR (toutes les cartes .unit-row dans le HTML initial,9# pas de pagination) — simple parse regex des champs par carte.10# Granularité : UNITÉ (numéro, pièces, étage, pi², « à partir de » $,11# statut). On écarte « Unité louée » et « Réservée ».12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from .base import BaseConnector18from ..schema import Listing1920LIST_URL = "https://location.signatureboisfranc.com/unites-disponibles/"21ADDRESS = "2020, rue Lucien-Thimens"22CITY = "Montréal"23SECTOR = "Saint-Laurent"2425_ROW_SPLIT_RE = re.compile(r'<div class="unit-row">')26_FIELD_RE = {27 "number": re.compile(r'unit-number__text">\s*#?([^<]+?)\s*<'),28 "rooms": re.compile(r'unit-room__text">\s*([^<]+?)\s*<'),29 "floor": re.compile(r'unit-floor__text">\s*([^<]+?)\s*<'),30 "area": re.compile(r'unit-area__text">\s*([^<]+?)\s*<'),31 "price": re.compile(r'unit-price__text">\s*([^<]+?)\s*<'),32 "status": re.compile(r'unit-availability__text">\s*([^<]*?)\s*<'),33}34_PLAN_RE = re.compile(r'unit-plan[^>]*>.*?href="([^"]+\.pdf)"', re.S)35_AREA_NUM_RE = re.compile(r'([\d\s ,.]+)\s*pi')36_PRICE_NUM_RE = re.compile(r'([\d\s ]{3,})\s*\$')37# statuts qui excluent l'unité de l'inventaire38_GONE_RE = re.compile(r'lou[ée]e?|r[ée]serv[ée]e?', re.I)394041class SignatureBoisFrancConnector(BaseConnector):42 """Signature Bois-Franc (Saint-Laurent, Montréal) — unités disponibles."""4344 source_id = "signature_bois_franc"45 request_delay = 0.846 use_detail_cache = False # une seule page, tout est dans la liste4748 def fetch(self) -> list[Listing]:49 page = re.sub(r"\s+", " ", self.get(LIST_URL).text)50 out: list[Listing] = []51 seen: set[str] = set()52 for block in _ROW_SPLIT_RE.split(page)[1:]:53 f = {k: (rx.search(block).group(1).strip()54 if rx.search(block) else "")55 for k, rx in _FIELD_RE.items()}56 num = f["number"].lstrip("#").strip()57 if not num:58 continue59 status = f["status"]60 if _GONE_RE.search(status):61 continue # louée / réservée6263 ext_id = f"unite-{num}" # un seul immeuble : nº stable64 if ext_id in seen:65 continue66 seen.add(ext_id)6768 area = None69 m = _AREA_NUM_RE.search(f["area"])70 if m:71 try:72 area = float(re.sub(r"[\s ,]", "", m.group(1)))73 except ValueError:74 pass75 price = None76 m = _PRICE_NUM_RE.search(f["price"].replace("$", " $"))77 if m:78 try:79 price = float(re.sub(r"[\s ]", "", m.group(1)))80 except ValueError:81 pass8283 details: dict = {}84 if f["floor"]:85 details["floor"] = f["floor"]86 plan = _PLAN_RE.search(block)87 if plan:88 details["floor_plan"] = plan.group(1)8990 out.append(Listing(91 source=self.source_id,92 external_id=ext_id,93 url=LIST_URL, # pas de page par unité94 title=f"Signature Bois-Franc — Unité {num}",95 address=ADDRESS,96 sector=SECTOR,97 city=CITY,98 unit_type=f["rooms"],99 price=price,100 price_label=(f"À partir de {price:.0f} $ /mois"101 if price else ""),102 availability=status or "Disponible",103 area_sqft=area,104 details=details,105 ))106 return out107