# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/lok36.py : connecteur LOK 36 (lok36.com) # Immeuble locatif au 36, boulevard de la Seigneurie Ouest, Blainville # (60 unités, 3½ à 5½). WordPress rendu serveur : la page /prix-plans/ # contient une image cliquable dont chaque unité a un tooltip #
avec Modèle : TypeN, # Numéro:NNN, Chambre(s) :4½, Superficie : NNN pi2, Prix : NNNN$, # Statut : LOUÉ|Disponible et un lien plan PDF. On ne garde que les unités # NON louées. Granularité : unité (external_id = numéro d'unité). # ----------------------------------------------------------------------------- from __future__ import annotations import re from ..schema import Listing, normalize_unit_type from .base import BaseConnector BASE = "https://www.lok36.com" PLANS_URL = f"{BASE}/prix-plans/" ADDRESS = "36, boulevard de la Seigneurie Ouest, Blainville" CITY = "Blainville" TOOLTIP_RE = re.compile( r'
') FIELD_RE = { "model": re.compile(r"Modèle\s*:\s*([^<\s][^<]*)"), "num": re.compile(r"Numéro\s*:\s*([\w-]+)"), "typo": re.compile(r"Chambre\(s\)\s*:\s*([^<]+)"), "area": re.compile(r"Superficie\s*:\s*(\d[\d\s]*)"), "price": re.compile(r"Prix\s*:\s*(\d[\d\s,]*)\s*\$"), "status": re.compile( r"Statut\s*:\s*(.+?)(?:\s*Télécharger|$)", re.I), } PDF_RE = re.compile(r'href="(https://www\.lok36\.com/[^"]+\.pdf)"') IMG_RE = re.compile( r"https://www\.lok36\.com/wp-content/uploads/[^\"'\s\\]+" r"\.(?:jpe?g|png|webp)", re.I) SKIP_IMG_RE = re.compile(r"logo|favicon|icon|plan|feuillet|-\d{2,4}x\d{2,4}\.", re.I) class Lok36Connector(BaseConnector): source_id = "lok36" request_delay = 0.6 max_images = 10 def _site_images(self) -> list[str]: """Photos du projet (page d'accueil) — les tooltips n'ont qu'un PDF.""" 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() parts = TOOLTIP_RE.split(html) for i in range(1, len(parts) - 1, 2): body = parts[i + 1] body = body.split("
")[0] # fin du tooltip text = re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", body)) fields = {k: (m.group(1).strip() if (m := rx.search(text)) else "") for k, rx in FIELD_RE.items()} num = fields["num"] if not num or num in seen: continue seen.add(num) if re.search(r"lou[ée]", fields["status"], re.I): continue # LOUÉ unit_type = normalize_unit_type(fields["typo"]) price = None price_label = "" if fields["price"]: try: v = float(fields["price"].replace(" ", "") .replace(" ", "").replace(",", "")) if 300 <= v <= 20000: price = v price_label = f"{int(v)}$/mois" except ValueError: pass area = None if fields["area"]: try: v = float(fields["area"].replace(" ", "").replace(" ", "")) if 100 <= v <= 10000: area = v except ValueError: pass details: dict = {} m = PDF_RE.search(body) if m: details["floor_plan"] = m.group(1) desc_bits = [f"Unité {num}", f"Modèle {fields['model']}" if fields["model"] else ""] listings.append(Listing( source=self.source_id, external_id=num, url=PLANS_URL, title=f"LOK 36 — Unité {num}", address=ADDRESS, city=CITY, unit_type=unit_type, price=price, price_label=price_label, availability=fields["status"] or "Disponible", area_sqft=area, description=" — ".join(b for b in desc_bits if b), details=details, images=list(images), )) return listings