# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/louis_alexandre.py : connecteur Le Louis-Alexandre # (condoslouisalexandre.ca — Longueuil, 2100, rue René, près du # Vieux-Longueuil). WordPress + Elementor rendu serveur : la page # « Appartements disponibles » liste chaque unité en blocs Elementor # « Condo #NNN | Superficie de X pi² | n chambres, n sdb | PRIX $ / mois » # avec image du plan et fiche PDF par unité. Le numéro d'unité (#NNN) # sert d'identifiant stable. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing from .base import BaseConnector BASE = "https://condoslouisalexandre.ca" LIST_URL = f"{BASE}/appartements-disponibles-longueuil/" ADDRESS = "2100, rue René, Longueuil" CITY = "Longueuil" UNIT_RE = re.compile(r"^Condo\s*#(\d+)\s*$") SQFT_RE = re.compile(r"Superficie\s+de\s+([\d\s]+)\s*pi", re.I) ROOMS_RE = re.compile(r"(Studio|\d\s*chambres?)\s*,\s*(\d)\s*sdb", re.I) PRICE_RE = re.compile(r"(\d[\d\s]{2,6})\s*\$\s*/\s*mois", re.I) IMG_RE = re.compile( r"https://condoslouisalexandre\.ca/wp-content/uploads/" r"[^\"\s\\']+\.(?:jpe?g|png|webp)", re.I) SKIP_IMG_RE = re.compile(r"logo|icon|favicon|-\d{2,3}x\d{2,3}\.", re.I) class LouisAlexandreConnector(BaseConnector): source_id = "louis_alexandre" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: list[Listing] = [] try: html = self.get(LIST_URL).text except Exception: return listings soup = BeautifulSoup(html, "html.parser") # contact de l'immeuble (mailto:/tel: structurés dans la page) contact: dict = {} m = re.search(r'href="mailto:([^"?]+)"', html) if m: contact["email"] = m.group(1).strip().lower() m = re.search(r'href="tel:\+?1?[\s.\-]?(\d{3})[\s.\-]?(\d{3})' r'[\s.\-]?(\d{4})"', html) if m: contact["phone"] = f"{m.group(1)}-{m.group(2)}-{m.group(3)}" seen: set[str] = set() for node in soup.find_all(string=UNIT_RE): try: unit_no = UNIT_RE.match(node.strip()).group(1) if unit_no in seen: continue # bloc de l'unité : on remonte jusqu'au conteneur Elementor # qui porte le prix (texte « NNN $ / mois ») block = node.parent while block is not None and "$" not in block.get_text(): block = block.parent if block is None: continue text = block.get_text(" | ", strip=True) if len(re.findall(r"Condo\s*#", text)) != 1: continue # conteneur trop large (plusieurs unités) seen.add(unit_no) price = None price_label = "" m = PRICE_RE.search(text) if m: price_label = m.group(0) try: val = float(m.group(1).replace(" ", "") .replace(" ", "").replace("\xa0", "")) if 300 <= val <= 20000: price = val except ValueError: pass area = None m = SQFT_RE.search(text) if m: try: area = float(m.group(1).replace(" ", "") .replace("\xa0", "")) except ValueError: area = None unit_type = "" bedrooms = None bathrooms = None m = ROOMS_RE.search(text) if m: rooms = m.group(1) bathrooms = float(m.group(2)) if re.match(r"studio", rooms, re.I): unit_type = "Studio" bedrooms = 0.0 else: n = int(re.match(r"\d+", rooms).group(0)) bedrooms = float(n) unit_type = f"{n + 2}½" # image du plan + fiche PDF : conteneur parent (colonne voisine) images: list[str] = [] pdf = "" wrap = block.parent while wrap is not None and not wrap.find("img"): wrap = wrap.parent if wrap is not None and \ len(re.findall(r"Condo\s*#", wrap.get_text())) == 1: for u in dict.fromkeys(IMG_RE.findall(str(wrap))): if not SKIP_IMG_RE.search(u): images.append(u) a = wrap.find("a", href=re.compile(r"\.pdf$", re.I)) if a: pdf = a.get("href", "") details: dict = {} if contact: details["contact"] = dict(contact) if pdf: details["plan_pdf"] = pdf amenities = [] if area: amenities.append(f"Superficie de {int(area)} pi²") if bathrooms: amenities.append(f"{int(bathrooms)} salle(s) de bain") listings.append(Listing( source=self.source_id, external_id=f"condo-{unit_no}", url=LIST_URL, title=f"Condo #{unit_no} — Le Louis-Alexandre", address=ADDRESS, city=CITY, unit_type=unit_type, bedrooms=bedrooms, bathrooms=bathrooms, price=price, price_label=price_label, area_sqft=area, amenities=amenities, details=details, images=images[:10], )) except Exception: continue return listings