# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/st_charles_granby.py : connecteur Le St-Charles (stcharlesgranby.com) # Condos locatifs modernes au 41, rue Saint-Charles Nord à Granby (studios, # 3½, 4½, 5½). WordPress/Elementor rendu serveur : la page d'accueil affiche # une carte par typologie (« 3 1/2 / À partir de 900$ / mois* »). # Granularité : typologie (pas d'unités individuelles publiées). # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from .base import BaseConnector from ..schema import Listing, normalize_unit_type BASE = "https://www.stcharlesgranby.com" ADDRESS = "41, rue Saint-Charles Nord, Granby" CITY = "Granby" TYPE_RE = re.compile(r"^(studio|\d\s*(?:1/2|½))$", re.I) PRICE_RE = re.compile(r"À\s*partir\s*de\s*(\d[\d\s ]{2,6})\$\s*/\s*mois", re.I) IMG_RE = re.compile(r'https://www\.stcharlesgranby\.com/wp-content/uploads/' r'[^"\s\\)]+\.(?:jpe?g|png|webp)', re.I) class StCharlesGranbyConnector(BaseConnector): source_id = "st_charles_granby" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: list[Listing] = [] try: html = self.get(BASE + "/").text except Exception: return listings soup = BeautifulSoup(html, "html.parser") lines = soup.get_text("\n", strip=True).split("\n") images = [u for u in dict.fromkeys(IMG_RE.findall(html)) if not re.search(r"logo|icon|favicon|plan|" r"-\d{2,4}x\d{2,4}\.", u, re.I)][:15] # cartes « Studio / À partir de 900$ / mois* » (le menu répète les # typologies sans prix : on n'accepte que type suivi d'un prix) for i, line in enumerate(lines): raw = line.strip() if not TYPE_RE.match(raw): continue nxt = lines[i + 1].strip() if i + 1 < len(lines) else "" if TYPE_RE.match(nxt): continue # menu de navigation (types à la suite) m = PRICE_RE.search(nxt) if not m: continue unit_type = ("Studio" if raw.lower() == "studio" else normalize_unit_type(raw)) try: price = float(m.group(1).replace(" ", "").replace(" ", "")) except ValueError: continue ext_id = ("granby-studio" if unit_type == "Studio" else f"granby-{unit_type.replace('½', '.5')}") if any(l.external_id == ext_id for l in listings): continue listings.append(Listing( source=self.source_id, external_id=ext_id, url=BASE + "/", title=f"{unit_type} — Le St-Charles", address=ADDRESS, city=CITY, unit_type=unit_type, price=price, price_label=f"À partir de {m.group(1).strip()}$ / mois", description="Condos locatifs modernes, lumineux et bien " "pensés au cœur de Granby (studios, 3½, 4½ " "et 5½), à distance de marche des services.", images=images, )) return listings