spb/lou-ka Public
Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 99.7%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/brio.py : connecteur Les Immeubles Brio (immeublesbrio.com)5# Projet mono-immeuble « Le Brio » à Val-Bélair (boul. Pie-XI, Québec).6# Les unités sont affichées par étage (hotspots Divi) avec statut7# Disponible / Loué ; les prix « à partir de » par type (3½/4½/5½)8# sont affichés sur la page d'accueil.9# -----------------------------------------------------------------------------10from __future__ import annotations1112import re1314from bs4 import BeautifulSoup1516from ..schema import Listing, infer_city, normalize_unit_type, parse_price17from .base import BaseConnector1819BASE = "https://immeublesbrio.com"20HOME_URL = f"{BASE}/"21LIST_URL = f"{BASE}/appartements-a-louer-val-belair/"22SECTOR = "Val-Bélair"23# Adresse civique affichée sur la page Contactez-nous (« Visitez-nous »)24ADDRESS = "1105, rue des Rigoles, Québec, QC G3K 0M7"252627class BrioConnector(BaseConnector):28 source_id = "brio"29 request_delay = 0.63031 def fetch(self) -> list[Listing]:32 # 1) Page d'accueil : prix « à partir de » par type (ex. « 3½ à33 # partir de 1500$ »), services de l'immeuble (blurbs Divi) et34 # contact (téléphone/courriel de l'en-tête).35 type_prices: dict[str, tuple[float | None, str]] = {}36 services: list[str] = []37 contact: dict = {}38 try:39 home = self.get(HOME_URL).text40 home_txt = re.sub(r"<[^>]+>", " ", home)41 for m in re.finditer(r"(\d)\s*½\s*à\s*partir\s*de\s*([\d\s ]+)\$",42 home_txt):43 label = f"{m.group(1)}½ à partir de {m.group(2).strip()}$"44 type_prices[m.group(1)] = (parse_price(f"{m.group(2)}$"), label)45 hsoup = BeautifulSoup(home, "html.parser")46 # Services de l'immeuble (Wifi, ascenseur, stationnement…)47 services = [h.get_text(" ", strip=True)48 for h in hsoup.select(".et_pb_blurb .et_pb_module_header")49 if h.get_text(strip=True)][:12]50 # Contact : courriel (lien mailto:) + téléphone (en-tête)51 mail = hsoup.select_one('a[href^="mailto:"]')52 if mail:53 contact["email"] = mail["href"].removeprefix("mailto:").strip()54 m = re.search(r"\b(\d{3})[-.\s](\d{3})[-.\s](\d{4})\b", home_txt)55 if m:56 contact["phone"] = f"{m.group(1)}-{m.group(2)}-{m.group(3)}"57 except Exception:58 pass5960 # 2) Page des appartements : hotspots par étage61 html = self.get(LIST_URL).text62 soup = BeautifulSoup(html, "html.parser")6364 # Galerie générale de l'immeuble (photos des pièces)65 gallery = []66 for img in soup.select("img"):67 src = img.get("data-src") or img.get("src") or ""68 if re.search(r"/wp-content/uploads/.*(chambre|salle|salon|cuisine)"69 r"[^\"]*\.(?:jpg|jpeg|png|webp)$", src, re.I):70 gallery.append(src if src.startswith("http") else BASE + src)71 gallery = list(dict.fromkeys(gallery))[:8]7273 listings: list[Listing] = []74 for info in soup.select("div.hotspot-info"):75 try:76 title_el = info.select_one(".hotspot-title")77 if not title_el:78 continue79 title = title_el.get_text(" ", strip=True)80 m = re.match(r"(Disponible|Lou[ée])\s*-?\s*Appartement\s*(\d+)"81 r"\s*:\s*(.+)", title, re.I)82 if not m:83 continue84 status, num, raw_type = m.groups()85 if not status.lower().startswith("dispo"):86 continue # on ne garde que les unités disponibles8788 unit_type = normalize_unit_type(raw_type)89 digit = re.search(r"(\d)", unit_type or "")90 price, price_label = (None, "")91 if digit and digit.group(1) in type_prices:92 price, price_label = type_prices[digit.group(1)]9394 # Contenu : superficie + caractéristiques95 content = info.select_one(".hotspot-content")96 amenities: list[str] = []97 description = ""98 if content:99 sup = content.find("strong")100 if sup:101 description = sup.get_text(strip=True)102 amenities = [li.get_text(" ", strip=True)103 for li in content.select("li")]104105 # Images : plan de l'unité (vignette + plan complet) + galerie106 images: list[str] = []107 thumb = info.select_one(".hotspot-thumb img")108 if thumb:109 src = thumb.get("data-src") or thumb.get("src") or ""110 if src.startswith("http"):111 # version pleine grandeur (retirer le suffixe -300x284)112 full = re.sub(r"-\d+x\d+(\.(?:jpg|jpeg|png|webp))$",113 r"\1", src)114 images.append(full)115 if content:116 for a in content.select("a[href]"):117 href = a.get("href", "")118 if re.search(r"\.(?:jpg|jpeg|png|webp)$", href, re.I):119 images.append(href)120 images.extend(gallery)121 images = list(dict.fromkeys(images))122123 details: dict = {}124 if contact:125 details["contact"] = dict(contact)126127 listings.append(Listing(128 source=self.source_id,129 external_id=f"appartement-{num}",130 url=LIST_URL,131 title=f"Le Brio — Appartement {num} ({unit_type})",132 address=ADDRESS,133 sector=SECTOR,134 city=infer_city(SECTOR),135 unit_type=unit_type,136 price=price,137 price_label=price_label,138 availability="Disponible",139 description=description,140 amenities=amenities + services,141 details=details,142 images=images,143 ))144 except Exception:145 continue146147 return listings148