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/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