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/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