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/loggia.py : connecteur Loggia Saint-Lambert (loggiasaintlambert.com)5# Trois immeubles locatifs (Loggia 1, 2, 3) sur l'avenue Saint-Charles à6# Saint-Lambert. WordPress rendu serveur : la page /le-projet/ embarque une7# carte interactive (blocs « div.available.bloc.appt-N-demi » libellés8# « 111-$2495. - 4 ½ / Disponible ») et ~260 fiches d'unités (« Appartement9# NNN / Loggia N / ÉTAGE N », p.dispo[.not] = Loué/Disponible/Réservé,10# p.superficie, plan PNG/PDF). On ne retient que les unités « Disponible ».11# Granularité : unité.12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from bs4 import BeautifulSoup1819from .base import BaseConnector20from ..schema import Listing, normalize_unit_type2122BASE = "https://www.loggiasaintlambert.com"23PAGE_URL = f"{BASE}/le-projet/"2425CITY = "Saint-Lambert"26ADDRESS = "avenue Saint-Charles, Saint-Lambert"2728# « Appartement 111-$2495. » (prix parfois accolé au numéro)29TITLE_RE = re.compile(r"Appartement\s+(\d+)\s*(?:[-–]\s*\$\s*([\d\s, ]+))?",30 re.I)31# libellé de bloc carte : « 111-$2495. - 4 ½ »32BLOC_RE = re.compile(r"(\d+)\s*[-–]\s*\$\s*([\d\s, ]+)[.\s]*[-–]\s*"33 r"(\d\s*½|\d\s*1/2|studio)", re.I)34SQFT_RE = re.compile(r"([\d,\s ]+)\s*pi", re.I)35LOGGIA_RE = re.compile(r"Loggia\s*(\d)", re.I)36ETAGE_RE = re.compile(r"[ÉE]TAGE\s*(\d+)", re.I)373839def _num(s: str) -> float | None:40 s = re.sub(r"[\s, ]", "", s or "")41 try:42 return float(s)43 except ValueError:44 return None454647class LoggiaConnector(BaseConnector):48 source_id = "loggia"49 request_delay = 0.65051 def fetch(self) -> list[Listing]:52 listings: list[Listing] = []53 try:54 html = self.get(PAGE_URL).text55 except Exception:56 return listings57 soup = BeautifulSoup(html, "html.parser")5859 # 1) typologie par unité via les blocs de la carte (disponibles seulement)60 # « 111-$2495. - 4 ½ » → {("<no>", "<prix>"): "4½"}61 types: dict[str, str] = {}62 for bloc in soup.select("div.available.bloc"):63 m = BLOC_RE.search(bloc.get_text(" ", strip=True))64 if m:65 types[m.group(1)] = normalize_unit_type(m.group(3))6667 # 2) fiches d'unités : on garde celles dont p.dispo = « Disponible »68 for p in soup.select("p.dispo"):69 try:70 if "not" in (p.get("class") or []):71 continue72 if "disponible" not in p.get_text(strip=True).lower():73 continue # « Réservé » etc.74 card = p.find_parent("div", class_="col-md-8")75 if not card:76 continue77 text = card.get_text(" ", strip=True)78 mt = TITLE_RE.search(text)79 if not mt:80 continue81 unit_no = mt.group(1)82 price = _num(mt.group(2)) if mt.group(2) else None8384 building = ""85 mb = LOGGIA_RE.search(text)86 if mb:87 building = f"Loggia {mb.group(1)}"88 floor = ""89 mf = ETAGE_RE.search(text)90 if mf:91 floor = f"Étage {mf.group(1)}"9293 sqft = None94 sp = card.select_one("p.superficie")95 if sp:96 ms = SQFT_RE.search(sp.get_text(strip=True))97 if ms:98 sqft = _num(ms.group(1))99100 images: list[str] = []101 for img in card.select("div.plan-unite img"):102 src = img.get("src") or ""103 if src.startswith("http"):104 images.append(src)105 details: dict = {}106 a = card.select_one('a[href*=".pdf"]')107 if a and a.get("href"):108 mp = re.search(r"(https://[^\s\"]+\.pdf)", a["href"])109 details["plan_pdf"] = mp.group(1) if mp else a["href"]110 if building:111 details["building"] = building112 if floor:113 details["floor"] = floor114115 unit_type = types.get(unit_no, "")116 bslug = building.lower().replace(" ", "") or "x"117 ext_id = f"{bslug}-{unit_no}"118 if any(l.external_id == ext_id for l in listings):119 continue120 listings.append(Listing(121 source=self.source_id,122 external_id=ext_id,123 url=PAGE_URL,124 title=f"Appartement {unit_no}"125 + (f" ({unit_type})" if unit_type else "")126 + (f" — {building}, Loggia Saint-Lambert" if building127 else " — Loggia Saint-Lambert"),128 address=ADDRESS,129 city=CITY,130 unit_type=unit_type,131 price=price,132 availability="Disponible",133 area_sqft=sqft,134 details=details,135 images=images[:15],136 ))137 except Exception:138 continue139 return listings140