# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/port_de_mer.py : connecteur Port de Mer (portdemer.ca) # Complexe locatif unique de 389 unités au 99-101 Place Charles-LeMoyne, # Longueuil — directement en face du métro Longueuil. Site une-page # (CMS Prog Expert) rendu serveur : description, caractéristiques, # fourchette de prix « À partir de : 1515 $ - 2040 $ ». Aucun inventaire # par unité publié (sitemap-Room vide) → une annonce par immeuble, # comme brochu.py. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing from .base import BaseConnector BASE = "https://portdemer.ca" PAGE_URL = f"{BASE}/fr/accueil" ADDRESS = "99-101, Place Charles-LeMoyne, Longueuil" PRICE_RE = re.compile(r"[ÀA]\s*partir\s*de\s*:?\s*([\d\s ,]+)\s*\$" r"(?:\s*[-–]\s*([\d\s ,]+)\s*\$)?", re.I) IMG_RE = re.compile(r"(?:https?:)?//portdemer\.ca/mod/file/" r"(?:Block|SubBlock)Image/[^\"'()\s]+\.(?:jpe?g|png|webp)" r"[^\"'()\s]*", re.I) def _num(txt: str) -> float | None: try: return float(txt.replace(" ", "").replace(" ", "").replace(",", "")) except (AttributeError, ValueError): return None class PortDeMerConnector(BaseConnector): source_id = "port_de_mer" request_delay = 0.6 def fetch(self) -> list[Listing]: try: html = self.get(PAGE_URL).text except Exception: return [] soup = BeautifulSoup(html, "html.parser") text = soup.get_text("\n", strip=True) price = None price_label = "" m = PRICE_RE.search(text) if m: price = _num(m.group(1)) price_label = re.sub(r"\s+", " ", m.group(0)).strip() # description : paragraphes substantiels de la section d'intro paras = [p.get_text(" ", strip=True) for p in soup.find_all("p") if len(p.get_text(strip=True)) > 80] description = re.sub(r"\s+", " ", " ".join(paras[:4])).strip()[:1200] # caractéristiques : les
  • des blocs sw-text (listes à puces) amenities: list[str] = [] for li in soup.select("div.sw-text li"): t = re.sub(r"\s+", " ", li.get_text(" ", strip=True)) if 0 < len(t) <= 60 and t not in amenities: amenities.append(t) images = [("https:" + u if u.startswith("//") else u).split("?")[0] for u in dict.fromkeys(IMG_RE.findall(html))][:20] contact: dict = {} m = re.search(r'href="mailto:([^"?]+)"', html) if m: contact["email"] = m.group(1).strip().lower() m = re.search(r'href="tel:\+?1?(\d{3})(\d{3})(\d{4})"', html) if m: contact["phone"] = f"{m.group(1)}-{m.group(2)}-{m.group(3)}" return [Listing( source=self.source_id, external_id="port-de-mer-longueuil", url=PAGE_URL, title="Port de Mer — appartements face au métro Longueuil", address=ADDRESS, city="Longueuil", price=price, price_label=price_label, availability="Informations et locations : " "communiquer avec l'immeuble", description=description, amenities=amenities[:20], details={"contact": contact} if contact else {}, images=images, )]