SPB Git

spb/lou-ka Public

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

HTML 99.7%
4.2 KB · 106 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/immogex.py : connecteur Immogex (immogex.com)5#   GoDaddy Website Builder 8. La page « À louer » expose chaque logement en6#   bloc ABOUT_* (data-aid) : HEADLINE = « Nom - prix$ », DESCRIPTION = texte7#   brut (Disponible…, Grandeur : 3 ½, Adresse : …, caractéristiques),8#   IMAGE (data-srclazy, img1.wsimg.com). 1 seule requête par sync ;9#   ville fixe Drummondville (tout le parc Immogex y est — voir rapport).10# -----------------------------------------------------------------------------11from __future__ import annotations1213import re1415from bs4 import BeautifulSoup1617from ..schema import Listing, normalize_unit_type, parse_price, strip_accents18from .base import BaseConnector1920BASE = "https://immogex.com"21LIST_URL = f"{BASE}/%C3%A0-louer"2223_GRANDEUR_RE = re.compile(r"Grandeur\s*:\s*([^\n]+)", re.I)24_ADRESSE_RE = re.compile(r"Adresse\s*:\s*([^\n]+)", re.I)25_DISPO_RE = re.compile(r"(Disponible[^\n]*)", re.I)262728def _slug(txt: str) -> str:29    """Slug stable dérivé du texte source (nom + n° civique)."""30    s = strip_accents(txt.lower())31    return re.sub(r"-{2,}", "-", re.sub(r"[^a-z0-9]+", "-", s)).strip("-")323334class ImmogexConnector(BaseConnector):35    source_id = "immogex"36    request_delay = 0.63738    def fetch(self) -> list[Listing]:39        soup = BeautifulSoup(self.get(LIST_URL).text, "html.parser")40        listings: dict[str, Listing] = {}41        # blocs ABOUT_HEADLINE_RENDERED<n> / ABOUT_DESCRIPTION_RENDERED<n> /42        # ABOUT_IMAGE_RENDERED<n> : appariés par le suffixe du data-aid43        for head in soup.select("[data-aid^=ABOUT_HEADLINE_RENDERED]"):44            try:45                self._parse_block(soup, head, listings)46            except Exception:47                continue48        return list(listings.values())4950    def _parse_block(self, soup, head, listings: dict[str, Listing]) -> None:51        suffix = head["data-aid"].replace("ABOUT_HEADLINE_RENDERED", "")52        title = head.get_text(" ", strip=True)53        if not title:54            return55        # exclusions : locaux commerciaux, stationnements, rangements56        if re.search(r"commercial|bureau|stationnement|rangement|entrep[ôo]t",57                     title, re.I):58            return5960        desc_el = soup.select_one(f'[data-aid="ABOUT_DESCRIPTION_RENDERED{suffix}"]')61        description = desc_el.get_text("\n", strip=True) if desc_el else ""6263        # « Jardins de la Rivia I - 1355$ » -> nom + étiquette de prix64        price_label = ""65        name = title66        m = re.search(r"^(.*?)[\s–-]+(\d[\d\s,.]*\$)\s*$", title)67        if m:68            name, price_label = m.group(1).strip(" -–"), m.group(2).strip()6970        grandeur = _GRANDEUR_RE.search(description)71        unit_type = normalize_unit_type(grandeur.group(1).strip()) if grandeur else ""72        adresse = _ADRESSE_RE.search(description)73        address = adresse.group(1).strip() if adresse else ""74        dispo = _DISPO_RE.search(description)75        availability = dispo.group(1).strip() if dispo else ""7677        # id stable : slug du nom + n° civique (pas d'id ni de fiche chez GoDaddy)78        civic = re.match(r"(\d+)", address)79        ext_id = _slug(f"{name}-{civic.group(1) if civic else ''}")80        if not ext_id or ext_id in listings:81            return8283        images = []84        img = soup.select_one(f'[data-aid="ABOUT_IMAGE_RENDERED{suffix}"]')85        if img:86            src = img.get("data-srclazy") or img.get("src") or ""87            if src.startswith("//"):88                src = "https:" + src89            if src.startswith("http"):90                images = [src]9192        listings[ext_id] = Listing(93            source=self.source_id,94            external_id=ext_id,95            url=f"{LIST_URL}#{ext_id}",   # pas de fiche individuelle96            title=title,97            address=address,98            city="Drummondville",99            unit_type=unit_type,100            price=parse_price(price_label),101            price_label=price_label,102            availability=availability,103            description=description[:2000],104            images=images,105        )106