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
3 days agolast push
HTML 98.9% Python 0.6%
4.7 KB · 128 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/lok36.py : connecteur LOK 36 (lok36.com)5#   Immeuble locatif au 36, boulevard de la Seigneurie Ouest, Blainville6#   (60 unités, 3½ à 5½). WordPress rendu serveur : la page /prix-plans/7#   contient une image cliquable dont chaque unité a un tooltip8#   <div id="wl-tooltip-XXXX-N" class="wl-tooltip"> avec Modèle : TypeN,9#   Numéro:NNN, Chambre(s) :4½, Superficie : NNN pi2, Prix : NNNN$,10#   Statut : LOUÉ|Disponible et un lien plan PDF. On ne garde que les unités11#   NON louées. Granularité : unité (external_id = numéro d'unité).12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from ..schema import Listing, normalize_unit_type1819from .base import BaseConnector2021BASE = "https://www.lok36.com"22PLANS_URL = f"{BASE}/prix-plans/"2324ADDRESS = "36, boulevard de la Seigneurie Ouest, Blainville"25CITY = "Blainville"2627TOOLTIP_RE = re.compile(28    r'<div id="wl-tooltip-([\d-]+)" class="wl-tooltip">')29FIELD_RE = {30    "model": re.compile(r"Modèle\s*:\s*([^<\s][^<]*)"),31    "num": re.compile(r"Numéro\s*:\s*([\w-]+)"),32    "typo": re.compile(r"Chambre\(s\)\s*:\s*([^<]+)"),33    "area": re.compile(r"Superficie\s*:\s*(\d[\d\s]*)"),34    "price": re.compile(r"Prix\s*:\s*(\d[\d\s,]*)\s*\$"),35    "status": re.compile(36        r"Statut\s*:\s*(.+?)(?:\s*Télécharger|$)", re.I),37}38PDF_RE = re.compile(r'href="(https://www\.lok36\.com/[^"]+\.pdf)"')39IMG_RE = re.compile(40    r"https://www\.lok36\.com/wp-content/uploads/[^\"'\s\\]+"41    r"\.(?:jpe?g|png|webp)", re.I)42SKIP_IMG_RE = re.compile(r"logo|favicon|icon|plan|feuillet|-\d{2,4}x\d{2,4}\.",43                         re.I)444546class Lok36Connector(BaseConnector):47    source_id = "lok36"48    request_delay = 0.649    max_images = 105051    def _site_images(self) -> list[str]:52        """Photos du projet (page d'accueil) — les tooltips n'ont qu'un PDF."""53        try:54            html = self.get(f"{BASE}/").text55        except Exception:56            return []57        return [u for u in dict.fromkeys(IMG_RE.findall(html))58                if not SKIP_IMG_RE.search(u)][: self.max_images]5960    def fetch(self) -> list[Listing]:61        html = self.get(PLANS_URL).text62        images = self._site_images()6364        listings: list[Listing] = []65        seen: set[str] = set()66        parts = TOOLTIP_RE.split(html)67        for i in range(1, len(parts) - 1, 2):68            body = parts[i + 1]69            body = body.split("</div></div>")[0]        # fin du tooltip70            text = re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", body))7172            fields = {k: (m.group(1).strip() if (m := rx.search(text)) else "")73                      for k, rx in FIELD_RE.items()}74            num = fields["num"]75            if not num or num in seen:76                continue77            seen.add(num)78            if re.search(r"lou[ée]", fields["status"], re.I):79                continue                                 # LOUÉ8081            unit_type = normalize_unit_type(fields["typo"])8283            price = None84            price_label = ""85            if fields["price"]:86                try:87                    v = float(fields["price"].replace(" ", "")88                              .replace(" ", "").replace(",", ""))89                    if 300 <= v <= 20000:90                        price = v91                        price_label = f"{int(v)}$/mois"92                except ValueError:93                    pass9495            area = None96            if fields["area"]:97                try:98                    v = float(fields["area"].replace(" ", "").replace(" ", ""))99                    if 100 <= v <= 10000:100                        area = v101                except ValueError:102                    pass103104            details: dict = {}105            m = PDF_RE.search(body)106            if m:107                details["floor_plan"] = m.group(1)108109            desc_bits = [f"Unité {num}",110                         f"Modèle {fields['model']}" if fields["model"] else ""]111            listings.append(Listing(112                source=self.source_id,113                external_id=num,114                url=PLANS_URL,115                title=f"LOK 36 — Unité {num}",116                address=ADDRESS,117                city=CITY,118                unit_type=unit_type,119                price=price,120                price_label=price_label,121                availability=fields["status"] or "Disponible",122                area_sqft=area,123                description=" — ".join(b for b in desc_bits if b),124                details=details,125                images=list(images),126            ))127        return listings128