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/lienard.py : connecteur Liénard (lienard.ca) — condos locatifs5# à Sainte-Foy (Québec), 2 phases : 950, rue Liénard (unités 1xx-6xx) et6# 1000, avenue Myrand (unités 1001-6006). WordPress rendu serveur ; les7# disponibilités vivent dans des plans d'étage interactifs Image Map Pro :8# chaque <script> embarque `var settings = {...}` (JSON) avec un spot par9# unité et un tooltip « Unité NNN » + statut (Disponible / Loué / Non10# disponible). Granularité : unité. Ni prix ni typologie publiés.11# -----------------------------------------------------------------------------12from __future__ import annotations1314import json15import re1617from ..schema import Listing18from .base import BaseConnector1920BASE = "https://lienard.ca"21# Les deux pages embarquent les 14 cartes (toutes phases) ; une seule suffit,22# la seconde sert de repli si la première change.23PAGES = [f"{BASE}/plans-et-disponibilites/",24 f"{BASE}/plans-et-disponibilites-phase-2/"]25PAGE_PHASE2 = f"{BASE}/plans-et-disponibilites-phase-2/"2627SETTINGS_RE = re.compile(r"var settings = (\{\"id\":\d+.*?\});", re.S)28UNIT_RE = re.compile(r"Unité\s+(\d+)", re.I)2930ADDR_PHASE1 = "950, rue Liénard, Québec"31ADDR_PHASE2 = "1000, avenue Myrand, Québec"32AMENITIES = ["Chauffé", "Éclairé", "Climatisé", "Wi-Fi illimité",33 "Électroménagers inclus"]343536def _tooltip_texts(node, acc: list[str]) -> list[str]:37 """Aplati récursivement les champs `text` d'un tooltip Image Map Pro."""38 if isinstance(node, dict):39 for k, v in node.items():40 if k == "text" and isinstance(v, str):41 acc.append(v.strip())42 else:43 _tooltip_texts(v, acc)44 elif isinstance(node, list):45 for x in node:46 _tooltip_texts(x, acc)47 return acc484950class LienardConnector(BaseConnector):51 source_id = "lienard"52 request_delay = 0.65354 def fetch(self) -> list[Listing]:55 listings: list[Listing] = []56 html = ""57 for page in PAGES:58 try:59 html = self.get(page).text60 except Exception:61 continue62 if SETTINGS_RE.search(html):63 break64 if not html:65 return listings6667 seen: set[str] = set()68 for m in SETTINGS_RE.finditer(html):69 try:70 settings = json.loads(m.group(1))71 except ValueError:72 continue73 map_name = (settings.get("general") or {}).get("name", "")74 plan_img = ((settings.get("image") or {}).get("url") or "")75 for spot in settings.get("spots") or []:76 texts = _tooltip_texts(spot.get("tooltip_content") or {}, [])77 joined = " | ".join(texts)78 um = UNIT_RE.search(joined)79 if not um:80 continue81 unit_no = um.group(1)82 status = next((t for t in texts if t and not UNIT_RE.match(t)),83 "")84 # ne garder que les unités réellement offertes en location85 if not re.match(r"^disponible", status, re.I):86 continue87 ext_id = f"unite-{unit_no}"88 if ext_id in seen:89 continue90 seen.add(ext_id)91 phase2 = len(unit_no) == 4 or "phase2" in map_name.lower()92 address = ADDR_PHASE2 if phase2 else ADDR_PHASE193 floor = unit_no[0]94 listings.append(Listing(95 source=self.source_id,96 external_id=ext_id,97 url=PAGE_PHASE2 if phase2 else PAGES[0],98 title=f"Unité {unit_no} — Liénard"99 f"{' phase 2' if phase2 else ''}",100 address=address,101 sector="Sainte-Foy",102 city="Québec",103 price_label="Prix sur demande",104 availability=status,105 description=f"Unité {unit_no}, {floor}e étage — condo "106 "locatif du projet Liénard à Sainte-Foy "107 "(chauffé, éclairé, climatisé, Wi-Fi et "108 "électroménagers inclus).",109 amenities=list(AMENITIES),110 details={"floor": floor,111 "contact": {"email": "location@lienard.ca",112 "phone": "418-808-8823"}},113 images=[plan_img] if plan_img else [],114 ))115 return listings116