# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/evol.py : connecteur EVOL Saint-Jean (evolstjean.com) # Complexe locatif neuf au 270, rue de la Cabinetterie à # Saint-Jean-sur-Richelieu (par Maître Carré). Site PHP statique rendu # serveur : la page /les-unites.php affiche le prix plancher par typologie # (« 3 ½ à partir de 1 575$ », « 2 ½ COMPLET »…) et les inclusions. # Granularité : une annonce par typologie ; les typologies « COMPLET » # (tout loué) sont ignorées. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type, parse_price from .base import BaseConnector BASE = "https://evolstjean.com" PAGE_URL = f"{BASE}/les-unites.php" ADDRESS = "270, rue de la Cabinetterie, Saint-Jean-sur-Richelieu" CITY = "Saint-Jean-sur-Richelieu" # blocs typologie : « 3 ½ » suivi de « à partir de 1 575$ » ou « COMPLET » TYPE_BLOCK_RE = re.compile( r"(\d)\s*½\s*(COMPLET|à partir de\s*[\d\s,]{3,9}\s*\$)", re.I) IMG_RE = re.compile(r'(?:src|href)="((?:/|images/)[^"]+\.(?:jpe?g|png|webp))"', re.I) PHONE_RE = re.compile(r"(\d{3})\s(\d{3})-(\d{4})") class EvolConnector(BaseConnector): source_id = "evol" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: list[Listing] = [] try: html = self.get(PAGE_URL).text except Exception: return listings soup = BeautifulSoup(html, "html.parser") for tag in soup(["script", "style", "noscript"]): tag.decompose() text = re.sub(r"\s+", " ", soup.get_text(" ", strip=True)) # inclusions (liste de la section « Inclusions ») amenities: list[str] = [] for kw, label in [ (r"Cinq [ée]lectrom[ée]nagers|5 [ée]lectrom[ée]nagers", "5 électroménagers (frigo, cuisinière, lave-vaisselle, laveuse, sécheuse)"), (r"Eau chaude incluse", "Eau chaude incluse"), (r"Climatisation int[ée]gr[ée]e", "Climatisation"), (r"c[âa]blodistribution et Internet incluses", "Câble et Internet inclus"), (r"Comptoirs en quartz", "Comptoirs en quartz"), (r"Balcons?-terrasses? priv[ée]s", "Balcon-terrasse privé"), (r"[Aa]scenseur", "Ascenseur"), (r"[Ss]tationnement int[ée]rieur", "Stationnement intérieur ($)"), ]: if re.search(kw, text): amenities.append(label) # photos du site (galerie de l'immeuble) images = [f"{BASE}/{u.lstrip('/')}" for u in dict.fromkeys(IMG_RE.findall(html)) if not re.search(r"logo|favicon|icon|plan", u, re.I)][:15] contact: dict = {} m = PHONE_RE.search(text) if m: contact["phone"] = f"{m.group(1)}-{m.group(2)}-{m.group(3)}" for n, label in TYPE_BLOCK_RE.findall(text): if "COMPLET" in label.upper(): continue # typologie entièrement louée en ce moment ut = normalize_unit_type(f"{n}½") price_label = re.sub(r"\s+", " ", label).strip() listings.append(Listing( source=self.source_id, external_id=f"evol-{n}.5", url=PAGE_URL, title=f"EVOL Saint-Jean — {ut}", address=ADDRESS, city=CITY, unit_type=ut, price=parse_price(price_label), price_label=price_label, description=("Complexe locatif neuf EVOL au cœur de " "Saint-Jean-sur-Richelieu : unités lumineuses " "aux finitions modernes (comptoirs en quartz, " "dosseret céramique), balcon-terrasse privé, " "généreuse fenestration."), amenities=list(amenities), details={"contact": dict(contact)} if contact else {}, images=images, )) return listings