# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/l_laval.py : connecteur Le L Laval (l-laval.com) # Projet locatif unique au 1585, boulevard des Laurentides, Laval (104 unités, # studio à 5½). WordPress (thème maison) rendu serveur : la page # /plans-condos-locatifs-laval/ contient un sélecteur d'étages avec UNE ligne # par unité — la classe « not-available » marque les # unités louées, et l'attribut onclick=loadPage(unité, prix, pi², étage, état, # typologie, sdb, salle d'eau, den, modulable) porte toutes les données. # Granularité : unité. Certaines unités libres affichent 0$ (prix sur demande). # ----------------------------------------------------------------------------- from __future__ import annotations import re from ..schema import Listing, normalize_unit_type from .base import BaseConnector BASE = "https://l-laval.com" PLANS_URL = f"{BASE}/plans-condos-locatifs-laval/" ADDRESS = "1585, boulevard des Laurentides, Laval" CITY = "Laval" # ROW_RE = re.compile( r']*class="([^"]*)"[^>]*' r"onclick='loadPage\(([^)]*)\)'", re.I) ARG_RE = re.compile(r'"([^"]*)"') IMG_RE = re.compile( r"https://l-laval\.com/wp-content/uploads/[^\"'\s\\]+\.(?:jpe?g|png|webp)", re.I) SKIP_IMG_RE = re.compile(r"logo|favicon|icon|plan|-\d{2,4}x\d{2,4}\.", re.I) class LLavalConnector(BaseConnector): source_id = "l_laval" request_delay = 0.6 max_images = 10 def _site_images(self) -> list[str]: """Photos du projet (page d'accueil) — les unités n'ont pas de galerie.""" try: html = self.get(f"{BASE}/").text except Exception: return [] return [u for u in dict.fromkeys(IMG_RE.findall(html)) if not SKIP_IMG_RE.search(u)][: self.max_images] def fetch(self) -> list[Listing]: html = self.get(PLANS_URL).text images = self._site_images() listings: list[Listing] = [] seen: set[str] = set() for unit, classes, raw_args in ROW_RE.findall(html): if "not-available" in classes or unit in seen: continue seen.add(unit) # loadPage(unité, prix, pi², étage, état, typologie, sdb, s. d'eau, # den, modulable) — état : 0 libre, 1 vendu, 2 réservé args = ARG_RE.findall(raw_args) if len(args) < 6: continue _, price_raw, sqft_raw, floor, etat, typo = args[:6] if etat.strip() not in ("", "0"): continue sdb = args[6] if len(args) > 6 else "" seau = args[7] if len(args) > 7 else "" typo = typo.replace("½", "½").replace("1/2", "½") unit_type = normalize_unit_type(typo) price = None price_label = "" m = re.search(r"(\d[\d\s,]*)\s*\$", price_raw) if m: try: val = float(m.group(1).replace(" ", "").replace(",", "")) if 300 <= val <= 20000: price = val price_label = price_raw.strip() except ValueError: pass if price is None: price_label = "Prix sur demande" area = None try: v = float(sqft_raw) if 100 <= v <= 10000: area = v except ValueError: pass desc_bits = [f"Unité {unit}, {floor}e étage" if floor else "", f"{sdb} salle(s) de bain" if sdb not in ("", "0") else "", "salle d'eau" if seau not in ("", "0") else ""] listings.append(Listing( source=self.source_id, external_id=unit, url=f"{PLANS_URL}#unite-{unit}", title=f"Le L Laval — Unité {unit}", address=ADDRESS, city=CITY, unit_type=unit_type, price=price, price_label=price_label, availability="Disponible", area_sqft=area, description=" — ".join(b for b in desc_bits if b), images=list(images), )) return listings