# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/vita_stl.py : connecteur Appartements VITA (appartementsvita.ca) # 2505, boulevard Marcel-Laurin, Saint-Laurent (Montréal) — deux phases, # 1½ à 5½, à deux pas du REM Bois-Franc. WordPress/Elementor : le sélecteur # d'unités (plugin wp-floor-plans) ne retourne AUCUNE unité individuelle # (vérifié via admin-ajax sur les deux phases et tous les étages) — la page # d'accueil affiche plutôt une ligne par type : « 3 ½ à partir de 1650$ » # ou « 1 ½ : liste d'attente ». Une annonce par type avec prix réel (les # types en liste d'attente sont exclus), id stable vita-. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing from .base import BaseConnector BASE = "https://appartementsvita.ca" ADDRESS = "2505, boulevard Marcel-Laurin, Saint-Laurent, Montréal, QC H4R 0R7" _TYPE_RE = re.compile(r"^([1-6])\s*½\s*(.*)$") _PRICE_RE = re.compile(r"à partir de\s*([\d\s,]+)\s*\$", re.I) _IMG_RE = re.compile( r'https://appartementsvita\.ca/wp-content/uploads/[^"\'\s\\)]+?' r'\.(?:jpg|jpeg|png|webp)', re.I) _SKIP_IMG = re.compile(r"logo|icon|favicon|-\d{2,4}x\d{2,4}\.", re.I) class VitaStlConnector(BaseConnector): source_id = "vita_stl" request_delay = 0.6 max_images = 20 def fetch(self) -> list[Listing]: html = self.get(f"{BASE}/").text soup = BeautifulSoup(html, "html.parser") # galerie du projet (accueil, sans logos/miniatures) images: list[str] = [] for u in _IMG_RE.findall(html): if _SKIP_IMG.search(u): continue if u not in images: images.append(u) images = images[: self.max_images] # description : méta de la page desc = "" meta = soup.find("meta", attrs={"name": "description"}) if meta and meta.get("content"): desc = meta["content"].strip()[:600] if not desc: og = soup.find("meta", property="og:description") if og and og.get("content"): desc = og["content"].strip()[:600] # une ligne « n ½ … » par type dans les listes à icônes Elementor listings: list[Listing] = [] seen: set[str] = set() for span in soup.select(".elementor-icon-list-text"): t = span.get_text(" ", strip=True) m = _TYPE_RE.match(t) if not m: continue n, rest = m.group(1), m.group(2) unit_type = f"{n}½" if unit_type in seen: continue pm = _PRICE_RE.search(rest) if not pm: continue # « liste d'attente » : exclu digits = re.sub(r"[^\d]", "", pm.group(1)) if not digits: continue price = float(digits) if not (100 <= price <= 20000): continue seen.add(unit_type) listings.append(Listing( source=self.source_id, external_id=f"vita-{n}5", url=f"{BASE}/", title=f"Appartements VITA — {unit_type}", address=ADDRESS, sector="Saint-Laurent", city="Montréal", unit_type=unit_type, price=price, price_label=f"À partir de {int(price)} $/mois", availability=t, description=desc, images=list(images), )) return listings