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/cinquante_ans_plus.py : connecteur Logements 50 ans et plus5# (logements50ans.ca — Gestion T. Francoeur). Trois immeubles locatifs pour6# clientèle autonome de 50 ans et plus (non RPA) : Le Chambéry et Boisé du7# Parc Équestre à Blainville, Le Bourg-Joli à Sainte-Adèle. Site vitrine8# MotoCMS rendu serveur : l'accueil publie « À PARTIR DE X$ » par immeuble,9# les pages projet donnent l'adresse, les commodités (puces « • ») et les10# photos. Granularité IMMEUBLE (aucune unité/typologie détaillée en ligne).11# -----------------------------------------------------------------------------12from __future__ import annotations1314import re1516from ..schema import Listing17from .base import BaseConnector1819BASE = "https://logements50ans.ca"2021# nom affiché sur l'accueil (repère du prix) -> (slug de page, nom, ville)22BUILDINGS = [23 ("CHAMBÉRY", "chambery", "Le Chambéry", "Blainville"),24 ("BOURG-JOLI", "bourg-joli", "Le Bourg-Joli", "Sainte-Adèle"),25 ("PARC ÉQUESTRE", "boise-du-parc-equestre",26 "Boisé du Parc Équestre", "Blainville"),27]2829PRICE_RE_TPL = r"%s.{0,600}?À PARTIR DE\s*([\d\s ,]+)\s*\$"30ADDR_RE = re.compile(31 r"(\d{1,5}[-\d]*,?\s+(?:boul\.?|rue|avenue|av\.|chemin|mont[ée]e|rang)"32 r"[^|<>]{0,60}?),\s*(?:Ville de\s*)?(Blainville|Ste-Ad[èe]le|"33 r"Sainte-Ad[èe]le)", re.I)34BULLET_RE = re.compile(r"•\s*([^|<>•]{3,60})")35IMG_RE = re.compile(36 r'(?:src|href)="(/mt-content/uploads/[^"]+\.(?:jpg|jpeg|png|webp))"', re.I)373839def _flat(html: str) -> str:40 txt = re.sub(r"<script.*?</script>", "", html, flags=re.S)41 txt = re.sub(r"<style.*?</style>", "", txt, flags=re.S)42 txt = re.sub(r"<[^>]+>", "|", txt).replace(" ", " ")43 return re.sub(r"[ \t\n]+", " ", re.sub(r"\|+", "|", txt))444546class CinquanteAnsPlusConnector(BaseConnector):47 source_id = "cinquante_ans_plus"48 request_delay = 0.64950 def fetch(self) -> list[Listing]:51 # échec HONNÊTE : une erreur réseau sur l'accueil doit lever (ok=0 dans52 # sync_log) et non retourner [] — un « 0 trouvé ok » dépublie les 353 # immeubles (missed=3, vu le 2026-09-06 16:04)54 listings: list[Listing] = []55 home = _flat(self.get(BASE + "/").text)56 if not any(marker in home for marker, *_ in BUILDINGS):57 raise RuntimeError(58 "logements50ans.ca : accueil sans aucun immeuble connu "59 "(rendu partiel ou site restructuré)")6061 for marker, slug, name, city in BUILDINGS:62 m = re.search(PRICE_RE_TPL % re.escape(marker), home, re.S)63 price_label = ""64 if m:65 price_label = ("À partir de "66 + re.sub(r"[\s ]", "", m.group(1)) + " $/mois")6768 page_url = f"{BASE}/{slug}/"69 address, amenities, images, description = "", [], [], ""70 try:71 page = self.get(page_url).text72 except Exception:73 page = ""74 if page:75 flat = _flat(page)76 am = ADDR_RE.search(flat)77 if am:78 address = am.group(1).strip(" ,")79 for b in BULLET_RE.findall(flat):80 b = b.strip(" .")81 if b and b not in amenities:82 amenities.append(b)83 images = [BASE + u for u in dict.fromkeys(IMG_RE.findall(page))84 if not re.search(r"logo|icon|-small\.", u, re.I)][:20]85 # premier paragraphe descriptif substantiel de la page projet86 for seg in flat.split("|"):87 seg = seg.strip()88 if len(seg) > 120 and "cookie" not in seg.lower():89 description = seg90 break9192 if not price_label and not address:93 continue # immeuble retiré du site94 listings.append(Listing(95 source=self.source_id,96 external_id=slug,97 url=page_url,98 title=f"{name} — logements 50 ans et plus, {city}",99 address=address,100 city=city,101 price_label=price_label,102 description=description[:1500],103 amenities=amenities[:25],104 details={"clientele": "50 ans et plus"},105 images=images,106 ))107 return listings108