SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
4.7 KB · 116 lines python
Raw Blame History
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