# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/capital_rdr.py : connecteur Gestion Capital RDR (gestionrdr.com) # 545 appartements à Trois-Rivières / Cap-de-la-Madeleine (fusionnés dans la # ville de Trois-Rivières). Site GoDaddy Website Builder : la page # « Logements à louer » est un widget « menu » dont chaque item porte un # data-aid stable (MENU_ITEM_) avec titre (« 4½ au 430 # Saint-Maurice »), prix (« 990.00$ »), photo et description en paragraphes # (« *Disponible immédiatement!* », adresse civique, inclusions, animaux…). # Pas de fiche individuelle : l'URL renvoie à la page + ancre de l'item. # robots.txt permissif (sitemap seulement). # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing from .base import BaseConnector BASE = "https://gestionrdr.com" LIST_URL = f"{BASE}/logements-%C3%A0-louer-1" # ligne d'adresse civique dans la description (« 430 rue St-Maurice App9 ») _ADDR_RE = re.compile( r"^\d[\dA-Za-z\-]*\s+(?:rue|av(?:enue)?\.?|boul(?:evard)?\.?|bd|chemin|" r"ch\.|côte|place|route|rang|montée)\b.*", re.I) # type d'unité au début du titre (« 4½ au 430 Saint-Maurice ») _TYPE_RE = re.compile(r"^\s*(\d\s*(?:½|1/2)|studio|loft|chambre)", re.I) class CapitalRDRConnector(BaseConnector): source_id = "capital_rdr" request_delay = 0.7 def fetch(self) -> list[Listing]: html = self.get(LIST_URL).text soup = BeautifulSoup(html, "html.parser") listings: list[Listing] = [] seen: set[str] = set() for item in soup.select('[data-aid^="MENU_ITEM_"]'): ext_id = item.get("data-aid", "")[len("MENU_ITEM_"):].strip() # MENU_ITEM_GRID_ = conteneur de grille, pas un item if not ext_id or ext_id.startswith("GRID") or ext_id in seen: continue seen.add(ext_id) title_el = item.select_one('[data-aid*="_TITLE"]') price_el = item.select_one('[data-aid*="_PRICE"]') desc_el = item.select_one('[data-aid*="_DESC"]') img_el = item.select_one('img[data-aid*="_IMAGE"]') title = title_el.get_text(" ", strip=True) if title_el else "" if not title: continue price_label = price_el.get_text(" ", strip=True) if price_el else "" # description : paragraphes bruts du widget lines: list[str] = [] if desc_el: for p in desc_el.find_all("p"): t = re.sub(r"\s+", " ", p.get_text(" ", strip=True)) t = t.replace(" ", " ").strip() if t: lines.append(t) # disponibilité : ligne « *Disponible …* » (texte source) availability = "" for t in lines: if re.search(r"disponible", t, re.I): availability = t.strip("*").strip() break # adresse : ligne civique de la description, sinon partie du titre address = "" for t in lines: if _ADDR_RE.match(t): address = t break if not address: m = re.search(r"\bau\s+(\d.*)$", title) if m: address = m.group(1).strip() m = _TYPE_RE.match(title) unit_type = m.group(1) if m else "" images = [] if img_el: src = img_el.get("src") or "" if src.startswith("//"): src = "https:" + src if src.startswith("http"): images.append(src) listings.append(Listing( source=self.source_id, external_id=ext_id, # dérivé du titre par le builder url=f"{LIST_URL}#{ext_id}", title=title, address=address, sector="", # tout le parc RDR est à Trois-Rivières (incl. l'ancien # Cap-de-la-Madeleine, fusionné en 2002) city="Trois-Rivières", unit_type=unit_type, price_label=price_label, availability=availability, description="\n".join(lines)[:900], images=images, )) return listings