# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/plantation.py : connecteur Domaine de la Plantation # (domainedelaplantation.ca) — Shawinigan, secteur Saint-Georges-de-Champlain # (entre Grand-Mère et le Lac-à-la-Tortue). Nouveau quartier de 4½ neufs de # Construction Michael Massicotte. Site vitrine Pixpa page unique rendu # serveur : section « Prix » avec paires

typologie (« Demi sous-sol # 4 ½ » / « Étages 4 ½ ») +

prix (« 1295$ par mois »), section # « Inclusions » en items, photos Pixpa en data-src (lazy-load). # Granularité = typologie (pas d'inventaire par unité publié). # external_id = typologie en slug — stable. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type, strip_accents from .base import BaseConnector BASE = "https://www.domainedelaplantation.ca" PAGE_URL = f"{BASE}/" ADDRESS = "Saint-Georges-de-Champlain, Shawinigan" PRICE_RE = re.compile(r"([\d\s ]{3,7})\$\s*par\s*mois", re.I) UNIT_RE = re.compile(r"(\d)\s*(?:½|1/2|½)") IMG_RE = re.compile( r"https://px-web-images[\w.-]*\.pixpa\.com/[^\"'\s)]+", re.I) def _slug(text: str) -> str: s = strip_accents(text.lower()) return re.sub(r"[^a-z0-9]+", "-", s).strip("-") def _num(txt: str) -> float | None: n = re.sub(r"[\s ]", "", txt or "") try: return float(n) except ValueError: return None class PlantationConnector(BaseConnector): source_id = "plantation" request_delay = 0.8 def fetch(self) -> list[Listing]: html = self.get(PAGE_URL).text soup = BeautifulSoup(html, "html.parser") # Photos (lazy-load Pixpa) — communes au projet images = [u for u in dict.fromkeys( m.group(0) for el in soup.select("[data-src]") for m in [IMG_RE.match(el["data-src"])] if m)][:12] # Inclusions : items de la section « Inclusions » amenities: list[str] = [] incl = soup.find("h2", string=re.compile(r"Inclusions", re.I)) if incl: sec = incl.parent for _ in range(4): if sec is None: break txt = sec.get_text("\n", strip=True) if len(txt) > 60: break sec = sec.parent if sec is not None: for line in sec.get_text("\n", strip=True).split("\n"): line = re.sub(r"\s+", " ", line).strip() if line and not re.match(r"Inclusions", line, re.I) \ and len(line) < 60 and line not in amenities: amenities.append(line) # Accroche du projet (texte héro) blurb = "" h3 = soup.find("h3", string=re.compile(r"Grand-Mère|Lac-à-la-Tortue")) if h3: blurb = re.sub(r"\s+", " ", h3.get_text(" ", strip=True))[:600] # Section « Prix » : paires h3 (typologie) + h4 (prix) listings: list[Listing] = [] prix_h2 = soup.find("h2", string=re.compile(r"^\s*Prix\s*$", re.I)) if not prix_h2: return listings sec = prix_h2 for _ in range(4): sec = sec.parent if sec is None: return listings if sec.find("h4"): break current_label = "" for el in sec.find_all(["h3", "h4"]): text = re.sub(r"\s+", " ", el.get_text(" ", strip=True)) if el.name == "h3": current_label = text continue pm = PRICE_RE.search(text) if not pm or not current_label: continue unit_type = "" um = UNIT_RE.search(current_label) if um: unit_type = normalize_unit_type(f"{um.group(1)} ½") listings.append(Listing( source=self.source_id, external_id=_slug(current_label), url=PAGE_URL, title=f"{current_label} — Domaine de la Plantation", address=ADDRESS, sector="Saint-Georges-de-Champlain", city="Shawinigan", unit_type=unit_type, price=_num(pm.group(1)), price_label=text, description=blurb, amenities=list(amenities), images=list(images), )) current_label = "" return listings