Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/plantation.py : connecteur Domaine de la Plantation5# (domainedelaplantation.ca) — Shawinigan, secteur Saint-Georges-de-Champlain6# (entre Grand-Mère et le Lac-à-la-Tortue). Nouveau quartier de 4½ neufs de7# Construction Michael Massicotte. Site vitrine Pixpa page unique rendu8# serveur : section « Prix » avec paires <h3> typologie (« Demi sous-sol9# 4 ½ » / « Étages 4 ½ ») + <h4> prix (« 1295$ par mois »), section10# « Inclusions » en items, photos Pixpa en data-src (lazy-load).11# Granularité = typologie (pas d'inventaire par unité publié).12# external_id = typologie en slug — stable.13# -----------------------------------------------------------------------------14from __future__ import annotations1516import re1718from bs4 import BeautifulSoup1920from ..schema import Listing, normalize_unit_type, strip_accents21from .base import BaseConnector2223BASE = "https://www.domainedelaplantation.ca"24PAGE_URL = f"{BASE}/"2526ADDRESS = "Saint-Georges-de-Champlain, Shawinigan"2728PRICE_RE = re.compile(r"([\d\s ]{3,7})\$\s*par\s*mois", re.I)29UNIT_RE = re.compile(r"(\d)\s*(?:½|1/2|½)")30IMG_RE = re.compile(31 r"https://px-web-images[\w.-]*\.pixpa\.com/[^\"'\s)]+", re.I)323334def _slug(text: str) -> str:35 s = strip_accents(text.lower())36 return re.sub(r"[^a-z0-9]+", "-", s).strip("-")373839def _num(txt: str) -> float | None:40 n = re.sub(r"[\s ]", "", txt or "")41 try:42 return float(n)43 except ValueError:44 return None454647class PlantationConnector(BaseConnector):48 source_id = "plantation"49 request_delay = 0.85051 def fetch(self) -> list[Listing]:52 html = self.get(PAGE_URL).text53 soup = BeautifulSoup(html, "html.parser")5455 # Photos (lazy-load Pixpa) — communes au projet56 images = [u for u in dict.fromkeys(57 m.group(0) for el in soup.select("[data-src]")58 for m in [IMG_RE.match(el["data-src"])] if m)][:12]5960 # Inclusions : items de la section « Inclusions »61 amenities: list[str] = []62 incl = soup.find("h2", string=re.compile(r"Inclusions", re.I))63 if incl:64 sec = incl.parent65 for _ in range(4):66 if sec is None:67 break68 txt = sec.get_text("\n", strip=True)69 if len(txt) > 60:70 break71 sec = sec.parent72 if sec is not None:73 for line in sec.get_text("\n", strip=True).split("\n"):74 line = re.sub(r"\s+", " ", line).strip()75 if line and not re.match(r"Inclusions", line, re.I) \76 and len(line) < 60 and line not in amenities:77 amenities.append(line)7879 # Accroche du projet (texte héro)80 blurb = ""81 h3 = soup.find("h3", string=re.compile(r"Grand-Mère|Lac-à-la-Tortue"))82 if h3:83 blurb = re.sub(r"\s+", " ", h3.get_text(" ", strip=True))[:600]8485 # Section « Prix » : paires h3 (typologie) + h4 (prix)86 listings: list[Listing] = []87 prix_h2 = soup.find("h2", string=re.compile(r"^\s*Prix\s*$", re.I))88 if not prix_h2:89 return listings90 sec = prix_h291 for _ in range(4):92 sec = sec.parent93 if sec is None:94 return listings95 if sec.find("h4"):96 break9798 current_label = ""99 for el in sec.find_all(["h3", "h4"]):100 text = re.sub(r"\s+", " ", el.get_text(" ", strip=True))101 if el.name == "h3":102 current_label = text103 continue104 pm = PRICE_RE.search(text)105 if not pm or not current_label:106 continue107 unit_type = ""108 um = UNIT_RE.search(current_label)109 if um:110 unit_type = normalize_unit_type(f"{um.group(1)} ½")111 listings.append(Listing(112 source=self.source_id,113 external_id=_slug(current_label),114 url=PAGE_URL,115 title=f"{current_label} — Domaine de la Plantation",116 address=ADDRESS,117 sector="Saint-Georges-de-Champlain",118 city="Shawinigan",119 unit_type=unit_type,120 price=_num(pm.group(1)),121 price_label=text,122 description=blurb,123 amenities=list(amenities),124 images=list(images),125 ))126 current_label = ""127 return listings128