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/sovima.py : connecteur Groupe Sovima (groupesovima.com)5# Appartements/condos locatifs neufs de la Rive-Sud (+ Repentigny) :6# AlterEGO (Brossard), Le Nest (Saint-Bruno-de-Montarville), Quartier7# Vauquelin 2 (Longueuil) et Le Neuf Cent Notre-Dame (Repentigny).8# Nest Ottawa (Ontario) exclu. Site Webflow rendu serveur : chaque projet a9# une page /nos-plans avec les typologies et le prix plancher — motif texte10# « <n> ½ … À PARTIR DE <prix> $ / mois » (ou « Non disponible pour le11# moment », ignoré). Granularité = TYPOLOGIE par projet.12# external_id = <projet>-<n>-demi — stable. ⚠️ Webflow sans charset dans les13# en-têtes -> forcer UTF-8 (même piège que aera3r).14# -----------------------------------------------------------------------------15from __future__ import annotations1617import html as _html18import re1920from ..schema import Listing21from .base import BaseConnector2223BASE = "https://www.groupesovima.com"2425# (slug projet, nom, adresse, ville)26SITES: tuple[tuple[str, str, str, str], ...] = (27 ("alterego", "AlterEGO sur le fleuve",28 "Tour 2, 8355, boulevard du St-Laurent, Brossard", "Brossard"),29 ("le-nest", "Le Nest",30 "2000, rue Benoît, Saint-Bruno-de-Montarville",31 "Saint-Bruno-de-Montarville"),32 ("quartier-vauquelin", "Quartier Vauquelin 2",33 "4325, rue des Dirigeables, Longueuil", "Longueuil"),34 ("le-neuf-cent", "Le Neuf Cent Notre-Dame",35 "900, rue Notre-Dame, Repentigny", "Repentigny"),36)3738# « 3 ½ À PARTIR DE 1770 $ / mois » dans le texte aplati (tags -> espaces)39PLAN_RE = re.compile(40 r"(\d)\s*½\s*À\s+PARTIR\s+DE\s+([\d\s ,]{3,9})\$\s*/\s*mois", re.I)41SCRIPT_RE = re.compile(r"<(script|style)[\s\S]*?</\1>", re.I)42TAG_RE = re.compile(r"<[^>]+>")43IMG_RE = re.compile(44 r'(?:src|href)="(https://cdn\.prod\.website-files\.com/[^"\s]+'45 r"\.(?:jpe?g|png|webp|avif))", re.I)464748def _num(txt: str) -> float | None:49 n = re.sub(r"[\s ,]", "", txt or "")50 try:51 return float(n)52 except ValueError:53 return None545556class SovimaConnector(BaseConnector):57 source_id = "sovima"58 request_delay = 0.85960 def fetch(self) -> list[Listing]:61 listings: list[Listing] = []62 for slug, name, address, city in SITES:63 url = f"{BASE}/{slug}/nos-plans"64 try:65 resp = self.get(url)66 resp.encoding = "utf-8" # Webflow sans charset -> latin-167 html = resp.text68 except Exception:69 continue70 # photos : la page /nos-plans n'a pas de galerie, on prend celle71 # de /nos-appartements (rendue serveur elle aussi)72 photos_html = ""73 try:74 p = self.get(f"{BASE}/{slug}/nos-appartements")75 p.encoding = "utf-8"76 photos_html = p.text77 except Exception:78 pass79 listings.extend(self._parse(slug, url, name, address, city,80 html, photos_html))81 return listings8283 def _parse(self, slug: str, url: str, name: str, address: str,84 city: str, html: str, photos_html: str = "") -> list[Listing]:85 text = _html.unescape(SCRIPT_RE.sub(" ", html))86 flat = re.sub(r"[\s ]+", " ", TAG_RE.sub(" ", text))8788 images = [u for u in dict.fromkeys(89 IMG_RE.findall(html) + IMG_RE.findall(photos_html))90 if not re.search(r"logo|favicon|webclip|icon|opengraph",91 u, re.I)][:8]9293 listings: list[Listing] = []94 seen: set[str] = set()95 for m in PLAN_RE.finditer(flat):96 n = int(m.group(1))97 price = _num(m.group(2))98 if not (2 <= n <= 7) or price is None or not (500 <= price <= 8000):99 continue100 ext_id = f"{slug}-{n}-demi"101 if ext_id in seen: # motif répété dans la page102 continue103 seen.add(ext_id)104 unit_type = f"{n}½"105 listings.append(Listing(106 source=self.source_id,107 external_id=ext_id,108 url=url,109 title=f"{unit_type} — {name}",110 address=address,111 sector="",112 city=city,113 unit_type=unit_type,114 price=price,115 price_label=f"à partir de {int(price)} $ / mois",116 description=f"{unit_type} au {name} ({address}) — projet "117 "locatif du Groupe Sovima.",118 images=list(images),119 ))120 return listings121