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/lak_nature.py : connecteur LAK Appartements Nature5# (lakappartementsnature.ca — 1004, boul. du Lac, Lac-Beauport, près de la6# station Le Relais ; 62 unités locatives, gestion Acropole). WordPress +7# Elementor avec sélecteur de plans « do-selecteur-plans » : chaque unité8# est un <polygon> SVG rendu serveur avec sa classe d'état9# (disponible/indisponible) et ses data-* (data-numero=id interne,10# data-etage, data-type, data-prix, data-promotion). On n'ingère que les11# unités DISPONIBLES ; la fiche JSON /wp-json/do-selecteur-plans/v1/unite12# ?id=<numero> (via cache détail) confirme l'état et fournit le vrai13# numéro d'unité, superficies, plan PDF et galerie.14# -----------------------------------------------------------------------------15from __future__ import annotations1617import hashlib18import re1920from bs4 import BeautifulSoup2122from ..schema import Listing, normalize_unit_type, parse_price23from .base import BaseConnector2425BASE = "https://lakappartementsnature.ca"26JSON_URL = f"{BASE}/wp-json/do-selecteur-plans/v1/unite"2728ADDRESS = "1004, boulevard du Lac, Lac-Beauport, G3B 2T4"29CITY = "Lac-Beauport"3031# photos de l'immeuble dans la page (hors logos/icônes/plans)32IMG_RE = re.compile(r'(?:src|href)="(https://lakappartementsnature\.ca/'33 r'wp-content/uploads/[^"]+\.(?:jpe?g|png|webp))"', re.I)343536class LakNatureConnector(BaseConnector):37 source_id = "lak_nature"38 request_delay = 0.639 max_details = 40 # garde-fou fiches JSON (vraies requêtes par sync)4041 def fetch(self) -> list[Listing]:42 listings: list[Listing] = []43 html = self.get(f"{BASE}/").text44 soup = BeautifulSoup(html, "html.parser")45 self._fetched = 04647 building_images = [u for u in dict.fromkeys(IMG_RE.findall(html))48 if not re.search(r"logo|icon|favicon|plan|Lak_plan",49 u, re.I)][:12]5051 details_common: dict = {}52 phones = dict.fromkeys(re.findall(r'href="tel:\+?1?(\d{10})"', html))53 if phones:54 d10 = next(iter(phones))55 details_common["contact"] = {56 "phone": f"{d10[:3]}-{d10[3:6]}-{d10[6:]}"}5758 # polygones SVG du sélecteur : un par unité (dupliqués desktop/mobile)59 seen: set[str] = set()60 for poly in soup.find_all("polygon"):61 try:62 classes = " ".join(poly.get("class") or [])63 numero = (poly.get("data-numero") or "").strip()64 if not numero or numero in seen:65 continue66 seen.add(numero)67 # on n'ingère que les unités encore offertes68 if "indisponible" in classes or "disponible" not in classes:69 continue7071 unit_type = normalize_unit_type(72 poly.get("data-type") or "")73 price = parse_price(poly.get("data-prix") or "")74 floor = (poly.get("data-etage") or "").strip()75 promo = (poly.get("data-promotion") or "").strip()7677 # fiche JSON de l'unité : vrai numéro, état confirmé,78 # superficies, plan PDF, galerie79 ext_id = numero80 availability = "Disponible"81 area = None82 images = list(building_images)83 details = dict(details_common)84 amenities: list[str] = []85 key = hashlib.sha1(86 f"{numero}|{classes}|{price}".encode()).hexdigest()87 try:88 d = self.detail(numero, key,89 lambda n=numero: self._unite_json(n))90 except Exception:91 d = {}92 if d:93 if d.get("etat") and d["etat"] != "disponible":94 continue # l'API fait foi sur l'état95 if d.get("unite"):96 ext_id = str(d["unite"])97 if d.get("date_libre"):98 availability = str(d["date_libre"])99 try:100 v = float(str(d.get("superficie_habitable") or "")101 .replace(" ", "").replace(",", "."))102 if 200 <= v <= 3000:103 area = v104 except ValueError:105 pass106 if d.get("superficie_terrasse"):107 amenities.append(108 f"Terrasse : {d['superficie_terrasse']} pi²")109 if d.get("style"):110 amenities.append(f"Style : {d['style']}")111 if d.get("pdf"):112 details["plan_pdf"] = d["pdf"]113 gal = [u for u in (d.get("gallery") or [])114 if isinstance(u, str)]115 if gal:116 images = gal + building_images117 if promo:118 amenities.append(f"Promotion : {promo}")119 if floor:120 amenities.append(f"Étage : {floor}")121122 listings.append(Listing(123 source=self.source_id,124 external_id=ext_id,125 url=f"{BASE}/",126 title=f"{unit_type} — unité {ext_id}, "127 f"LAK Appartements Nature",128 address=ADDRESS,129 city=CITY,130 unit_type=unit_type,131 price=price,132 availability=availability,133 area_sqft=area,134 amenities=amenities,135 details=details,136 images=images[:20],137 ))138 except Exception:139 continue140 return listings141142 # -- fiche JSON d'une unité (endpoint du plugin do-selecteur-plans) ----------143 def _unite_json(self, numero: str) -> dict:144 if self._fetched >= self.max_details:145 raise RuntimeError("budget de fiches JSON atteint")146 self._fetched += 1147 d = self.get(f"{JSON_URL}?id={numero}").json()148 return d if isinstance(d, dict) else {}149