# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/saro.py : connecteur Les Constructions Saro (constructionsaro.com) # Immeubles locatifs haut de gamme à Saint-Hubert (Longueuil, Rive-Sud), # clientèle 50 ans et plus, secteur du parc de la Cité. Site ASP.NET # (ProducNet) une seule page rendue serveur : sections par modèle # (« 3 ½ Haut de gamme — 850 pi² », 4 ½, 5 ½, 7 ½ bi-génération) avec # diaporamas d'images par typologie. Aucun prix ni disponibilité publiés. # Granularité : une annonce par typologie/modèle. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type from .base import BaseConnector BASE = "https://constructionsaro.com" PAGE_URL = f"{BASE}/Default.aspx" CITY = "Longueuil" SECTOR = "Saint-Hubert" # « 3 ½ Haut de gamme » suivi de « 850 pi2 … » (texte aplati de la section) MODEL_RE = re.compile( r"(\d)\s*½\s*Haut de gamme\s*(?:Bi-g[ée]n[ée]ration[\w\s]{0,20}?)?" r"([\d\s]{3,6})\s*pi", re.I) DIAPO_IMG_RE = re.compile( r'href="(Images/Diaporama([A-Za-z0-9]+)/[^"]+\.(?:jpe?g|png))"', re.I) PHONE_RE = re.compile(r"(\d{3})[.\-](\d{3})[.\-](\d{4})") # suffixe du dossier diaporama par typologie (3 -> Diaporama3etDemi…) DIAPO_OF = {"3": "3etDemi", "4": "4etDemi", "5": "5etDemi", "7": "7etDemi"} class SaroConnector(BaseConnector): source_id = "saro" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: list[Listing] = [] try: html = self.get(PAGE_URL).text except Exception: return listings soup = BeautifulSoup(html, "html.parser") for tag in soup(["script", "style", "noscript"]): tag.decompose() text = re.sub(r"\s+", " ", soup.get_text(" ", strip=True)) # commodités : liste « star » de l'immeuble (éléments h5 concaténés) amenities: list[str] = [] for kw, label in [ (r"aire ouverte avec très grande salle de bain", "Condos à aire ouverte, très grande salle de bain"), (r"walk-in", "Garde-robe walk-in"), (r"insonorisation sup[ée]rieur", "Insonorisation supérieure"), (r"Cam[ée]ra de surveillance", "Caméra de surveillance et entrée contrôlée"), (r"Plancher en lattes", "Plancher en lattes et céramique"), (r"Air climatis[ée]", "Air climatisé"), (r"Salle de lavage ind[ée]pendante", "Salle de lavage indépendante"), (r"rangement int[ée]rieure?", "Rangement intérieur"), (r"Cuisine avec ilo?t", "Cuisine avec îlot"), (r"Porte patio", "Porte patio"), (r"Grand balcon terrasse", "Grand balcon terrasse"), (r"ascenseur", "Ascenseur"), (r"50 ans et plus", "Pour les 50 ans et plus"), ]: if re.search(kw, text, re.I): amenities.append(label) contact: dict = {} m = PHONE_RE.search(text) if m: contact["phone"] = f"{m.group(1)}-{m.group(2)}-{m.group(3)}" # images par diaporama de typologie imgs_by_type: dict[str, list[str]] = {} for path, diapo in DIAPO_IMG_RE.findall(html): imgs_by_type.setdefault(diapo, []).append(f"{BASE}/{path}") seen: set[str] = set() for n, area in MODEL_RE.findall(text): if n in seen: continue seen.add(n) ut = normalize_unit_type(f"{n}½") try: sqft = float(area.replace(" ", "")) except ValueError: sqft = None bigen = (n == "7") desc = ("Immeuble locatif neuf haut de gamme des Constructions " "Saro à Saint-Hubert (Longueuil), secteur du parc de la " "Cité, pour les 50 ans et plus. Construction supérieure " "avec plancher de béton et ascenseur.") if bigen: desc = "Modèle bi-génération sur deux étages. " + desc listings.append(Listing( source=self.source_id, external_id=f"saro-{n}.5", url=PAGE_URL, title=f"{n} ½ Haut de gamme — Constructions Saro" + (" (bi-génération)" if bigen else ""), address="", sector=SECTOR, city=CITY, unit_type=ut, area_sqft=sqft, description=desc, amenities=list(amenities), details={"contact": dict(contact)} if contact else {}, images=imgs_by_type.get(DIAPO_OF.get(n, ""), [])[:15], )) return listings