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/saro.py : connecteur Les Constructions Saro (constructionsaro.com)5# Immeubles locatifs haut de gamme à Saint-Hubert (Longueuil, Rive-Sud),6# clientèle 50 ans et plus, secteur du parc de la Cité. Site ASP.NET7# (ProducNet) une seule page rendue serveur : sections par modèle8# (« 3 ½ Haut de gamme — 850 pi² », 4 ½, 5 ½, 7 ½ bi-génération) avec9# diaporamas d'images par typologie. Aucun prix ni disponibilité publiés.10# Granularité : une annonce par typologie/modèle.11# -----------------------------------------------------------------------------12from __future__ import annotations1314import re1516from bs4 import BeautifulSoup1718from ..schema import Listing, normalize_unit_type19from .base import BaseConnector2021BASE = "https://constructionsaro.com"22PAGE_URL = f"{BASE}/Default.aspx"2324CITY = "Longueuil"25SECTOR = "Saint-Hubert"2627# « 3 ½ Haut de gamme » suivi de « 850 pi2 … » (texte aplati de la section)28MODEL_RE = re.compile(29 r"(\d)\s*½\s*Haut de gamme\s*(?:Bi-g[ée]n[ée]ration[\w\s]{0,20}?)?"30 r"([\d\s]{3,6})\s*pi", re.I)31DIAPO_IMG_RE = re.compile(32 r'href="(Images/Diaporama([A-Za-z0-9]+)/[^"]+\.(?:jpe?g|png))"', re.I)33PHONE_RE = re.compile(r"(\d{3})[.\-](\d{3})[.\-](\d{4})")3435# suffixe du dossier diaporama par typologie (3 -> Diaporama3etDemi…)36DIAPO_OF = {"3": "3etDemi", "4": "4etDemi", "5": "5etDemi", "7": "7etDemi"}373839class SaroConnector(BaseConnector):40 source_id = "saro"41 request_delay = 0.64243 def fetch(self) -> list[Listing]:44 listings: list[Listing] = []45 try:46 html = self.get(PAGE_URL).text47 except Exception:48 return listings49 soup = BeautifulSoup(html, "html.parser")50 for tag in soup(["script", "style", "noscript"]):51 tag.decompose()52 text = re.sub(r"\s+", " ", soup.get_text(" ", strip=True))5354 # commodités : liste « star » de l'immeuble (éléments h5 concaténés)55 amenities: list[str] = []56 for kw, label in [57 (r"aire ouverte avec très grande salle de bain",58 "Condos à aire ouverte, très grande salle de bain"),59 (r"walk-in", "Garde-robe walk-in"),60 (r"insonorisation sup[ée]rieur", "Insonorisation supérieure"),61 (r"Cam[ée]ra de surveillance", "Caméra de surveillance et entrée contrôlée"),62 (r"Plancher en lattes", "Plancher en lattes et céramique"),63 (r"Air climatis[ée]", "Air climatisé"),64 (r"Salle de lavage ind[ée]pendante", "Salle de lavage indépendante"),65 (r"rangement int[ée]rieure?", "Rangement intérieur"),66 (r"Cuisine avec ilo?t", "Cuisine avec îlot"),67 (r"Porte patio", "Porte patio"),68 (r"Grand balcon terrasse", "Grand balcon terrasse"),69 (r"ascenseur", "Ascenseur"),70 (r"50 ans et plus", "Pour les 50 ans et plus"),71 ]:72 if re.search(kw, text, re.I):73 amenities.append(label)7475 contact: dict = {}76 m = PHONE_RE.search(text)77 if m:78 contact["phone"] = f"{m.group(1)}-{m.group(2)}-{m.group(3)}"7980 # images par diaporama de typologie81 imgs_by_type: dict[str, list[str]] = {}82 for path, diapo in DIAPO_IMG_RE.findall(html):83 imgs_by_type.setdefault(diapo, []).append(f"{BASE}/{path}")8485 seen: set[str] = set()86 for n, area in MODEL_RE.findall(text):87 if n in seen:88 continue89 seen.add(n)90 ut = normalize_unit_type(f"{n}½")91 try:92 sqft = float(area.replace(" ", ""))93 except ValueError:94 sqft = None95 bigen = (n == "7")96 desc = ("Immeuble locatif neuf haut de gamme des Constructions "97 "Saro à Saint-Hubert (Longueuil), secteur du parc de la "98 "Cité, pour les 50 ans et plus. Construction supérieure "99 "avec plancher de béton et ascenseur.")100 if bigen:101 desc = "Modèle bi-génération sur deux étages. " + desc102 listings.append(Listing(103 source=self.source_id,104 external_id=f"saro-{n}.5",105 url=PAGE_URL,106 title=f"{n} ½ Haut de gamme — Constructions Saro"107 + (" (bi-génération)" if bigen else ""),108 address="",109 sector=SECTOR,110 city=CITY,111 unit_type=ut,112 area_sqft=sqft,113 description=desc,114 amenities=list(amenities),115 details={"contact": dict(contact)} if contact else {},116 images=imgs_by_type.get(DIAPO_OF.get(n, ""), [])[:15],117 ))118 return listings119