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/viventi.py : connecteur Complexe Viventi Laval (viventilaval.com)5# Immeuble unique pour 45/50 ans et plus au 1100, boul. Vanier (secteur6# Saint-Vincent-de-Paul, Laval). WordPress/Elementor rendu serveur : la page7# /nos-unites/ affiche une section par TYPOLOGIE (3½, 4½) avec prix8# « À partir de X$/mois » et liste d'inclusions. Granularité TYPOLOGIE,9# pas de disponibilité par unité.10# -----------------------------------------------------------------------------11from __future__ import annotations1213import re1415from bs4 import BeautifulSoup1617from ..schema import Listing, normalize_unit_type18from .base import BaseConnector1920BASE = "https://viventilaval.com"21PAGE_URL = f"{BASE}/nos-unites/"2223ADDRESS = "1100, boul. Vanier"24CITY = "Laval"25SECTOR = "Saint-Vincent-de-Paul"2627TYPE_RE = re.compile(r"^\d\s*(?:1/2|½)$")28PRICE_RE = re.compile(r"À partir de\s*([\d\s ,]+)\s*\$\s*/\s*mois", re.I)29IMG_RE = re.compile(30 r"https://viventilaval\.com/wp-content/uploads/[^\"'\s\\)]+?"31 r"\.(?:jpg|jpeg|png|webp)", re.I)323334class ViventiConnector(BaseConnector):35 source_id = "viventi"36 request_delay = 0.63738 def fetch(self) -> list[Listing]:39 listings: list[Listing] = []40 try:41 html = self.get(PAGE_URL).text42 except Exception:43 return listings44 soup = BeautifulSoup(html, "html.parser")4546 # photos communes de l'immeuble (les sections typologie n'ont pas de47 # galerie propre) — on écarte logos/favicons et miniatures redimensionnées48 images = [u for u in dict.fromkeys(IMG_RE.findall(html))49 if not re.search(r"logo|favicon|icon|-\d+x\d+\.", u, re.I)][:20]5051 # une section par typologie : <h2>3 1/2</h2> suivi d'un widget texte52 # avec « À partir de X$/mois » + <ul> d'inclusions53 for heading in soup.select(".elementor-heading-title"):54 label = heading.get_text(" ", strip=True)55 if not TYPE_RE.match(label):56 continue57 unit_type = normalize_unit_type(label)58 container = heading.find_parent(59 "div", class_="elementor-widget-heading")60 price_label, description, amenities = "", "", []61 node = container62 # widgets suivants de la même section (jusqu'à la prochaine typologie)63 while node is not None:64 node = node.find_next_sibling("div")65 if node is None:66 break67 text = node.get_text(" ", strip=True)68 if TYPE_RE.match(text):69 break70 m = PRICE_RE.search(text)71 if m and not price_label:72 price_label = ("À partir de "73 + re.sub(r"[\s ]", "", m.group(1))74 + " $/mois")75 for li in node.select("li"):76 item = li.get_text(" ", strip=True)77 if item and item not in amenities:78 amenities.append(item)79 if not description:80 for p in node.select("p"):81 t = p.get_text(" ", strip=True)82 if len(t) > 60 and "partir de" not in t.lower():83 description = t84 break85 if not price_label:86 continue87 ext_id = f"vanier-{unit_type.replace('½', '.5')}"88 listings.append(Listing(89 source=self.source_id,90 external_id=ext_id,91 url=PAGE_URL,92 title=f"{unit_type} — Complexe Viventi Laval (50 ans et plus)",93 address=ADDRESS,94 sector=SECTOR,95 city=CITY,96 unit_type=unit_type,97 price_label=price_label,98 description=description[:1500],99 amenities=amenities,100 details={"clientele": "50 ans et plus"},101 images=images,102 ))103 return listings104