# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/cabinetterie.py : connecteur Place de la Cabinetterie # (lacabinetterie.ca) — condos locatifs haut de gamme à # Saint-Jean-sur-Richelieu (200-210, rue Saint-Paul et 271-277, rue de la # Cabinetterie). WordPress + Avada rendu serveur : la page d'accueil liste # 3 modèles (« Confortable 3½ » 1 ch/972 pi², « Spacieux 4½ » 2 ch/1247 pi², # « Grand 5½ » 3 ch/1418 pi²) avec un prix plancher global « À partir de # 1960$/mois » (rattaché au 3½ seulement, le plus petit modèle). Une # annonce par modèle ; la page de chaque modèle fournit l'URL de fiche et # le plan. Services inclus : électricité, chauffage, Internet, télé. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type from .base import BaseConnector BASE = "https://lacabinetterie.ca" LIST_URL = f"{BASE}/" ADDRESS = "200-210, rue Saint-Paul, Saint-Jean-sur-Richelieu" ADDRESS2 = "271-277, rue de la Cabinetterie, Saint-Jean-sur-Richelieu" CITY = "Saint-Jean-sur-Richelieu" PHONE = "514-497-3790" # modèles : nom affiché sur la home -> spécifications extraites du texte MODEL_RE = re.compile( r"(Confortable 3½|Spacieux 4½|Grand 5½)\s+Chambres\s+Chambres\s+(\d)\s+" r"Salles de bain\s+Salles de bain\s+([\d½]+)\s+" r"Superficie\s+Superficie\s+([\d\s]{3,6})\s*pi²") BASE_PRICE_RE = re.compile(r"À partir de\s*([\d\s ]{3,6})\s*\$/?\s*mois", re.I) SERVICES_RE = re.compile(r"Services inclus\s*:\s*([^.]+)\.") IMG_RE = re.compile( r"https://lacabinetterie\.ca/wp-content/uploads/[^\"\s\\']+" r"\.(?:jpe?g|png|webp)", re.I) SKIP_IMG_RE = re.compile( r"logo|icon|favicon|cropped|capture|[Pp]lan|-\d{2,4}x\d{2,4}\.", re.I) class CabinetterieConnector(BaseConnector): source_id = "cabinetterie" 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") text = re.sub(r"\s+", " ", soup.get_text(" ", strip=True)) # prix plancher global (le plus petit modèle : 3½) base_price = None base_label = "" m = BASE_PRICE_RE.search(text) if m: base_label = m.group(0) try: base_price = float(re.sub(r"[^\d]", "", m.group(1))) except ValueError: base_price = None # services inclus + caractéristiques de l'immeuble amenities: list[str] = [] m = SERVICES_RE.search(text) if m: amenities = [f"{s.strip().capitalize()} incluse" if s.strip() == "électricité" else f"{s.strip().capitalize()} inclus" for s in re.split(r",| et ", m.group(1)) if s.strip()] building_feats = [] for ul in soup.find_all("ul"): lis = [li.get_text(" ", strip=True) for li in ul.find_all("li")] if lis and any("ascenseur" in x.lower() or "Piscine" in x for x in lis): building_feats = lis[:15] break # liens vers les pages modèles (nav « Modèles disponibles ») model_urls: dict[str, str] = {} for a in soup.find_all("a", href=True): t = a.get_text(" ", strip=True) if re.fullmatch(r"[345]½", t) and BASE in a["href"]: model_urls.setdefault(t, a["href"]) images = [u for u in dict.fromkeys(IMG_RE.findall(html)) if not SKIP_IMG_RE.search(u)][:10] seen: set[str] = set() for m in MODEL_RE.finditer(text): try: name, beds, baths, sqft = m.groups() unit_type = normalize_unit_type( re.search(r"\d½", name).group(0)) ext_id = unit_type.replace("½", ".5") if ext_id in seen: continue seen.add(ext_id) bathrooms = 1.5 if "½" in baths else float(baths) area = float(re.sub(r"[^\d]", "", sqft)) is_smallest = unit_type == "3½" listings.append(Listing( source=self.source_id, external_id=ext_id, url=model_urls.get(unit_type, LIST_URL), title=f"{name} — Place de la Cabinetterie", address=ADDRESS, city=CITY, unit_type=unit_type, bedrooms=float(beds), bathrooms=bathrooms, price=base_price if is_smallest else None, price_label=base_label if is_smallest else "", area_sqft=area, amenities=amenities + building_feats, details={"contact": {"phone": PHONE}, "adresse_2": ADDRESS2}, images=images, )) except Exception: continue return listings