# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/carre_legendes.py : connecteur Carré des Légendes # (carredeslegendes.ca — Saint-Jean-sur-Richelieu, boul. Saint-Luc). # Condos locatifs haut de gamme (~7 bâtiments). WordPress rendu serveur : # la page /habitations/ embarque une carte SVG interactive (bâtiments → # niveaux → unités) dont TOUTES les données sont dans le HTML statique : # tooltip « disp / indisp » (Disponible, Réservée, Déjà louée) + bloc # « content level4 » (bâtiment, no d'unité, type | superficie, inclusions, # photos, plan PDF). Granularité : unité. Prix non publiés. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type from .base import BaseConnector BASE = "https://carredeslegendes.ca" PAGE_URL = f"{BASE}/habitations/" CITY = "Saint-Jean-sur-Richelieu" # « 4½ | 1240 pi. ca. » TYPE_SQFT_RE = re.compile(r"([\dS][\d ½/]*)\s*\|\s*([\d\s]+)\s*pi", re.I) BATIMENT_RE = re.compile(r"B[âa]timent\s*#?\s*(\d+)", re.I) class CarreLegendesConnector(BaseConnector): source_id = "carre_legendes" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: list[Listing] = [] try: html = self.get(PAGE_URL).text except Exception: return listings soup = BeautifulSoup(html, "html.parser") # 1) statut par unité : tooltips « ttip_ » (disp = encore offerte) status: dict[str, str] = {} for tip in soup.select("div.tooltip"): tid = (tip.get("id") or "").replace("ttip_", "") spans = [s.get_text(" ", strip=True) for s in tip.find_all("span")] if not tid or not spans or not spans[0].startswith("Unité"): continue classes = tip.get("class") or [] if "disp" not in classes: continue # « indisp » = déjà louée status[tid] = spans[1] if len(spans) > 1 else "Disponible" # 2) fiches d'unités : blocs « content level4 » (content_) for block in soup.select("div.content.level4"): bid = (block.get("id") or "").replace("content_", "") if bid not in status: continue try: infos = block.select_one("div.bottom_infos") if not infos: continue h2 = infos.find("h2") unit_no = "" if h2: m = re.search(r"(\d+)", h2.get_text(" ", strip=True)) if m: unit_no = m.group(1) if not unit_no: continue batiment = "" st = infos.select_one("p.sous-titre") if st: m = BATIMENT_RE.search(st.get_text(" ", strip=True)) if m: batiment = m.group(1) unit_type, sqft = "", None h3 = block.find("h3") if h3: m = TYPE_SQFT_RE.search(h3.get_text(" ", strip=True)) if m: unit_type = normalize_unit_type(m.group(1)) try: sqft = float(m.group(2).replace(" ", "") .replace(" ", "")) except ValueError: sqft = None # inclusions de l'immeuble (paragraphe centré de la fiche) amenities: list[str] = [] for p in infos.find_all("p"): if "sous-titre" in (p.get("class") or []): continue txt = p.get_text("\n", strip=True) amenities += [a.strip() for a in txt.split("\n") if 3 < len(a.strip()) < 80] images = [] for img in block.select("div.gallery img"): src = img.get("src") or "" if src.startswith("http"): images.append(re.sub(r"-\d+x\d+(\.\w+)$", r"\1", src)) plan = block.select_one("a.planpdf") details: dict = {} if plan and plan.get("href"): details["plan_pdf"] = plan["href"] if batiment: details["building"] = f"Bâtiment {batiment}" ext_id = f"b{batiment or 'x'}-{unit_no}" listings.append(Listing( source=self.source_id, external_id=ext_id, url=PAGE_URL, title=f"Unité {unit_no} — Bâtiment {batiment}, " f"Carré des Légendes" if batiment else f"Unité {unit_no} — Carré des Légendes", address="boulevard Saint-Luc, Saint-Jean-sur-Richelieu", city=CITY, unit_type=unit_type, availability=status.get(bid, ""), area_sqft=sqft, amenities=list(dict.fromkeys(amenities))[:15], details=details, images=list(dict.fromkeys(images))[:15], )) except Exception: continue # dédoublonner (le même content_ ne devrait sortir qu'une fois) uniq: dict[str, Listing] = {} for l in listings: uniq.setdefault(l.external_id, l) return list(uniq.values())