spb/lou-ka Public
Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 99.7%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/capital_rdr.py : connecteur Gestion Capital RDR (gestionrdr.com)5# 545 appartements à Trois-Rivières / Cap-de-la-Madeleine (fusionnés dans la6# ville de Trois-Rivières). Site GoDaddy Website Builder : la page7# « Logements à louer » est un widget « menu » dont chaque item porte un8# data-aid stable (MENU_ITEM_<hash du titre>) avec titre (« 4½ au 4309# Saint-Maurice »), prix (« 990.00$ »), photo et description en paragraphes10# (« *Disponible immédiatement!* », adresse civique, inclusions, animaux…).11# Pas de fiche individuelle : l'URL renvoie à la page + ancre de l'item.12# robots.txt permissif (sitemap seulement).13# -----------------------------------------------------------------------------14from __future__ import annotations1516import re1718from bs4 import BeautifulSoup1920from ..schema import Listing21from .base import BaseConnector2223BASE = "https://gestionrdr.com"24LIST_URL = f"{BASE}/logements-%C3%A0-louer-1"2526# ligne d'adresse civique dans la description (« 430 rue St-Maurice App9 »)27_ADDR_RE = re.compile(28 r"^\d[\dA-Za-z\-]*\s+(?:rue|av(?:enue)?\.?|boul(?:evard)?\.?|bd|chemin|"29 r"ch\.|côte|place|route|rang|montée)\b.*", re.I)30# type d'unité au début du titre (« 4½ au 430 Saint-Maurice »)31_TYPE_RE = re.compile(r"^\s*(\d\s*(?:½|1/2)|studio|loft|chambre)", re.I)323334class CapitalRDRConnector(BaseConnector):35 source_id = "capital_rdr"36 request_delay = 0.73738 def fetch(self) -> list[Listing]:39 html = self.get(LIST_URL).text40 soup = BeautifulSoup(html, "html.parser")4142 listings: list[Listing] = []43 seen: set[str] = set()44 for item in soup.select('[data-aid^="MENU_ITEM_"]'):45 ext_id = item.get("data-aid", "")[len("MENU_ITEM_"):].strip()46 # MENU_ITEM_GRID_<n> = conteneur de grille, pas un item47 if not ext_id or ext_id.startswith("GRID") or ext_id in seen:48 continue49 seen.add(ext_id)50 title_el = item.select_one('[data-aid*="_TITLE"]')51 price_el = item.select_one('[data-aid*="_PRICE"]')52 desc_el = item.select_one('[data-aid*="_DESC"]')53 img_el = item.select_one('img[data-aid*="_IMAGE"]')54 title = title_el.get_text(" ", strip=True) if title_el else ""55 if not title:56 continue57 price_label = price_el.get_text(" ", strip=True) if price_el else ""5859 # description : paragraphes bruts du widget60 lines: list[str] = []61 if desc_el:62 for p in desc_el.find_all("p"):63 t = re.sub(r"\s+", " ", p.get_text(" ", strip=True))64 t = t.replace(" ", " ").strip()65 if t:66 lines.append(t)6768 # disponibilité : ligne « *Disponible …* » (texte source)69 availability = ""70 for t in lines:71 if re.search(r"disponible", t, re.I):72 availability = t.strip("*").strip()73 break7475 # adresse : ligne civique de la description, sinon partie du titre76 address = ""77 for t in lines:78 if _ADDR_RE.match(t):79 address = t80 break81 if not address:82 m = re.search(r"\bau\s+(\d.*)$", title)83 if m:84 address = m.group(1).strip()8586 m = _TYPE_RE.match(title)87 unit_type = m.group(1) if m else ""8889 images = []90 if img_el:91 src = img_el.get("src") or ""92 if src.startswith("//"):93 src = "https:" + src94 if src.startswith("http"):95 images.append(src)9697 listings.append(Listing(98 source=self.source_id,99 external_id=ext_id, # dérivé du titre par le builder100 url=f"{LIST_URL}#{ext_id}",101 title=title,102 address=address,103 sector="",104 # tout le parc RDR est à Trois-Rivières (incl. l'ancien105 # Cap-de-la-Madeleine, fusionné en 2002)106 city="Trois-Rivières",107 unit_type=unit_type,108 price_label=price_label,109 availability=availability,110 description="\n".join(lines)[:900],111 images=images,112 ))113 return listings114