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/nelson.py : connecteur Le Nelson (lenelson.com)5# Immeuble locatif de 5 étages au 111, rue Jean-Besré à Cowansville6# (Brome-Missisquoi). Site PHP statique rendu serveur : une page par étage7# (/appartements-a-louer/logements-a-louer-cowansville-<etage>) avec, pour8# chaque unité, un lien porteur de data-attributes (data-appt, data-area,9# data-room, data-bathroom, data-pdf, data-availability, data-type).10# ⚠️ HTTPS obligatoire (les URL http bouclent en 301) et User-Agent complet11# requis (406 sinon — géré par l'escalade de BaseConnector). Granularité :12# unité ; on ne retient que celles non « LOUÉ ». Prix non publiés.13# -----------------------------------------------------------------------------14from __future__ import annotations1516import re1718from bs4 import BeautifulSoup1920from .base import BaseConnector21from ..schema import Listing, normalize_unit_type2223BASE = "https://lenelson.com"24FLOOR_PAGES = [25 ("premiere-etage", 1),26 ("deuxieme-etage", 2),27 ("troisieme-etage", 3),28 ("quatrieme-etage", 4),29 ("cinquieme-etage", 5),30]3132ADDRESS = "111, rue Jean-Besré, Cowansville"33CITY = "Cowansville"3435SQFT_RE = re.compile(r"(\d{3,4})")36UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "37 "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36")383940class NelsonConnector(BaseConnector):41 source_id = "nelson"42 request_delay = 0.64344 def fetch(self) -> list[Listing]:45 listings: list[Listing] = []46 for slug, floor in FLOOR_PAGES:47 url = (f"{BASE}/appartements-a-louer/"48 f"logements-a-louer-cowansville-{slug}")49 try:50 html = self.get(url, headers={"User-Agent": UA}).text51 except Exception:52 continue53 soup = BeautifulSoup(html, "html.parser")5455 for a in soup.select("a[data-appt]"):56 try:57 unit_no = (a.get("data-appt") or "").strip()58 if not unit_no:59 continue60 availability = (a.get("data-availability") or "").strip()61 if re.search(r"lou[ée]", availability, re.I):62 continue # déjà loué63 unit_type = normalize_unit_type(64 (a.get("data-type") or "").strip())65 sqft = None66 ms = SQFT_RE.search(a.get("data-area") or "")67 if ms:68 sqft = float(ms.group(1))69 bedrooms = bathrooms = None70 if (a.get("data-room") or "").isdigit():71 bedrooms = int(a["data-room"])72 if (a.get("data-bathroom") or "").isdigit():73 bathrooms = int(a["data-bathroom"])7475 details: dict = {"floor": f"Étage {floor}"}76 pdf = (a.get("data-pdf") or "").strip()77 if pdf:78 details["plan_pdf"] = (pdf if pdf.startswith("http")79 else BASE + pdf)80 images: list[str] = []81 thumb = (a.get("data-thumbnail") or "").strip()82 if thumb and "default-thumbnail" not in thumb:83 images.append(thumb if thumb.startswith("http")84 else BASE + thumb)8586 ext_id = f"logement-{unit_no}"87 if any(l.external_id == ext_id for l in listings):88 continue89 listings.append(Listing(90 source=self.source_id,91 external_id=ext_id,92 url=url,93 title=f"Logement {unit_no}"94 + (f" ({unit_type})" if unit_type else "")95 + " — Le Nelson",96 address=ADDRESS,97 city=CITY,98 unit_type=unit_type,99 bedrooms=bedrooms,100 bathrooms=bathrooms,101 availability=availability or "Disponible",102 area_sqft=sqft,103 details=details,104 images=images,105 ))106 except Exception:107 continue108 return listings109