# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/jardins_nd.py : connecteur Les Jardins Notre Dame # (logement-victoriaville.jardinsnotredame.com) — Victoriaville, # 465, rue Notre-Dame Ouest. Complexe locatif neuf tout inclus (studios # meublés à 5½). Landing Unbounce page unique (~580 Ko) rendue serveur : # pas de sélecteurs sémantiques (divs lp-pom-*) — on parse le TEXTE en ordre # du document : une ligne typologie (« Studios », « 3 1/2 », « 4 1/2 », # « 5 1/2 ») suivie de lignes d'inclusions puis d'une fourchette de prix # (« De 1 550 $ à 1 625 $ /mois »). Granularité = typologie (pas # d'inventaire par unité publié). external_id = typologie en slug. # ----------------------------------------------------------------------------- from __future__ import annotations import html as _html import re from ..schema import Listing, normalize_unit_type from .base import BaseConnector PAGE_URL = "https://logement-victoriaville.jardinsnotredame.com/" ADDRESS = "465, rue Notre-Dame Ouest, Victoriaville" TYPE_RE = re.compile(r"^(Studios?|\d\s*(?:1/2|½))$", re.I) PRICE_RANGE_RE = re.compile( r"^(?:De\s+)?([\d\s ]{3,7})\$\s*(?:à\s*([\d\s ]{3,7})\$)?\s*/\s*mois", re.I) IMG_RE = re.compile( r"//d9hhrg4mnvzow\.cloudfront\.net/[^\"'\s),]+\.(?:jpg|jpeg|png|webp)", re.I) SCRIPT_RE = re.compile(r"<(script|style)[\s\S]*?", re.I) TAG_RE = re.compile(r"<[^>]+>") def _num(txt: str) -> float | None: n = re.sub(r"[\s ]", "", txt or "") try: return float(n) except ValueError: return None class JardinsNotreDameConnector(BaseConnector): source_id = "jardins_nd" request_delay = 0.8 def fetch(self) -> list[Listing]: html = self.get(PAGE_URL).text # Photos (CDN Unbounce) : garder la variante la plus large par visuel images: list[str] = [] for u in dict.fromkeys(IMG_RE.findall(html)): u = "https:" + u base = re.sub(r"_[\w]+o\.(jpg|jpeg|png|webp)$", "", u) if not any(im.startswith(base) for im in images): images.append(u) images = images[:12] # HTML -> lignes de texte en ordre du document txt = SCRIPT_RE.sub(" ", html) txt = _html.unescape(TAG_RE.sub("\n", txt)) lines = [re.sub(r"[\s ]+", " ", l).strip() for l in txt.split("\n")] lines = [l for l in lines if l] listings: list[Listing] = [] seen: set[str] = set() current: str = "" amenities: list[str] = [] for line in lines: if TYPE_RE.match(line): current = line amenities = [] continue if not current: continue pm = PRICE_RANGE_RE.match(line) if not pm: # inclusions courtes entre la typologie et le prix if len(line) < 60 and not re.search(r"Voir|plan", line, re.I): amenities.append(line) elif len(amenities) > 4: # trop loin : carte abandonnée current = "" continue unit_type = "Studio" if re.match(r"studio", current, re.I) \ else normalize_unit_type(current) ext_id = "studio" if unit_type == "Studio" \ else unit_type.replace("½", ".5") if ext_id in seen: # la page répète les cartes current = "" continue seen.add(ext_id) price = _num(pm.group(1)) price_max = _num(pm.group(2)) if pm.group(2) else None details: dict = {} if price_max: details["price_max"] = price_max listings.append(Listing( source=self.source_id, external_id=ext_id, url=PAGE_URL, title=f"{unit_type} — Les Jardins Notre Dame", address=ADDRESS, sector="", city="Victoriaville", unit_type=unit_type, price=price, price_label=pm.group(0).strip(), description=f"{unit_type} tout inclus aux Jardins Notre Dame, " "complexe locatif neuf de Victoriaville " "(piscine, tennis, espaces communs).", amenities=list(dict.fromkeys(amenities)), details=details, images=list(images), )) current = "" return listings