# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/lambert.py : connecteur Société Immobilière Lambert # (lambertimmobilier.com — Louiseville, Yamachiche, Mauricie). WordPress # avec thème custom « cognitif-starter » : la page /logements-disponibles/ # liste des
(type h2, prix,
, photo en # background-image). On ne garde que la section « Nos logements à louer » # (les
suivant le titre « Projets à venir » = terrains, exclus). # Aucune page détail ; external_id = slug de l'adresse. 1 requête par sync. # ----------------------------------------------------------------------------- from __future__ import annotations import re import unicodedata from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type, parse_price from .base import BaseConnector BASE = "https://lambertimmobilier.com" LIST_URL = f"{BASE}/logements-disponibles/" _BG_URL_RE = re.compile(r"background-image\s*:\s*url\(['\"]?([^'\")]+)") _CITIES = [ ("louiseville", "Louiseville"), ("yamachiche", "Yamachiche"), ("trois-rivieres", "Trois-Rivières"), ] def _strip_accents(s: str) -> str: return "".join(c for c in unicodedata.normalize("NFD", s) if unicodedata.category(c) != "Mn") def _slug(s: str) -> str: s = _strip_accents(s.lower()) return re.sub(r"[^a-z0-9]+", "-", s).strip("-") class LambertConnector(BaseConnector): source_id = "lambert" request_delay = 0.7 def fetch(self) -> list[Listing]: html = self.get(LIST_URL).text soup = BeautifulSoup(html, "html.parser") listings: dict[str, Listing] = {} in_projects = False for el in soup.find_all(["h1", "h2", "article"]): if el.name in ("h1", "h2"): txt = el.get_text(" ", strip=True) if re.search(r"projets? à venir", txt, re.I): in_projects = True # terrains/projets : hors annonces continue if in_projects or "apartment" not in (el.get("class") or []): continue try: self._parse_card(el, listings) except Exception: continue return list(listings.values()) def _parse_card(self, card, listings: dict[str, Listing]) -> None: head = card.select_one("h2") head_txt = re.sub(r"\s+", " ", head.get_text(" ", strip=True)) if head else "" if re.search(r"terrain|commercial|local\b", head_txt, re.I): return addr_el = card.find("address") address_full = re.sub(r"\s+", " ", addr_el.get_text(" ", strip=True)) if addr_el else "" if not address_full and not head_txt: return # ville en fin d'adresse (« 131 St-Ubald Louiseville ») city, address = "", address_full low = _strip_accents(address_full.lower()) for key, name in _CITIES: if key in low: city = name address = re.sub(rf",?\s*{key}\s*$", "", address_full, flags=re.I).strip(" ,") break price_el = card.select_one(".apartment--price") price_label = re.sub(r"\s+", " ", price_el.get_text(" ", strip=True)) if price_el else "" images: list[str] = [] img_div = card.select_one("div.img[style]") if img_div: m = _BG_URL_RE.search(img_div["style"]) if m and m.group(1).startswith("http"): images.append(m.group(1)) ext = _slug(address_full or head_txt) if not ext or ext in listings: return title = f"{head_txt} — {address_full}" if head_txt else address_full listings[ext] = Listing( source=self.source_id, external_id=ext, url=LIST_URL, title=title, address=address, city=city, unit_type=normalize_unit_type(head_txt), price=parse_price(price_label), price_label=price_label, images=images, )