SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
5.4 KB · 133 lines python
Raw Blame History
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