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.6 KB · 108 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/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("&nbsp;", " ")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