# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/logement_mauricie.py : connecteur Logement Mauricie # (logementmauricie.com) — regroupement de propriétaires (+200 logements). # Builder type WebSelf : 4 pages secteur (Trois-Rivières, Cap-de-la- # Madeleine, Shawinigan, Shawinigan-Sud), chaque immeuble = un widget # texte (id GUID stable) avec champs libellés « Type / Actuellement # disponible / Date / Chauffé éclairé / Combien $ / Contact… ». On ne # retient que les immeubles avec une disponibilité réelle (≠ Complet). # ----------------------------------------------------------------------------- from __future__ import annotations import re from urllib.parse import urljoin from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type, parse_price from .base import BaseConnector BASE = "http://logementmauricie.com" # pages secteur -> (ville réelle, secteur) ; Cap-de-la-Madeleine et # Shawinigan-Sud sont des secteurs des villes fusionnées (2002) PAGES = [ ("trois-rivieres", "Trois-Rivières", ""), ("cap-de-la-madeleine", "Trois-Rivières", "Cap-de-la-Madeleine"), ("shawinigan", "Shawinigan", ""), ("shawinigan-sud", "Shawinigan", "Shawinigan-Sud"), ] # libellés des champs du gabarit d'immeuble _LABELS = re.compile( r"^(Type|Actuellement disponible|Date|Chauff[ée] [ée]clair[ée]|" r"Commodit[ée]s?|Entr[ée]e laveuse et s[ée]cheuse|Stationnements?|" r"Plus|Combien \$|Contact)\s*:?\s*$", re.I) def _parse_fields(lines: list[str]) -> dict[str, str]: """Paires libellé -> valeur (les valeurs suivent le libellé, ligne(s) suivante(s) jusqu'au prochain libellé).""" fields: dict[str, list[str]] = {} cur: str | None = None for ln in lines: m = _LABELS.match(ln) if m: cur = m.group(1).lower() fields.setdefault(cur, []) elif cur is not None: fields[cur].append(ln) return {k: " ".join(v).strip() for k, v in fields.items()} class LogementMauricieConnector(BaseConnector): source_id = "logement_mauricie" request_delay = 0.8 max_pages = 4 # 4 pages secteur, pas de pagination def fetch(self) -> list[Listing]: listings: list[Listing] = [] for slug, city, sector in PAGES[:self.max_pages]: url = f"{BASE}/{slug}/" try: html = self.get(url).text except Exception: continue soup = BeautifulSoup(html, "html.parser") for w in soup.select("div.widget.widget-text[id]"): try: lst = self._parse_widget(w, url, city, sector) except Exception: continue if lst is not None: listings.append(lst) return listings def _parse_widget(self, w, page_url: str, city: str, sector: str) -> Listing | None: txt = w.get_text("\n", strip=True) if "Actuellement disponible" not in txt: return None # widget décoratif (intro, bandeau…) lines = [re.sub(r"\s+", " ", ln).strip() for ln in txt.split("\n") if ln.strip()] fields = _parse_fields(lines) # disponibilité réelle seulement : « Complet », vide ou « - » = ignoré dispo = fields.get("actuellement disponible", "").strip(" - ") if not dispo or re.search(r"^complet", dispo, re.I): return None if re.search(r"commercial|stationnement seulement|local\b", dispo, re.I): return None h2 = w.find("h2") address = h2.get_text(" ", strip=True) if h2 else "" if not address or not re.match(r"\d", address): return None # bloc sans adresse civique = pas un immeuble ext_id = w.get("id", "") # GUID du widget, stable dans le builder if not ext_id: return None date_txt = fields.get("date", "").strip(" - ") price_label = fields.get("combien $", "").strip(" - ") # description : bloc de champs + paragraphe descriptif du secteur desc_lines = [] started = False for ln in lines: if ln == address: started = True continue if started and ln.lower() not in ("revenir en haut",): desc_lines.append(ln) images: list[str] = [] for img in w.find_all("img", src=True): src = urljoin(page_url, img["src"]) if re.search(r"google-maps|icon|logo", src, re.I): continue if src.startswith("http") and src not in images: images.append(src) return Listing( source=self.source_id, external_id=ext_id, url=f"{page_url}#{ext_id}", # pas de fiche individuelle title=f"{dispo} — {address}", address=address, sector=sector, city=city, unit_type=normalize_unit_type(dispo), price=parse_price(price_label), price_label=price_label, availability=date_txt, description="\n".join(desc_lines)[:900], images=images[:10], )