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/carre_legendes.py : connecteur Carré des Légendes5# (carredeslegendes.ca — Saint-Jean-sur-Richelieu, boul. Saint-Luc).6# Condos locatifs haut de gamme (~7 bâtiments). WordPress rendu serveur :7# la page /habitations/ embarque une carte SVG interactive (bâtiments →8# niveaux → unités) dont TOUTES les données sont dans le HTML statique :9# tooltip « disp / indisp » (Disponible, Réservée, Déjà louée) + bloc10# « content level4 » (bâtiment, no d'unité, type | superficie, inclusions,11# photos, plan PDF). Granularité : unité. Prix non publiés.12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from bs4 import BeautifulSoup1819from ..schema import Listing, normalize_unit_type20from .base import BaseConnector2122BASE = "https://carredeslegendes.ca"23PAGE_URL = f"{BASE}/habitations/"2425CITY = "Saint-Jean-sur-Richelieu"2627# « 4½ | 1240 pi. ca. »28TYPE_SQFT_RE = re.compile(r"([\dS][\d ½/]*)\s*\|\s*([\d\s]+)\s*pi", re.I)29BATIMENT_RE = re.compile(r"B[âa]timent\s*#?\s*(\d+)", re.I)303132class CarreLegendesConnector(BaseConnector):33 source_id = "carre_legendes"34 request_delay = 0.63536 def fetch(self) -> list[Listing]:37 listings: list[Listing] = []38 try:39 html = self.get(PAGE_URL).text40 except Exception:41 return listings42 soup = BeautifulSoup(html, "html.parser")4344 # 1) statut par unité : tooltips « ttip_<id> » (disp = encore offerte)45 status: dict[str, str] = {}46 for tip in soup.select("div.tooltip"):47 tid = (tip.get("id") or "").replace("ttip_", "")48 spans = [s.get_text(" ", strip=True) for s in tip.find_all("span")]49 if not tid or not spans or not spans[0].startswith("Unité"):50 continue51 classes = tip.get("class") or []52 if "disp" not in classes:53 continue # « indisp » = déjà louée54 status[tid] = spans[1] if len(spans) > 1 else "Disponible"5556 # 2) fiches d'unités : blocs « content level4 » (content_<id>)57 for block in soup.select("div.content.level4"):58 bid = (block.get("id") or "").replace("content_", "")59 if bid not in status:60 continue61 try:62 infos = block.select_one("div.bottom_infos")63 if not infos:64 continue65 h2 = infos.find("h2")66 unit_no = ""67 if h2:68 m = re.search(r"(\d+)", h2.get_text(" ", strip=True))69 if m:70 unit_no = m.group(1)71 if not unit_no:72 continue73 batiment = ""74 st = infos.select_one("p.sous-titre")75 if st:76 m = BATIMENT_RE.search(st.get_text(" ", strip=True))77 if m:78 batiment = m.group(1)7980 unit_type, sqft = "", None81 h3 = block.find("h3")82 if h3:83 m = TYPE_SQFT_RE.search(h3.get_text(" ", strip=True))84 if m:85 unit_type = normalize_unit_type(m.group(1))86 try:87 sqft = float(m.group(2).replace(" ", "")88 .replace(" ", ""))89 except ValueError:90 sqft = None9192 # inclusions de l'immeuble (paragraphe centré de la fiche)93 amenities: list[str] = []94 for p in infos.find_all("p"):95 if "sous-titre" in (p.get("class") or []):96 continue97 txt = p.get_text("\n", strip=True)98 amenities += [a.strip() for a in txt.split("\n")99 if 3 < len(a.strip()) < 80]100101 images = []102 for img in block.select("div.gallery img"):103 src = img.get("src") or ""104 if src.startswith("http"):105 images.append(re.sub(r"-\d+x\d+(\.\w+)$", r"\1", src))106 plan = block.select_one("a.planpdf")107 details: dict = {}108 if plan and plan.get("href"):109 details["plan_pdf"] = plan["href"]110 if batiment:111 details["building"] = f"Bâtiment {batiment}"112113 ext_id = f"b{batiment or 'x'}-{unit_no}"114 listings.append(Listing(115 source=self.source_id,116 external_id=ext_id,117 url=PAGE_URL,118 title=f"Unité {unit_no} — Bâtiment {batiment}, "119 f"Carré des Légendes" if batiment else120 f"Unité {unit_no} — Carré des Légendes",121 address="boulevard Saint-Luc, Saint-Jean-sur-Richelieu",122 city=CITY,123 unit_type=unit_type,124 availability=status.get(bid, ""),125 area_sqft=sqft,126 amenities=list(dict.fromkeys(amenities))[:15],127 details=details,128 images=list(dict.fromkeys(images))[:15],129 ))130 except Exception:131 continue132133 # dédoublonner (le même content_<id> ne devrait sortir qu'une fois)134 uniq: dict[str, Listing] = {}135 for l in listings:136 uniq.setdefault(l.external_id, l)137 return list(uniq.values())138