# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/immeubles_db.py : connecteur Les Immeubles D.B. Busque-Massé # (immeublesdb.com) — 12 immeubles / ~400 apparts à Sherbrooke, 4 secteurs. # Site custom PHP (« pm_editor », derrière Cloudflare — le UA de base.py # passe) : 4 pages statiques /logements-a-louer/secteur-{nord,est, # centre-ville,ouest}/. Chaque logement offert = une section .container # avec une cellule galerie (photos /modules/upload/…) et une cellule texte # (h3 = adresse civique, liste
  • = type, prix « … $ par mois », # inclusions). Aucun identifiant publié : external_id = empreinte stable # (secteur + adresse + ligne de type). Les logements loués sont simplement # retirés de la page (aucun marqueur « complet » à filtrer). # ----------------------------------------------------------------------------- from __future__ import annotations import hashlib import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type, parse_price from .base import BaseConnector BASE = "https://immeublesdb.com" SECTORS = { "secteur-nord": "Nord", "secteur-est": "Est", "secteur-centre-ville": "Centre-ville", "secteur-ouest": "Ouest", } _PRICE_LI = re.compile(r"\d[\d\s]{0,5}\s?\$\s*par mois", re.I) def _clean_price_label(label: str) -> str: """« 1 125$ par mois » (espace de milliers) -> compatible parse_price.""" return re.sub(r"(\d)\s(\d{3})", r"\1\2", label) class ImmeublesDbConnector(BaseConnector): source_id = "immeubles_db" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: dict[str, Listing] = {} for slug, sector in SECTORS.items(): try: html = self.get(f"{BASE}/logements-a-louer/{slug}/").text except Exception: continue soup = BeautifulSoup(html, "html.parser") for container in soup.select("div.container"): try: lst = self._parse_block(container, sector) except Exception: continue if lst and lst.external_id not in listings: listings[lst.external_id] = lst return list(listings.values()) # -- une section .container = un logement offert -------------------------------- def _parse_block(self, container, sector: str) -> Listing | None: h3 = container.select_one(".cell_container h3") ul = container.select_one(".cell_container ul") if not (h3 and ul): return None address_full = re.sub(r"\s+", " ", h3.get_text(" ", strip=True)).strip(" ,") lis = [re.sub(r"\s+", " ", li.get_text(" ", strip=True)).strip() for li in ul.select("li")] lis = [t for t in lis if t] if not (address_full and lis): return None # « 250, Olivier, Sherbrooke, QC » -> adresse sans la province parts = [p.strip() for p in address_full.split(",") if p.strip()] address = ", ".join(p for p in parts if p.upper() != "QC") city = "Sherbrooke" type_line = lis[0] # « 4 CHAMBRES disponibles à partir de 550 $ » = location à la chambre # (normalize_unit_type transformerait « 4 chambres » en 6½+) if re.search(r"chambres?\s+disponibles?", type_line, re.I): unit_type = "Chambre" else: unit_type = normalize_unit_type(type_line) if not re.fullmatch(r"\d½\+?|6½\+|Studio|Loft|Chambre|Maison", unit_type or ""): unit_type = "" # prix : première ligne « … $ par mois » price = None price_label = "" for t in lis: if _PRICE_LI.search(t): price_label = t price = parse_price(_clean_price_label(t)) break # disponibilité : seulement si la source l'écrit (sinon vide) availability = "" m = re.search(r"libre\s+imm[ée]diatement", " ".join(lis), re.I) if m: availability = "Libre immédiatement" # photos de la galerie jumelle (chemins relatifs /modules/upload/…) images: list[str] = [] for img in container.select(".galerie_img_block a[href]"): href = (img.get("href") or "").strip() if href.startswith("/"): href = BASE + href if href.startswith("http") and href not in images: images.append(href) ext_id = hashlib.sha1( f"{sector}|{address_full}|{type_line}".encode("utf-8")).hexdigest()[:16] return Listing( source=self.source_id, external_id=ext_id, url=f"{BASE}/logements-a-louer/" f"{[k for k, v in SECTORS.items() if v == sector][0]}/", title=f"{type_line} — {address}", address=address, sector=sector, city=city, unit_type=unit_type, price=price, price_label=price_label, availability=availability, description=" | ".join(lis)[:1500], images=images[:20], )