# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/imvest.py : connecteur Société Imvest (societeimvest.ca) # Le Méribel, 195 logements en 2 phases au 1191, rue de Courchevel à Lévis # (secteur Saint-Romuald). WordPress (thème TeamPress) avec plans d'étage # interactifs : endpoints AJAX POST maison — # /ajax/recherche-etage/ (id=immeuble) -> liste des étages (li.Etage) # /ajax/load-etage/ (id=étage) -> par unité (no, type, # superficie dans data-tooltip) + JSON `areas` dont fillColor code le # statut : cbcbcb = loué, 940723 = réservé, autre = disponible # /ajax/recherche-unite/ (id=unité) -> détail (disponibilité, plan, # caractéristiques) # Granularité : unité. Pas de prix publié (Tier B). # ----------------------------------------------------------------------------- from __future__ import annotations import hashlib import json import re from ..schema import Listing, normalize_unit_type from .base import BaseConnector BASE = "https://www.societeimvest.ca" AJAX_ETAGES = f"{BASE}/ajax/recherche-etage/" AJAX_PLAN = f"{BASE}/ajax/load-etage/" AJAX_UNITE = f"{BASE}/ajax/recherche-unite/" # phases (slug de page « immeubles ») — l'id AJAX est lu sur la page PHASES = [("le-meribel", "Le Méribel — Phase 1"), ("le-meribel-phase-2", "Le Méribel — Phase 2")] ADDRESS = "1191, rue de Courchevel, Lévis" LAT, LNG = 46.732973, -71.262941 IMMEUBLE_ID_RE = re.compile(r"immeuble_id = '(\d+)'") FLOOR_RE = re.compile(r'
  • ]*' r'data-key="(\d+)"') AREAS_JSON_RE = re.compile(r"areas:\s*(\[.*?\])", re.S) TYPE_RE = re.compile(r"([^<]*)") SQFT_RE = re.compile(r"Superficie\s*:\s*([\d\s]+)\s*p\.?c\.?", re.I) DISPO_RE = re.compile(r"Disponibilit[^:]*:\s*" r"([^<]*)", re.I) PLAN_RE = re.compile(r'href="(https://www\.societeimvest\.ca/wp-content/' r'uploads/[^"]+\.pdf)"') IMG_RE = re.compile(r']+src="([^"]+phpThumb[^"]+)"') CARAC_RE = re.compile(r"Caractéristiques du logement\s*
      (.*?)
    ", re.S) LI_RE = re.compile(r"
  • (.*?)
  • ", re.S) # statuts non offerts en location (gris = loué, rouge foncé = réservé) TAKEN_COLORS = {"cbcbcb", "940723"} class ImvestConnector(BaseConnector): source_id = "imvest" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: list[Listing] = [] for slug, phase_name in PHASES: page_url = f"{BASE}/immeubles/{slug}/" try: page = self.get(page_url).text except Exception: continue m = IMMEUBLE_ID_RE.search(page) if not m: continue try: floors_html = self.post(AJAX_ETAGES, data={"id": m.group(1)}).text except Exception: continue for floor_id, floor_name in FLOOR_RE.findall(floors_html): try: listings.extend(self._parse_floor( slug, phase_name, page_url, floor_id, floor_name)) except Exception: continue return listings def _parse_floor(self, slug: str, phase_name: str, page_url: str, floor_id: str, floor_name: str) -> list[Listing]: out: list[Listing] = [] plan_html = self.post(AJAX_PLAN, data={"id": floor_id}).text # statut de chaque unité (JSON mapster : key = no d'unité) status: dict[str, str] = {} m = AREAS_JSON_RE.search(plan_html) if m: try: for a in json.loads(m.group(1)): status[str(a.get("key"))] = a.get("fillColor", "") except ValueError: pass for tooltip, unit_id, unit_no in AREA_RE.findall(plan_html): color = status.get(unit_no, "") if not color or color.lower() in TAKEN_COLORS: continue # loué / réservé / inconnu tm = TYPE_RE.search(tooltip) unit_type = normalize_unit_type(tm.group(1)) if tm else "" sm = SQFT_RE.search(tooltip) area = float(sm.group(1).replace(" ", "").replace(" ", "")) \ if sm else None key = hashlib.sha1(f"{color}|{tooltip}".encode()).hexdigest() d = self.detail(f"{slug}-{unit_no}", key, lambda uid=unit_id: self._fetch_unit(uid)) out.append(Listing( source=self.source_id, external_id=f"{slug}-{unit_no}", url=page_url, title=f"Unité {unit_no} — {phase_name}", address=ADDRESS, sector="Saint-Romuald", city="Lévis", unit_type=unit_type, area_sqft=area, price_label="Prix sur demande", availability=d.get("availability", ""), description=f"Unité {unit_no} ({unit_type}), {floor_name} — " f"{phase_name}, Société Imvest, Lévis.", amenities=d.get("amenities") or [], details={"floor": floor_name, **({"floor_plan": d["plan"]} if d.get("plan") else {})}, images=d.get("images") or [], lat=LAT, lng=LNG, )) return out def _fetch_unit(self, unit_id: str) -> dict: """Détail d'une unité via /ajax/recherche-unite/ (POST id=…).""" out: dict = {} try: html = self.post(AJAX_UNITE, data={"id": unit_id}).text except Exception: return out m = DISPO_RE.search(html) if m: dispo = m.group(1).strip() out["availability"] = dispo if re.match(r"^\d|^libre|^immédiat", dispo, re.I) \ else f"Disponibilité : {dispo}" m = PLAN_RE.search(html) if m: out["plan"] = m.group(1) m = CARAC_RE.search(html) if m: out["amenities"] = [ re.sub(r"<[^>]+>", "", li).strip() for li in LI_RE.findall(m.group(1)) if re.sub(r"<[^>]+>", "", li).strip()][:20] imgs = [u.replace("&", "&") for u in IMG_RE.findall(html) if "ajax_loading" not in u] if imgs: out["images"] = imgs[:5] return out