SPB Git

spb/lou-ka Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

HTML 99.7%
4.2 KB · 114 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/lambert.py : connecteur Société Immobilière Lambert5#   (lambertimmobilier.com — Louiseville, Yamachiche, Mauricie). WordPress6#   avec thème custom « cognitif-starter » : la page /logements-disponibles/7#   liste des <article class="apartment"> (type h2, prix, <address>, photo en8#   background-image). On ne garde que la section « Nos logements à louer »9#   (les <article> suivant le titre « Projets à venir » = terrains, exclus).10#   Aucune page détail ; external_id = slug de l'adresse. 1 requête par sync.11# -----------------------------------------------------------------------------12from __future__ import annotations1314import re15import unicodedata1617from bs4 import BeautifulSoup1819from ..schema import Listing, normalize_unit_type, parse_price20from .base import BaseConnector2122BASE = "https://lambertimmobilier.com"23LIST_URL = f"{BASE}/logements-disponibles/"2425_BG_URL_RE = re.compile(r"background-image\s*:\s*url\(['\"]?([^'\")]+)")26_CITIES = [27    ("louiseville", "Louiseville"),28    ("yamachiche", "Yamachiche"),29    ("trois-rivieres", "Trois-Rivières"),30]313233def _strip_accents(s: str) -> str:34    return "".join(c for c in unicodedata.normalize("NFD", s)35                   if unicodedata.category(c) != "Mn")363738def _slug(s: str) -> str:39    s = _strip_accents(s.lower())40    return re.sub(r"[^a-z0-9]+", "-", s).strip("-")414243class LambertConnector(BaseConnector):44    source_id = "lambert"45    request_delay = 0.74647    def fetch(self) -> list[Listing]:48        html = self.get(LIST_URL).text49        soup = BeautifulSoup(html, "html.parser")50        listings: dict[str, Listing] = {}51        in_projects = False52        for el in soup.find_all(["h1", "h2", "article"]):53            if el.name in ("h1", "h2"):54                txt = el.get_text(" ", strip=True)55                if re.search(r"projets? à venir", txt, re.I):56                    in_projects = True        # terrains/projets : hors annonces57                continue58            if in_projects or "apartment" not in (el.get("class") or []):59                continue60            try:61                self._parse_card(el, listings)62            except Exception:63                continue64        return list(listings.values())6566    def _parse_card(self, card, listings: dict[str, Listing]) -> None:67        head = card.select_one("h2")68        head_txt = re.sub(r"\s+", " ", head.get_text(" ", strip=True)) if head else ""69        if re.search(r"terrain|commercial|local\b", head_txt, re.I):70            return71        addr_el = card.find("address")72        address_full = re.sub(r"\s+", " ",73                              addr_el.get_text(" ", strip=True)) if addr_el else ""74        if not address_full and not head_txt:75            return7677        # ville en fin d'adresse (« 131 St-Ubald Louiseville »)78        city, address = "", address_full79        low = _strip_accents(address_full.lower())80        for key, name in _CITIES:81            if key in low:82                city = name83                address = re.sub(rf",?\s*{key}\s*$", "", address_full,84                                 flags=re.I).strip(" ,")85                break8687        price_el = card.select_one(".apartment--price")88        price_label = re.sub(r"\s+", " ",89                             price_el.get_text(" ", strip=True)) if price_el else ""9091        images: list[str] = []92        img_div = card.select_one("div.img[style]")93        if img_div:94            m = _BG_URL_RE.search(img_div["style"])95            if m and m.group(1).startswith("http"):96                images.append(m.group(1))9798        ext = _slug(address_full or head_txt)99        if not ext or ext in listings:100            return101        title = f"{head_txt}{address_full}" if head_txt else address_full102        listings[ext] = Listing(103            source=self.source_id,104            external_id=ext,105            url=LIST_URL,106            title=title,107            address=address,108            city=city,109            unit_type=normalize_unit_type(head_txt),110            price=parse_price(price_label),111            price_label=price_label,112            images=images,113        )114