# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/grandeur_natura.py : connecteur Grandeur Natura # (grandeurnatura.ca, Groupe Mathieux) — 6-plex locatifs neufs au 1199, # rue Marcel-de la Sablonnière, Terrebonne (Urbanova), J0N 1H0. # WordPress/Bootstrap rendu serveur : la page d'accueil décrit chaque # typologie dans un bloc « Unité locative - Appartement n ½ » (SDB, # chambres, garage, carrousel de photos par unité, plan PDF commun). # Les blocs « Maisons de prestige » sont des maisons à VENDRE : exclus. # Aucun prix publié (jamais inventé). Granularité TYPOLOGIE (3½/4½/5½). # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing from .base import BaseConnector BASE = "https://grandeurnatura.ca" ADDRESS = "1199, rue Marcel-de la Sablonnière" CITY = "Terrebonne" SECTOR = "Urbanova" UNIT_H3_RE = re.compile(r"^Unité locative\s*[–-]\s*Appartement\s*([2-6])\s*½") SDB_RE = re.compile(r"(\d)\s*SDB", re.I) BEDS_RE = re.compile(r"(\d)\s*chambres?\b", re.I) GARAGE_RE = re.compile(r"(Garage[^.\n|]{0,40})", re.I) IMG_OK_RE = re.compile(r"\.(?:jpe?g|png|webp)$", re.I) class GrandeurNaturaConnector(BaseConnector): source_id = "grandeur_natura" request_delay = 0.8 def fetch(self) -> list[Listing]: listings: list[Listing] = [] try: html = self.get(BASE + "/").text except Exception: return listings soup = BeautifulSoup(html, "html.parser") for h3 in soup.select("h3"): title = h3.get_text(" ", strip=True) m = UNIT_H3_RE.match(title) if not m: continue # « Maisons de prestige » = vente n = m.group(1) unit_type = f"{n}½" row = h3.find_parent("div", class_="row") or h3.parent text = row.get_text(" | ", strip=True) baths = beds = None bm = SDB_RE.search(text) if bm: baths = float(bm.group(1)) cm = BEDS_RE.search(text) if cm: beds = float(cm.group(1)) amenities: list[str] = [] gm = GARAGE_RE.search(text) if gm: amenities.append(gm.group(1).strip()) # phrase descriptive du bloc (le premier paragraphe substantiel) desc = "" for p in row.select("p"): t = p.get_text(" ", strip=True) if len(t) > 40: desc = t break # photos du carrousel de l'unité (variantes @2x/redimensionnées # dédupliquées ; logos exclus) images: list[str] = [] for img in row.select("img"): src = img.get("src") or img.get("data-src") or "" if src.startswith("/"): src = BASE + src if not src.startswith("http") or not IMG_OK_RE.search(src): continue if re.search(r"logo|icon|@2x|-\d+x\d+\.|-scaled", src, re.I): continue if src not in images: images.append(src) details: dict = {"building": "Grandeur Natura"} plan = row.select_one('a[href$=".pdf"]') if plan is not None: href = plan.get("href") or "" details["plan_pdf"] = (BASE + href) if href.startswith("/") \ else href listings.append(Listing( source=self.source_id, external_id=f"unite-{n}.5", # typologie : stable url=BASE + "/", title=f"Grandeur Natura — {unit_type}", address=ADDRESS, sector=SECTOR, city=CITY, unit_type=unit_type, bedrooms=beds, bathrooms=baths, price=None, price_label="", availability="Sur demande", description=desc[:2000], amenities=amenities, details=details, images=images[:15], )) return listings