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%
3.9 KB · 87 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/omhm.py : OMHM — Office municipal d'habitation de Montréal.5#   Le HLM classique fonctionne par liste d'attente (non agrégeable) ; SEULE6#   la page « logements disponibles » (résidences abordables ENHARMONIE pour7#   aînés autonomes 60 ans +) liste de vraies unités. Site Drupal SSR, aucun8#   anti-bot. Chaque unité = un <article class="node--type-logement-disponible">9#   avec adresse civique complète et grille de prix par type (« 3 ½ entre10#   1 357 $ et 1 497 $ »). Volume faible (~5) mais différenciateur social11#   (filtre « logement abordable » de Lou-Ka).12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from bs4 import BeautifulSoup1819from ..schema import Listing, normalize_unit_type20from .base import BaseConnector2122URL = "https://www.omhm.qc.ca/fr/soumettre-une-demande/logements-disponibles"2324_POSTAL_RE = re.compile(r"[A-Z]\d[A-Z]\s?\d[A-Z]\d")25# « 3 ½ entre 1 357 $ et 1 497 $ » ou « studio 730 $ à 925 $ » ou « 4 ½ à 2 167 $ »26_UNIT_PRICE_RE = re.compile(27    r"(studio|\d\s*½|\d\s*1/2)\s*(?:entre|à|de|:)?\s*"28    r"([\d\s]+)\s*\$(?:\s*(?:et|à|-)\s*([\d\s]+)\s*\$)?", re.I)293031class OmhmConnector(BaseConnector):32    source_id = "omhm"33    request_delay = 0.83435    def fetch(self) -> list[Listing]:36        soup = BeautifulSoup(self.get(URL).text, "html.parser")37        out: list[Listing] = []38        for art in soup.select("article.node--type-logement-disponible"):39            title_el = art.find(re.compile("^h[2-4]$"))40            name = title_el.get_text(" ", strip=True) if title_el else ""41            full = re.sub(r"\s+", " ", art.get_text(" | ", strip=True))4243            # adresse : « 12100, rue Rodolphe-Forget | Montréal | H1E 7S2 »44            m = re.search(r"(\d+[^|]+?)\s*\|\s*([A-ZÉÈÀ][^|]+?)\s*\|\s*"45                          r"(" + _POSTAL_RE.pattern + r")", full)46            address = city = postal = ""47            if m:48                address = m.group(1).strip(" ,")49                city = m.group(2).strip()50                postal = m.group(3).strip()5152            slug = re.sub(r"[^a-z0-9]+", "-",53                          (name or address).lower()).strip("-") or "omhm"54            # une annonce par type d'unité offert dans la résidence55            seen = set()56            for um in _UNIT_PRICE_RE.finditer(full):57                unit_type = normalize_unit_type(um.group(1))58                if unit_type in seen:59                    continue60                seen.add(unit_type)61                try:62                    price = float(um.group(2).replace(" ", ""))63                except ValueError:64                    continue65                pmax = um.group(3)66                label = (f"Entre {price:,.0f} $ et {float(pmax.replace(' ','')):,.0f} $"67                         .replace(",", " ") if pmax68                         else f"À partir de {price:,.0f} $".replace(",", " "))69                out.append(Listing(70                    source=self.source_id,71                    external_id=f"{slug}-{unit_type}",72                    url=URL,73                    title=f"{name} — {unit_type}" if name else unit_type,74                    address=address,75                    city=city or "Montréal",76                    unit_type=unit_type,77                    price=price,78                    price_label=label,79                    availability="Disponible",80                    details={"Code postal": postal,81                             "Logement abordable": "oui",82                             "Résidence pour aînés": "oui"} if postal else83                            {"Logement abordable": "oui",84                             "Résidence pour aînés": "oui"},85                ))86        return out87