# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/nelson.py : connecteur Le Nelson (lenelson.com) # Immeuble locatif de 5 étages au 111, rue Jean-Besré à Cowansville # (Brome-Missisquoi). Site PHP statique rendu serveur : une page par étage # (/appartements-a-louer/logements-a-louer-cowansville-) avec, pour # chaque unité, un lien porteur de data-attributes (data-appt, data-area, # data-room, data-bathroom, data-pdf, data-availability, data-type). # ⚠️ HTTPS obligatoire (les URL http bouclent en 301) et User-Agent complet # requis (406 sinon — géré par l'escalade de BaseConnector). Granularité : # unité ; on ne retient que celles non « LOUÉ ». Prix non publiés. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from .base import BaseConnector from ..schema import Listing, normalize_unit_type BASE = "https://lenelson.com" FLOOR_PAGES = [ ("premiere-etage", 1), ("deuxieme-etage", 2), ("troisieme-etage", 3), ("quatrieme-etage", 4), ("cinquieme-etage", 5), ] ADDRESS = "111, rue Jean-Besré, Cowansville" CITY = "Cowansville" SQFT_RE = re.compile(r"(\d{3,4})") UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36") class NelsonConnector(BaseConnector): source_id = "nelson" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: list[Listing] = [] for slug, floor in FLOOR_PAGES: url = (f"{BASE}/appartements-a-louer/" f"logements-a-louer-cowansville-{slug}") try: html = self.get(url, headers={"User-Agent": UA}).text except Exception: continue soup = BeautifulSoup(html, "html.parser") for a in soup.select("a[data-appt]"): try: unit_no = (a.get("data-appt") or "").strip() if not unit_no: continue availability = (a.get("data-availability") or "").strip() if re.search(r"lou[ée]", availability, re.I): continue # déjà loué unit_type = normalize_unit_type( (a.get("data-type") or "").strip()) sqft = None ms = SQFT_RE.search(a.get("data-area") or "") if ms: sqft = float(ms.group(1)) bedrooms = bathrooms = None if (a.get("data-room") or "").isdigit(): bedrooms = int(a["data-room"]) if (a.get("data-bathroom") or "").isdigit(): bathrooms = int(a["data-bathroom"]) details: dict = {"floor": f"Étage {floor}"} pdf = (a.get("data-pdf") or "").strip() if pdf: details["plan_pdf"] = (pdf if pdf.startswith("http") else BASE + pdf) images: list[str] = [] thumb = (a.get("data-thumbnail") or "").strip() if thumb and "default-thumbnail" not in thumb: images.append(thumb if thumb.startswith("http") else BASE + thumb) ext_id = f"logement-{unit_no}" if any(l.external_id == ext_id for l in listings): continue listings.append(Listing( source=self.source_id, external_id=ext_id, url=url, title=f"Logement {unit_no}" + (f" ({unit_type})" if unit_type else "") + " — Le Nelson", address=ADDRESS, city=CITY, unit_type=unit_type, bedrooms=bedrooms, bathrooms=bathrooms, availability=availability or "Disponible", area_sqft=sqft, details=details, images=images, )) except Exception: continue return listings