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/vita_stl.py : connecteur Appartements VITA (appartementsvita.ca)5# 2505, boulevard Marcel-Laurin, Saint-Laurent (Montréal) — deux phases,6# 1½ à 5½, à deux pas du REM Bois-Franc. WordPress/Elementor : le sélecteur7# d'unités (plugin wp-floor-plans) ne retourne AUCUNE unité individuelle8# (vérifié via admin-ajax sur les deux phases et tous les étages) — la page9# d'accueil affiche plutôt une ligne par type : « 3 ½ à partir de 1650$ »10# ou « 1 ½ : liste d'attente ». Une annonce par type avec prix réel (les11# types en liste d'attente sont exclus), id stable vita-<type>.12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from bs4 import BeautifulSoup1819from ..schema import Listing20from .base import BaseConnector2122BASE = "https://appartementsvita.ca"23ADDRESS = "2505, boulevard Marcel-Laurin, Saint-Laurent, Montréal, QC H4R 0R7"2425_TYPE_RE = re.compile(r"^([1-6])\s*½\s*(.*)$")26_PRICE_RE = re.compile(r"à partir de\s*([\d\s,]+)\s*\$", re.I)27_IMG_RE = re.compile(28 r'https://appartementsvita\.ca/wp-content/uploads/[^"\'\s\\)]+?'29 r'\.(?:jpg|jpeg|png|webp)', re.I)30_SKIP_IMG = re.compile(r"logo|icon|favicon|-\d{2,4}x\d{2,4}\.", re.I)313233class VitaStlConnector(BaseConnector):34 source_id = "vita_stl"35 request_delay = 0.636 max_images = 203738 def fetch(self) -> list[Listing]:39 html = self.get(f"{BASE}/").text40 soup = BeautifulSoup(html, "html.parser")4142 # galerie du projet (accueil, sans logos/miniatures)43 images: list[str] = []44 for u in _IMG_RE.findall(html):45 if _SKIP_IMG.search(u):46 continue47 if u not in images:48 images.append(u)49 images = images[: self.max_images]5051 # description : méta de la page52 desc = ""53 meta = soup.find("meta", attrs={"name": "description"})54 if meta and meta.get("content"):55 desc = meta["content"].strip()[:600]56 if not desc:57 og = soup.find("meta", property="og:description")58 if og and og.get("content"):59 desc = og["content"].strip()[:600]6061 # une ligne « n ½ … » par type dans les listes à icônes Elementor62 listings: list[Listing] = []63 seen: set[str] = set()64 for span in soup.select(".elementor-icon-list-text"):65 t = span.get_text(" ", strip=True)66 m = _TYPE_RE.match(t)67 if not m:68 continue69 n, rest = m.group(1), m.group(2)70 unit_type = f"{n}½"71 if unit_type in seen:72 continue73 pm = _PRICE_RE.search(rest)74 if not pm:75 continue # « liste d'attente » : exclu76 digits = re.sub(r"[^\d]", "", pm.group(1))77 if not digits:78 continue79 price = float(digits)80 if not (100 <= price <= 20000):81 continue82 seen.add(unit_type)83 listings.append(Listing(84 source=self.source_id,85 external_id=f"vita-{n}5",86 url=f"{BASE}/",87 title=f"Appartements VITA — {unit_type}",88 address=ADDRESS,89 sector="Saint-Laurent",90 city="Montréal",91 unit_type=unit_type,92 price=price,93 price_label=f"À partir de {int(price)} $/mois",94 availability=t,95 description=desc,96 images=list(images),97 ))98 return listings99