# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/westwalk.py : connecteur Westwalk DDO (westwalk.ca) # Immeuble neuf au 12, rue Davignon, Dollard-des-Ormeaux (West-Island), # à 10 min à pied du REM. WordPress/Elementor statique : la page # /en/apartments-for-rent-dollard-des-ormeaux/ affiche un « image-box » par # type d'unité (LOFT, 1-BEDROOM, 2-BEDROOMS, 3-BEDROOMS) avec soit un prix # « Starting at $2,300 », soit « 100% rented » — seuls les types avec un # prix réel sont annoncés (une annonce par type, id stable ww-ddo-). # Le site liste aussi Anjou (sans cartes) et Pointe-Claire (prix # placeholders $X,XXX) : exclus. Commodités = titres h3 de la page. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, strip_accents from .base import BaseConnector BASE = "https://westwalk.ca" PAGE = f"{BASE}/en/apartments-for-rent-dollard-des-ormeaux/" ADDRESS = "12, rue Davignon, Dollard-des-Ormeaux, QC H9B 0B6" # Libellé du type -> type d'unité QC _TYPES = { "LOFT": "Loft", "1-BEDROOM": "3½", "2-BEDROOMS": "4½", "3-BEDROOMS": "5½", } _PRICE_RE = re.compile(r"\$\s*([\d][\d,\s]*\d|\d)") _IMG_RE = re.compile( r'https://westwalk\.ca/wp-content/uploads/[^"\'\s]+?' r'\.(?:jpg|jpeg|png|webp)', re.I) _SKIP_IMG = re.compile(r"logo|icon|favicon|bullet|map-|-\d{2,4}x\d{2,4}\.", re.I) # h3 de la page qui ne sont PAS des commodités _SKIP_H3 = re.compile(r"^(unit\s|follow|contact|westwalk)", re.I) class WestwalkConnector(BaseConnector): source_id = "westwalk" request_delay = 0.6 max_images = 20 def fetch(self) -> list[Listing]: html = self.get(PAGE).text soup = BeautifulSoup(html, "html.parser") # galerie (photos du projet, sans logos/miniatures) images: list[str] = [] for u in _IMG_RE.findall(html): if _SKIP_IMG.search(u): continue if u not in images: images.append(u) images = images[: self.max_images] # commodités : titres h3 courts de la page (Elevator, Gym, Pool…) amenities: list[str] = [] for h in soup.find_all("h3"): t = h.get_text(" ", strip=True) if t and len(t) < 60 and not _SKIP_H3.match(t): if t not in amenities: amenities.append(t) amenities = amenities[:25] # description : méta de la page desc = "" meta = soup.find("meta", attrs={"name": "description"}) if meta and meta.get("content"): desc = meta["content"].strip()[:600] listings: list[Listing] = [] for box in soup.select(".elementor-image-box-content"): t_el = box.select_one(".elementor-image-box-title") d_el = box.select_one(".elementor-image-box-description") if not (t_el and d_el): continue label = t_el.get_text(" ", strip=True).upper() if label not in _TYPES: continue price_txt = d_el.get_text(" ", strip=True) pm = _PRICE_RE.search(price_txt) if not pm: continue # « 100% rented » ou placeholder : exclu try: price = float(re.sub(r"[^\d]", "", pm.group(1))) except ValueError: continue if not (100 <= price <= 20000): continue slug = re.sub(r"[^a-z0-9]+", "-", strip_accents(label.lower())).strip("-") listings.append(Listing( source=self.source_id, external_id=f"ddo-{slug}", url=PAGE, title=f"Westwalk DDO — {label.title()}", address=ADDRESS, sector="Dollard-des-Ormeaux", city="Dollard-des-Ormeaux", unit_type=_TYPES[label], price=price, price_label=f"À partir de {int(price)} $/mois", availability=price_txt, description=desc, amenities=list(amenities), images=list(images), )) return listings