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/cabinetterie.py : connecteur Place de la Cabinetterie5# (lacabinetterie.ca) — condos locatifs haut de gamme à6# Saint-Jean-sur-Richelieu (200-210, rue Saint-Paul et 271-277, rue de la7# Cabinetterie). WordPress + Avada rendu serveur : la page d'accueil liste8# 3 modèles (« Confortable 3½ » 1 ch/972 pi², « Spacieux 4½ » 2 ch/1247 pi²,9# « Grand 5½ » 3 ch/1418 pi²) avec un prix plancher global « À partir de10# 1960$/mois » (rattaché au 3½ seulement, le plus petit modèle). Une11# annonce par modèle ; la page de chaque modèle fournit l'URL de fiche et12# le plan. Services inclus : électricité, chauffage, Internet, télé.13# -----------------------------------------------------------------------------14from __future__ import annotations1516import re1718from bs4 import BeautifulSoup1920from ..schema import Listing, normalize_unit_type21from .base import BaseConnector2223BASE = "https://lacabinetterie.ca"24LIST_URL = f"{BASE}/"2526ADDRESS = "200-210, rue Saint-Paul, Saint-Jean-sur-Richelieu"27ADDRESS2 = "271-277, rue de la Cabinetterie, Saint-Jean-sur-Richelieu"28CITY = "Saint-Jean-sur-Richelieu"29PHONE = "514-497-3790"3031# modèles : nom affiché sur la home -> spécifications extraites du texte32MODEL_RE = re.compile(33 r"(Confortable 3½|Spacieux 4½|Grand 5½)\s+Chambres\s+Chambres\s+(\d)\s+"34 r"Salles de bain\s+Salles de bain\s+([\d½]+)\s+"35 r"Superficie\s+Superficie\s+([\d\s]{3,6})\s*pi²")36BASE_PRICE_RE = re.compile(r"À partir de\s*([\d\s ]{3,6})\s*\$/?\s*mois",37 re.I)38SERVICES_RE = re.compile(r"Services inclus\s*:\s*([^.]+)\.")39IMG_RE = re.compile(40 r"https://lacabinetterie\.ca/wp-content/uploads/[^\"\s\\']+"41 r"\.(?:jpe?g|png|webp)", re.I)42SKIP_IMG_RE = re.compile(43 r"logo|icon|favicon|cropped|capture|[Pp]lan|-\d{2,4}x\d{2,4}\.", re.I)444546class CabinetterieConnector(BaseConnector):47 source_id = "cabinetterie"48 request_delay = 0.64950 def fetch(self) -> list[Listing]:51 listings: list[Listing] = []52 try:53 html = self.get(LIST_URL).text54 except Exception:55 return listings56 soup = BeautifulSoup(html, "html.parser")57 text = re.sub(r"\s+", " ", soup.get_text(" ", strip=True))5859 # prix plancher global (le plus petit modèle : 3½)60 base_price = None61 base_label = ""62 m = BASE_PRICE_RE.search(text)63 if m:64 base_label = m.group(0)65 try:66 base_price = float(re.sub(r"[^\d]", "", m.group(1)))67 except ValueError:68 base_price = None6970 # services inclus + caractéristiques de l'immeuble71 amenities: list[str] = []72 m = SERVICES_RE.search(text)73 if m:74 amenities = [f"{s.strip().capitalize()} incluse"75 if s.strip() == "électricité" else76 f"{s.strip().capitalize()} inclus"77 for s in re.split(r",| et ", m.group(1))78 if s.strip()]79 building_feats = []80 for ul in soup.find_all("ul"):81 lis = [li.get_text(" ", strip=True) for li in ul.find_all("li")]82 if lis and any("ascenseur" in x.lower() or "Piscine" in x83 for x in lis):84 building_feats = lis[:15]85 break8687 # liens vers les pages modèles (nav « Modèles disponibles »)88 model_urls: dict[str, str] = {}89 for a in soup.find_all("a", href=True):90 t = a.get_text(" ", strip=True)91 if re.fullmatch(r"[345]½", t) and BASE in a["href"]:92 model_urls.setdefault(t, a["href"])9394 images = [u for u in dict.fromkeys(IMG_RE.findall(html))95 if not SKIP_IMG_RE.search(u)][:10]9697 seen: set[str] = set()98 for m in MODEL_RE.finditer(text):99 try:100 name, beds, baths, sqft = m.groups()101 unit_type = normalize_unit_type(102 re.search(r"\d½", name).group(0))103 ext_id = unit_type.replace("½", ".5")104 if ext_id in seen:105 continue106 seen.add(ext_id)107108 bathrooms = 1.5 if "½" in baths else float(baths)109 area = float(re.sub(r"[^\d]", "", sqft))110 is_smallest = unit_type == "3½"111112 listings.append(Listing(113 source=self.source_id,114 external_id=ext_id,115 url=model_urls.get(unit_type, LIST_URL),116 title=f"{name} — Place de la Cabinetterie",117 address=ADDRESS,118 city=CITY,119 unit_type=unit_type,120 bedrooms=float(beds),121 bathrooms=bathrooms,122 price=base_price if is_smallest else None,123 price_label=base_label if is_smallest else "",124 area_sqft=area,125 amenities=amenities + building_feats,126 details={"contact": {"phone": PHONE},127 "adresse_2": ADDRESS2},128 images=images,129 ))130 except Exception:131 continue132 return listings133