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/vestric.py : connecteur Vestric — Condos locatifs (vestric.ca)5# Projet unique à Candiac (Montérégie). WordPress (thème Vortex « vy_ »)6# rendu serveur : la page /les-unites liste les unités disponibles7# (li.vy_units_listing_item : no d'unité, modèle, étage, type, superficie,8# disponibilité + lien /units/unite-<no>). Granularité : unité. Prix non9# publiés (formulaire de contact avec fourchettes de budget seulement).10# -----------------------------------------------------------------------------11from __future__ import annotations1213import re1415from bs4 import BeautifulSoup1617from .base import BaseConnector18from ..schema import Listing, normalize_unit_type1920BASE = "https://vestric.ca"21LIST_URL = f"{BASE}/les-unites"2223CITY = "Candiac"2425IMG_RE = re.compile(r'https://vestric\.ca/app/uploads/'26 r'[^"\s\\)]+\.(?:jpe?g|png|webp)', re.I)27PDF_RE = re.compile(r'https://vestric\.ca/app/uploads/[^"\s\\)]+\.pdf', re.I)28SQFT_RE = re.compile(r"(\d{3,4})\s*pi", re.I)293031class VestricConnector(BaseConnector):32 source_id = "vestric"33 request_delay = 0.63435 def fetch(self) -> list[Listing]:36 listings: list[Listing] = []37 try:38 html = self.get(LIST_URL).text39 except Exception:40 return listings41 soup = BeautifulSoup(html, "html.parser")4243 for item in soup.select("li.vy_units_listing_item"):44 try:45 def cell(cls: str) -> str:46 el = item.select_one(f"div.vy_units_listing_{cls} span")47 return el.get_text(" ", strip=True) if el else ""4849 unit_no = cell("unit")50 if not unit_no:51 continue52 model = cell("model").replace("Modèle", "").strip()53 floor = cell("floor")54 unit_type = normalize_unit_type(cell("type"))55 sqft = None56 m = SQFT_RE.search(cell("square"))57 if m:58 sqft = float(m.group(1))59 availability = cell("availability")6061 a = item.select_one('a[href*="/units/"]')62 url = (a.get("href", "").split("#")[0] if a63 else f"{BASE}/units/unite-{unit_no}")6465 details: dict = {}66 if model:67 details["model"] = model68 if floor:69 details["floor"] = floor7071 # page détail (cache BD) : photos de l'unité72 key = f"{unit_type}|{sqft}|{availability}|{model}"73 d = self.detail(f"unite-{unit_no}", key,74 lambda url=url: self._fetch_detail(url))7576 listings.append(Listing(77 source=self.source_id,78 external_id=f"unite-{unit_no}",79 url=url,80 title=f"Unité {unit_no} ({unit_type}) — Vestric",81 address="200, avenue des Chênes, Candiac",82 city=CITY,83 unit_type=unit_type,84 availability=availability,85 area_sqft=sqft,86 details={**details, **({"plan_pdf": d["plan_pdf"]} if d.get("plan_pdf") else {})},87 images=d.get("images") or [],88 ))89 except Exception:90 continue91 return listings9293 def _fetch_detail(self, url: str) -> dict:94 out: dict = {}95 try:96 html = self.get(url).text97 except Exception:98 return out99 out["images"] = [u for u in dict.fromkeys(IMG_RE.findall(html))100 if not re.search(r"logo|icon|favicon|og-image|"101 r"decoration|bloc-vestric|"102 r"-\d{2,3}x\d{2,3}\.", u, re.I)][:15]103 pdfs = PDF_RE.findall(html)104 if pdfs:105 out["plan_pdf"] = pdfs[0]106 return out107