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/l_laval.py : connecteur Le L Laval (l-laval.com)5# Projet locatif unique au 1585, boulevard des Laurentides, Laval (104 unités,6# studio à 5½). WordPress (thème maison) rendu serveur : la page7# /plans-condos-locatifs-laval/ contient un sélecteur d'étages avec UNE ligne8# <tr id="rowUnitNNN"> par unité — la classe « not-available » marque les9# unités louées, et l'attribut onclick=loadPage(unité, prix, pi², étage, état,10# typologie, sdb, salle d'eau, den, modulable) porte toutes les données.11# Granularité : unité. Certaines unités libres affichent 0$ (prix sur demande).12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from ..schema import Listing, normalize_unit_type18from .base import BaseConnector1920BASE = "https://l-laval.com"21PLANS_URL = f"{BASE}/plans-condos-locatifs-laval/"2223ADDRESS = "1585, boulevard des Laurentides, Laval"24CITY = "Laval"2526# <tr id="rowUnit104" … class="ch1 not-available" … onclick='loadPage("104",…)'>27ROW_RE = re.compile(28 r'<tr\s+id="rowUnit(\w+)"[^>]*class="([^"]*)"[^>]*'29 r"onclick='loadPage\(([^)]*)\)'", re.I)30ARG_RE = re.compile(r'"([^"]*)"')31IMG_RE = re.compile(32 r"https://l-laval\.com/wp-content/uploads/[^\"'\s\\]+\.(?:jpe?g|png|webp)",33 re.I)34SKIP_IMG_RE = re.compile(r"logo|favicon|icon|plan|-\d{2,4}x\d{2,4}\.", re.I)353637class LLavalConnector(BaseConnector):38 source_id = "l_laval"39 request_delay = 0.640 max_images = 104142 def _site_images(self) -> list[str]:43 """Photos du projet (page d'accueil) — les unités n'ont pas de galerie."""44 try:45 html = self.get(f"{BASE}/").text46 except Exception:47 return []48 return [u for u in dict.fromkeys(IMG_RE.findall(html))49 if not SKIP_IMG_RE.search(u)][: self.max_images]5051 def fetch(self) -> list[Listing]:52 html = self.get(PLANS_URL).text53 images = self._site_images()5455 listings: list[Listing] = []56 seen: set[str] = set()57 for unit, classes, raw_args in ROW_RE.findall(html):58 if "not-available" in classes or unit in seen:59 continue60 seen.add(unit)61 # loadPage(unité, prix, pi², étage, état, typologie, sdb, s. d'eau,62 # den, modulable) — état : 0 libre, 1 vendu, 2 réservé63 args = ARG_RE.findall(raw_args)64 if len(args) < 6:65 continue66 _, price_raw, sqft_raw, floor, etat, typo = args[:6]67 if etat.strip() not in ("", "0"):68 continue69 sdb = args[6] if len(args) > 6 else ""70 seau = args[7] if len(args) > 7 else ""7172 typo = typo.replace("½", "½").replace("1/2", "½")73 unit_type = normalize_unit_type(typo)7475 price = None76 price_label = ""77 m = re.search(r"(\d[\d\s,]*)\s*\$", price_raw)78 if m:79 try:80 val = float(m.group(1).replace(" ", "").replace(",", ""))81 if 300 <= val <= 20000:82 price = val83 price_label = price_raw.strip()84 except ValueError:85 pass86 if price is None:87 price_label = "Prix sur demande"8889 area = None90 try:91 v = float(sqft_raw)92 if 100 <= v <= 10000:93 area = v94 except ValueError:95 pass9697 desc_bits = [f"Unité {unit}, {floor}e étage" if floor else "",98 f"{sdb} salle(s) de bain" if sdb not in ("", "0") else "",99 "salle d'eau" if seau not in ("", "0") else ""]100 listings.append(Listing(101 source=self.source_id,102 external_id=unit,103 url=f"{PLANS_URL}#unite-{unit}",104 title=f"Le L Laval — Unité {unit}",105 address=ADDRESS,106 city=CITY,107 unit_type=unit_type,108 price=price,109 price_label=price_label,110 availability="Disponible",111 area_sqft=area,112 description=" — ".join(b for b in desc_bits if b),113 images=list(images),114 ))115 return listings116