# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/in_via.py : connecteur IN VIA appartements urbains # (inviamirabel.com — CAP Immobilier, appartements locatifs neufs au # 14385 et 14365 rue Roger-Thomas, secteur Saint-Janvier à Mirabel, # phases de 32 unités). WordPress/Elementor rendu SERVEUR : la page # /appartements/ annonce les typologies offertes en location (3½, 4½, # 5½) et les phases avec leur adresse ; AUCUN prix ni disponibilité par # unité publiés (« renseignez-vous sur la disponibilité ») — rien # d'inventé. Granularité TYPOLOGIE (external_id = « invia-N.5 », stable). # Les phases « À venir » ne créent pas d'annonce distincte. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type from .base import BaseConnector BASE = "https://www.inviamirabel.com" PAGE_URL = f"{BASE}/appartements/" ADDRESS = "14385, rue Roger-Thomas" SECTOR = "Saint-Janvier" CITY = "Mirabel" # « Louez un appartement 3 ½, 4 ½ ou 5 ½ » OFFER_RE = re.compile(r"Louez un appartement((?:\s*\d\s*½[,\s]*(?:ou)?\s*)+)", re.I) TYPO_RE = re.compile(r"(\d)\s*½") PHASE_RE = re.compile(r"Phase\s*(\d+)\s+((?:\d{4,5}\s+)?[\wÀ-ü' -]+?)" r"(?=\s*(?:Plans intérieurs|Phase|$))") IMG_RE = re.compile(r"https://www\.inviamirabel\.com/wp-content/uploads/" r"[^\"\s\\]+?\.(?:jpe?g|png|webp)", re.I) IMG_SIZE_RE = re.compile(r"-\d+x\d+(?=\.(?:jpe?g|png|webp))", re.I) class InViaConnector(BaseConnector): source_id = "in_via" request_delay = 0.8 def fetch(self) -> list[Listing]: try: html = self.get(PAGE_URL).text except Exception: return [] soup = BeautifulSoup(html, "html.parser") text = re.sub(r"\s+", " ", soup.get_text(" ", strip=True)) # typologies réellement offertes (« Louez un appartement 3 ½, 4 ½ ou 5 ½ ») m = OFFER_RE.search(text) typos = TYPO_RE.findall(m.group(1)) if m else TYPO_RE.findall(text[:600]) typos = list(dict.fromkeys(typos)) if not typos: return [] # phases livrées (adresse civique) — « Phase 3 À venir » ignorée phases: list[str] = [] for pm in PHASE_RE.finditer(text): label = pm.group(2).strip() if re.match(r"^\d{4,5}\s", label): phases.append(f"Phase {pm.group(1)} : {label}") # description : paragraphes de présentation du site desc_parts: list[str] = [] for pat in (r"(Déménagez dans un appartement[^.]+\.)", r"(Vivez dans un espace de vie neuf[^.]+\.)", r"(Chaque unité inclut un stationnement[^.]+\.)"): mm = re.search(pat, text) if mm: desc_parts.append(mm.group(1).strip()) blurb = " ".join(desc_parts) images = [] for u in dict.fromkeys(IMG_RE.findall(html)): u = IMG_SIZE_RE.sub("", u) if not re.search(r"logo|icon|favicon|plan", u, re.I) \ and u not in images: images.append(u) amenities = ["Comptoirs de quartz", "Air climatisé", "Stationnement intérieur inclus", "Espace de rangement au garage"] listings: list[Listing] = [] for t in typos: unit_type = normalize_unit_type(f"{t} 1/2") listings.append(Listing( source=self.source_id, external_id=f"invia-{t}.5", url=PAGE_URL, title=f"Appartement {unit_type} — IN VIA Mirabel", address=ADDRESS, sector=SECTOR, city=CITY, unit_type=unit_type, description=(blurb + (" " + " | ".join(phases) if phases else ""))[:1200], amenities=amenities, details={"phases": phases} if phases else {}, images=images[:20], )) return listings