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/in_via.py : connecteur IN VIA appartements urbains5# (inviamirabel.com — CAP Immobilier, appartements locatifs neufs au6# 14385 et 14365 rue Roger-Thomas, secteur Saint-Janvier à Mirabel,7# phases de 32 unités). WordPress/Elementor rendu SERVEUR : la page8# /appartements/ annonce les typologies offertes en location (3½, 4½,9# 5½) et les phases avec leur adresse ; AUCUN prix ni disponibilité par10# unité publiés (« renseignez-vous sur la disponibilité ») — rien11# d'inventé. Granularité TYPOLOGIE (external_id = « invia-N.5 », stable).12# Les phases « À venir » ne créent pas d'annonce distincte.13# -----------------------------------------------------------------------------14from __future__ import annotations1516import re1718from bs4 import BeautifulSoup1920from ..schema import Listing, normalize_unit_type21from .base import BaseConnector2223BASE = "https://www.inviamirabel.com"24PAGE_URL = f"{BASE}/appartements/"25ADDRESS = "14385, rue Roger-Thomas"26SECTOR = "Saint-Janvier"27CITY = "Mirabel"2829# « Louez un appartement 3 ½, 4 ½ ou 5 ½ »30OFFER_RE = re.compile(r"Louez un appartement((?:\s*\d\s*½[,\s]*(?:ou)?\s*)+)",31 re.I)32TYPO_RE = re.compile(r"(\d)\s*½")33PHASE_RE = re.compile(r"Phase\s*(\d+)\s+((?:\d{4,5}\s+)?[\wÀ-ü' -]+?)"34 r"(?=\s*(?:Plans intérieurs|Phase|$))")35IMG_RE = re.compile(r"https://www\.inviamirabel\.com/wp-content/uploads/"36 r"[^\"\s\\]+?\.(?:jpe?g|png|webp)", re.I)37IMG_SIZE_RE = re.compile(r"-\d+x\d+(?=\.(?:jpe?g|png|webp))", re.I)383940class InViaConnector(BaseConnector):41 source_id = "in_via"42 request_delay = 0.84344 def fetch(self) -> list[Listing]:45 try:46 html = self.get(PAGE_URL).text47 except Exception:48 return []49 soup = BeautifulSoup(html, "html.parser")50 text = re.sub(r"\s+", " ", soup.get_text(" ", strip=True))5152 # typologies réellement offertes (« Louez un appartement 3 ½, 4 ½ ou 5 ½ »)53 m = OFFER_RE.search(text)54 typos = TYPO_RE.findall(m.group(1)) if m else TYPO_RE.findall(text[:600])55 typos = list(dict.fromkeys(typos))56 if not typos:57 return []5859 # phases livrées (adresse civique) — « Phase 3 À venir » ignorée60 phases: list[str] = []61 for pm in PHASE_RE.finditer(text):62 label = pm.group(2).strip()63 if re.match(r"^\d{4,5}\s", label):64 phases.append(f"Phase {pm.group(1)} : {label}")6566 # description : paragraphes de présentation du site67 desc_parts: list[str] = []68 for pat in (r"(Déménagez dans un appartement[^.]+\.)",69 r"(Vivez dans un espace de vie neuf[^.]+\.)",70 r"(Chaque unité inclut un stationnement[^.]+\.)"):71 mm = re.search(pat, text)72 if mm:73 desc_parts.append(mm.group(1).strip())74 blurb = " ".join(desc_parts)7576 images = []77 for u in dict.fromkeys(IMG_RE.findall(html)):78 u = IMG_SIZE_RE.sub("", u)79 if not re.search(r"logo|icon|favicon|plan", u, re.I) \80 and u not in images:81 images.append(u)8283 amenities = ["Comptoirs de quartz", "Air climatisé",84 "Stationnement intérieur inclus",85 "Espace de rangement au garage"]86 listings: list[Listing] = []87 for t in typos:88 unit_type = normalize_unit_type(f"{t} 1/2")89 listings.append(Listing(90 source=self.source_id,91 external_id=f"invia-{t}.5",92 url=PAGE_URL,93 title=f"Appartement {unit_type} — IN VIA Mirabel",94 address=ADDRESS,95 sector=SECTOR,96 city=CITY,97 unit_type=unit_type,98 description=(blurb + (" " + " | ".join(phases)99 if phases else ""))[:1200],100 amenities=amenities,101 details={"phases": phases} if phases else {},102 images=images[:20],103 ))104 return listings105