SPB Git

spb/lou-ka Public

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

HTML 99.7%
4.6 KB · 114 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/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