# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/brio.py : connecteur Les Immeubles Brio (immeublesbrio.com) # Projet mono-immeuble « Le Brio » à Val-Bélair (boul. Pie-XI, Québec). # Les unités sont affichées par étage (hotspots Divi) avec statut # Disponible / Loué ; les prix « à partir de » par type (3½/4½/5½) # sont affichés sur la page d'accueil. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, infer_city, normalize_unit_type, parse_price from .base import BaseConnector BASE = "https://immeublesbrio.com" HOME_URL = f"{BASE}/" LIST_URL = f"{BASE}/appartements-a-louer-val-belair/" SECTOR = "Val-Bélair" # Adresse civique affichée sur la page Contactez-nous (« Visitez-nous ») ADDRESS = "1105, rue des Rigoles, Québec, QC G3K 0M7" class BrioConnector(BaseConnector): source_id = "brio" request_delay = 0.6 def fetch(self) -> list[Listing]: # 1) Page d'accueil : prix « à partir de » par type (ex. « 3½ à # partir de 1500$ »), services de l'immeuble (blurbs Divi) et # contact (téléphone/courriel de l'en-tête). type_prices: dict[str, tuple[float | None, str]] = {} services: list[str] = [] contact: dict = {} try: home = self.get(HOME_URL).text home_txt = re.sub(r"<[^>]+>", " ", home) for m in re.finditer(r"(\d)\s*½\s*à\s*partir\s*de\s*([\d\s ]+)\$", home_txt): label = f"{m.group(1)}½ à partir de {m.group(2).strip()}$" type_prices[m.group(1)] = (parse_price(f"{m.group(2)}$"), label) hsoup = BeautifulSoup(home, "html.parser") # Services de l'immeuble (Wifi, ascenseur, stationnement…) services = [h.get_text(" ", strip=True) for h in hsoup.select(".et_pb_blurb .et_pb_module_header") if h.get_text(strip=True)][:12] # Contact : courriel (lien mailto:) + téléphone (en-tête) mail = hsoup.select_one('a[href^="mailto:"]') if mail: contact["email"] = mail["href"].removeprefix("mailto:").strip() m = re.search(r"\b(\d{3})[-.\s](\d{3})[-.\s](\d{4})\b", home_txt) if m: contact["phone"] = f"{m.group(1)}-{m.group(2)}-{m.group(3)}" except Exception: pass # 2) Page des appartements : hotspots par étage html = self.get(LIST_URL).text soup = BeautifulSoup(html, "html.parser") # Galerie générale de l'immeuble (photos des pièces) gallery = [] for img in soup.select("img"): src = img.get("data-src") or img.get("src") or "" if re.search(r"/wp-content/uploads/.*(chambre|salle|salon|cuisine)" r"[^\"]*\.(?:jpg|jpeg|png|webp)$", src, re.I): gallery.append(src if src.startswith("http") else BASE + src) gallery = list(dict.fromkeys(gallery))[:8] listings: list[Listing] = [] for info in soup.select("div.hotspot-info"): try: title_el = info.select_one(".hotspot-title") if not title_el: continue title = title_el.get_text(" ", strip=True) m = re.match(r"(Disponible|Lou[ée])\s*-?\s*Appartement\s*(\d+)" r"\s*:\s*(.+)", title, re.I) if not m: continue status, num, raw_type = m.groups() if not status.lower().startswith("dispo"): continue # on ne garde que les unités disponibles unit_type = normalize_unit_type(raw_type) digit = re.search(r"(\d)", unit_type or "") price, price_label = (None, "") if digit and digit.group(1) in type_prices: price, price_label = type_prices[digit.group(1)] # Contenu : superficie + caractéristiques content = info.select_one(".hotspot-content") amenities: list[str] = [] description = "" if content: sup = content.find("strong") if sup: description = sup.get_text(strip=True) amenities = [li.get_text(" ", strip=True) for li in content.select("li")] # Images : plan de l'unité (vignette + plan complet) + galerie images: list[str] = [] thumb = info.select_one(".hotspot-thumb img") if thumb: src = thumb.get("data-src") or thumb.get("src") or "" if src.startswith("http"): # version pleine grandeur (retirer le suffixe -300x284) full = re.sub(r"-\d+x\d+(\.(?:jpg|jpeg|png|webp))$", r"\1", src) images.append(full) if content: for a in content.select("a[href]"): href = a.get("href", "") if re.search(r"\.(?:jpg|jpeg|png|webp)$", href, re.I): images.append(href) images.extend(gallery) images = list(dict.fromkeys(images)) details: dict = {} if contact: details["contact"] = dict(contact) listings.append(Listing( source=self.source_id, external_id=f"appartement-{num}", url=LIST_URL, title=f"Le Brio — Appartement {num} ({unit_type})", address=ADDRESS, sector=SECTOR, city=infer_city(SECTOR), unit_type=unit_type, price=price, price_label=price_label, availability="Disponible", description=description, amenities=amenities + services, details=details, images=images, )) except Exception: continue return listings