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/grandeur_natura.py : connecteur Grandeur Natura5# (grandeurnatura.ca, Groupe Mathieux) — 6-plex locatifs neufs au 1199,6# rue Marcel-de la Sablonnière, Terrebonne (Urbanova), J0N 1H0.7# WordPress/Bootstrap rendu serveur : la page d'accueil décrit chaque8# typologie dans un bloc « Unité locative - Appartement n ½ » (SDB,9# chambres, garage, carrousel de photos par unité, plan PDF commun).10# Les blocs « Maisons de prestige » sont des maisons à VENDRE : exclus.11# Aucun prix publié (jamais inventé). Granularité TYPOLOGIE (3½/4½/5½).12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from bs4 import BeautifulSoup1819from ..schema import Listing20from .base import BaseConnector2122BASE = "https://grandeurnatura.ca"23ADDRESS = "1199, rue Marcel-de la Sablonnière"24CITY = "Terrebonne"25SECTOR = "Urbanova"2627UNIT_H3_RE = re.compile(r"^Unité locative\s*[–-]\s*Appartement\s*([2-6])\s*½")28SDB_RE = re.compile(r"(\d)\s*SDB", re.I)29BEDS_RE = re.compile(r"(\d)\s*chambres?\b", re.I)30GARAGE_RE = re.compile(r"(Garage[^.\n|]{0,40})", re.I)31IMG_OK_RE = re.compile(r"\.(?:jpe?g|png|webp)$", re.I)323334class GrandeurNaturaConnector(BaseConnector):35 source_id = "grandeur_natura"36 request_delay = 0.83738 def fetch(self) -> list[Listing]:39 listings: list[Listing] = []40 try:41 html = self.get(BASE + "/").text42 except Exception:43 return listings44 soup = BeautifulSoup(html, "html.parser")45 for h3 in soup.select("h3"):46 title = h3.get_text(" ", strip=True)47 m = UNIT_H3_RE.match(title)48 if not m:49 continue # « Maisons de prestige » = vente50 n = m.group(1)51 unit_type = f"{n}½"52 row = h3.find_parent("div", class_="row") or h3.parent53 text = row.get_text(" | ", strip=True)5455 baths = beds = None56 bm = SDB_RE.search(text)57 if bm:58 baths = float(bm.group(1))59 cm = BEDS_RE.search(text)60 if cm:61 beds = float(cm.group(1))62 amenities: list[str] = []63 gm = GARAGE_RE.search(text)64 if gm:65 amenities.append(gm.group(1).strip())6667 # phrase descriptive du bloc (le premier paragraphe substantiel)68 desc = ""69 for p in row.select("p"):70 t = p.get_text(" ", strip=True)71 if len(t) > 40:72 desc = t73 break7475 # photos du carrousel de l'unité (variantes @2x/redimensionnées76 # dédupliquées ; logos exclus)77 images: list[str] = []78 for img in row.select("img"):79 src = img.get("src") or img.get("data-src") or ""80 if src.startswith("/"):81 src = BASE + src82 if not src.startswith("http") or not IMG_OK_RE.search(src):83 continue84 if re.search(r"logo|icon|@2x|-\d+x\d+\.|-scaled", src, re.I):85 continue86 if src not in images:87 images.append(src)8889 details: dict = {"building": "Grandeur Natura"}90 plan = row.select_one('a[href$=".pdf"]')91 if plan is not None:92 href = plan.get("href") or ""93 details["plan_pdf"] = (BASE + href) if href.startswith("/") \94 else href9596 listings.append(Listing(97 source=self.source_id,98 external_id=f"unite-{n}.5", # typologie : stable99 url=BASE + "/",100 title=f"Grandeur Natura — {unit_type}",101 address=ADDRESS,102 sector=SECTOR,103 city=CITY,104 unit_type=unit_type,105 bedrooms=beds,106 bathrooms=baths,107 price=None,108 price_label="",109 availability="Sur demande",110 description=desc[:2000],111 amenities=amenities,112 details=details,113 images=images[:15],114 ))115 return listings116