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/port_de_mer.py : connecteur Port de Mer (portdemer.ca)5# Complexe locatif unique de 389 unités au 99-101 Place Charles-LeMoyne,6# Longueuil — directement en face du métro Longueuil. Site une-page7# (CMS Prog Expert) rendu serveur : description, caractéristiques,8# fourchette de prix « À partir de : 1515 $ - 2040 $ ». Aucun inventaire9# par unité publié (sitemap-Room vide) → une annonce par immeuble,10# comme brochu.py.11# -----------------------------------------------------------------------------12from __future__ import annotations1314import re1516from bs4 import BeautifulSoup1718from ..schema import Listing19from .base import BaseConnector2021BASE = "https://portdemer.ca"22PAGE_URL = f"{BASE}/fr/accueil"2324ADDRESS = "99-101, Place Charles-LeMoyne, Longueuil"25PRICE_RE = re.compile(r"[ÀA]\s*partir\s*de\s*:?\s*([\d\s ,]+)\s*\$"26 r"(?:\s*[-–]\s*([\d\s ,]+)\s*\$)?", re.I)27IMG_RE = re.compile(r"(?:https?:)?//portdemer\.ca/mod/file/"28 r"(?:Block|SubBlock)Image/[^\"'()\s]+\.(?:jpe?g|png|webp)"29 r"[^\"'()\s]*", re.I)303132def _num(txt: str) -> float | None:33 try:34 return float(txt.replace(" ", "").replace(" ", "").replace(",", ""))35 except (AttributeError, ValueError):36 return None373839class PortDeMerConnector(BaseConnector):40 source_id = "port_de_mer"41 request_delay = 0.64243 def fetch(self) -> list[Listing]:44 try:45 html = self.get(PAGE_URL).text46 except Exception:47 return []48 soup = BeautifulSoup(html, "html.parser")49 text = soup.get_text("\n", strip=True)5051 price = None52 price_label = ""53 m = PRICE_RE.search(text)54 if m:55 price = _num(m.group(1))56 price_label = re.sub(r"\s+", " ", m.group(0)).strip()5758 # description : paragraphes substantiels de la section d'intro59 paras = [p.get_text(" ", strip=True) for p in soup.find_all("p")60 if len(p.get_text(strip=True)) > 80]61 description = re.sub(r"\s+", " ", " ".join(paras[:4])).strip()[:1200]6263 # caractéristiques : les <li> des blocs sw-text (listes à puces)64 amenities: list[str] = []65 for li in soup.select("div.sw-text li"):66 t = re.sub(r"\s+", " ", li.get_text(" ", strip=True))67 if 0 < len(t) <= 60 and t not in amenities:68 amenities.append(t)6970 images = [("https:" + u if u.startswith("//") else u).split("?")[0]71 for u in dict.fromkeys(IMG_RE.findall(html))][:20]7273 contact: dict = {}74 m = re.search(r'href="mailto:([^"?]+)"', html)75 if m:76 contact["email"] = m.group(1).strip().lower()77 m = re.search(r'href="tel:\+?1?(\d{3})(\d{3})(\d{4})"', html)78 if m:79 contact["phone"] = f"{m.group(1)}-{m.group(2)}-{m.group(3)}"8081 return [Listing(82 source=self.source_id,83 external_id="port-de-mer-longueuil",84 url=PAGE_URL,85 title="Port de Mer — appartements face au métro Longueuil",86 address=ADDRESS,87 city="Longueuil",88 price=price,89 price_label=price_label,90 availability="Informations et locations : "91 "communiquer avec l'immeuble",92 description=description,93 amenities=amenities[:20],94 details={"contact": contact} if contact else {},95 images=images,96 )]97