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 · 113 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/bati5.py : connecteur Bâti5 — Boisés Dion (www.bati5.ca)5#   Condos locatifs neufs (sixplex) au 7150, rue Jean-Claude-Langlois,6#   Boisbriand (quartier Boisés Dion). Zoho Sites rendu SERVEUR : la page7#   d'accueil contient des cartes typologie « n½ [+ bureau] (niveau X) …8#   à partir de NNNN $ [PROMO* $] … 1 unité disponible pour … » ou9#   « Tous loués… ». On ignore les segments « masqué: … » (contenu caché par10#   l'éditeur) et on NE GARDE que les typologies avec « n unité(s)11#   disponible(s) ». Granularité : TYPOLOGIE (prix « à partir de »).12# -----------------------------------------------------------------------------13from __future__ import annotations1415import html as _html16import re1718from ..schema import Listing1920from .base import BaseConnector2122BASE = "https://www.bati5.ca"2324ADDRESS = "7150, rue Jean-Claude-Langlois, Boisbriand"25CITY = "Boisbriand"2627# « 5½ + bureau (niveau RDC) 3 CHAMBRES + BUREAU + 2 SALLES DE BAIN28#   à partir de 2560 $ 2450* $ 1 unité disponible pour 1 er mai 2026 »29CARD_RE = re.compile(30    r"(\d)\s*½\s*(\+\s*bureau)?\s*\(niveau\s*([^)]*)\)\s*"   # typo, bureau, niv.31    r"(.{0,200}?)à\s*partir\s*de\s*(\d[\d\s]*)\s*\$"          # desc, prix32    r"(?:\s*(\d[\d\s]*)\*\s*\$)?"                             # prix promo*33    r"(.{0,260}?)"                                            # queue (dispo)34    r"(?=\d\s*½\s*(?:\+\s*bureau\s*)?\(niveau|Demander une visite|$)", re.S)35MASQUE_RE = re.compile(r"masqué\s*:\s*.*?(?=Tous lou|Demander|$)", re.S | re.I)36AVAIL_RE = re.compile(37    r"\d+\s*unités?\s*disponibles?[^.]{0,60}?(?:\d{4}|$)", re.I)38IMG_RE = re.compile(39    r"https://www\.bati5\.ca/files/[^\"'\s\\]+\.(?:jpe?g|png|webp)", re.I)40SKIP_IMG_RE = re.compile(r"logo|favicon|icon|bati5-01|_\d{2,4}x\d{2,4}\.", re.I)414243class Bati5Connector(BaseConnector):44    source_id = "bati5"45    request_delay = 0.646    max_images = 104748    def fetch(self) -> list[Listing]:49        raw = self.get(f"{BASE}/").text50        images = [u for u in dict.fromkeys(IMG_RE.findall(raw))51                  if not SKIP_IMG_RE.search(u)][: self.max_images]5253        text = _html.unescape(raw)54        text = re.sub(r"<script.*?</script>", " ", text, flags=re.S)55        text = re.sub(r"<style.*?</style>", " ", text, flags=re.S)56        text = re.sub(r"<[^>]+>", " ", text)57        text = re.sub(r"\s+", " ", text)5859        listings: list[Listing] = []60        seen: set[str] = set()61        for n, bureau, niveau, desc, price_raw, promo_raw, tail \62                in CARD_RE.findall(text):63            tail = MASQUE_RE.sub(" ", tail)64            m = AVAIL_RE.search(tail)65            if m is None:                        # « Tous loués… » -> on saute66                continue67            availability = re.sub(r"\b(\d+)\s*er\b", r"\1er",68                                  re.sub(r"\s+", " ", m.group(0))).strip()6970            unit_type = f"{n}½"71            variant = "bureau" if bureau else niveau.strip().lower() or "std"72            ext_id = f"bati5-{n}.5-{re.sub(r'[^a-z0-9]+', '-', variant)}"73            if ext_id in seen:74                continue75            seen.add(ext_id)7677            price = None78            amount = (promo_raw or price_raw).replace(" ", "").replace(" ", "")79            try:80                v = float(amount)81                if 300 <= v <= 20000:82                    price = v83            except ValueError:84                pass85            label_bits = [f"à partir de {price_raw.strip()}$"]86            if promo_raw:87                label_bits.append(f"{promo_raw.strip()}$ en promotion")8889            beds = None90            m = re.search(r"(\d)\s*chambres?", desc, re.I)91            if m:92                beds = float(m.group(1))9394            title = f"Bâti5 — Boisés Dion — {unit_type}" \95                    + (" + bureau" if bureau else "") \96                    + (f" (niveau {niveau.strip()})" if niveau.strip() else "")97            listings.append(Listing(98                source=self.source_id,99                external_id=ext_id,100                url=f"{BASE}/",101                title=title,102                address=ADDRESS,103                city=CITY,104                unit_type=unit_type,105                bedrooms=beds,106                price=price,107                price_label=" — ".join(label_bits),108                availability=availability,109                description=re.sub(r"\s+", " ", desc).strip().capitalize(),110                images=list(images),111            ))112        return listings113