SPB Git

spb/lou-ka Public

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

HTML 99.7%
5.3 KB · 125 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/gestion_valco.py : connecteur Gestion Valco (gestionvalco.ca)5#   WordPress Divi/Elementor, page « Logements à louer » rédigée à la main :6#   chaque logement = une section Elementor « titre » (« 402 Laviolette7#   Trois-Rivières 4 1/2 »), suivie d'un bloc texte (inclusions, dispo…)8#   et de galeries d'images. 1 seule requête par sync ; external_id =9#   data-id Elementor de la section titre (stable tant que la section vit).10# -----------------------------------------------------------------------------11from __future__ import annotations1213import re1415from bs4 import BeautifulSoup1617from ..schema import Listing, normalize_unit_type18from .base import BaseConnector1920BASE = "https://gestionvalco.ca"21LIST_URL = f"{BASE}/logements-a-louer/"   # redirige vers /elementor-344/logements-a-louer/2223# titre d'annonce : contient un type d'unité (« 4 1/2 », « 3½ », studio…)24_TYPE_RE = re.compile(r"\d\s*(?:½|1/2)|studio|loft", re.I)25# villes desservies par Gestion Valco (Mauricie / Centre-du-Québec)26_CITIES = [27    ("trois-rivieres", "Trois-Rivières"),28    ("trois- rivieres", "Trois-Rivières"),29    ("cap-de-la-madeleine", "Trois-Rivières"),30    ("shawinigan", "Shawinigan"),31    ("nicolet", "Nicolet"),32    ("louiseville", "Louiseville"),33    ("st-narcisse", "Saint-Narcisse"),34    ("saint-narcisse", "Saint-Narcisse"),35]363738def _norm(txt: str) -> str:39    """Aplatis les <font> de Google Translate : espaces multiples, « Trois- Rivières »."""40    txt = re.sub(r"\s+", " ", txt).strip()41    return re.sub(r"(\w)-\s+(\w)", r"\1-\2", txt)424344def _strip_accents(s: str) -> str:45    import unicodedata46    return "".join(c for c in unicodedata.normalize("NFD", s)47                   if unicodedata.category(c) != "Mn")484950class GestionValcoConnector(BaseConnector):51    source_id = "gestion_valco"52    request_delay = 0.75354    def fetch(self) -> list[Listing]:55        html = self.get(LIST_URL).text   # requests suit la redirection56        soup = BeautifulSoup(html, "html.parser")57        main = soup.find("main") or soup5859        listings: list[Listing] = []60        current: Listing | None = None61        desc_lines: list[str] = []6263        def _flush() -> None:64            nonlocal current, desc_lines65            if current is not None:66                current.description = "\n".join(desc_lines)[:900]67                listings.append(current)68            current, desc_lines = None, []6970        for sec in main.select("section.elementor-top-section"):71            head = sec.select_one(".elementor-widget-heading .elementor-heading-title")72            title = _norm(head.get_text(" ", strip=True)) if head else ""73            if title and _TYPE_RE.search(title) and not re.search(74                    r"commercial|stationnement|rangement|local\b", title, re.I):75                # nouvelle annonce : section titre76                _flush()77                low = _strip_accents(title.lower())78                city, city_pos = "", -179                for key, name in _CITIES:80                    p = low.find(key)81                    if p >= 0:82                        city, city_pos = name, p83                        break84                # adresse civique : début du titre jusqu'au nom de ville85                address = ""86                if city_pos > 0 and re.match(r"\d", title):87                    address = title[:city_pos].strip(" ,-")88                # disponibilité : mention « Libre … » du titre (texte source)89                m = re.search(r"\b(Libre\b.*)$", title, re.I)90                availability = m.group(1).strip() if m else ""91                ext_id = sec.get("data-id") or ""92                if not ext_id:93                    continue94                current = Listing(95                    source=self.source_id,96                    external_id=str(ext_id),97                    url=f"{BASE}/elementor-344/logements-a-louer/#{ext_id}",98                    title=title,99                    address=address,100                    city=city,101                    unit_type=normalize_unit_type(title),102                    availability=availability,103                )104                # la section titre peut aussi porter le bloc texte : on continue105            if current is None:106                continue107            # même section ou sections suivantes : description (texte), galeries108            for txt in sec.select(".elementor-widget-text-editor"):109                for p in txt.find_all(["p", "li"]) or [txt]:110                    t = re.sub(r"\s+", " ", p.get_text(" ", strip=True))111                    if t and t not in desc_lines:112                        desc_lines.append(t)113                if not current.availability:114                    for t in desc_lines:115                        if re.search(r"\b(libre|disponible)\b", t, re.I):116                            current.availability = t117                            break118            for a in sec.select("a.e-gallery-item[href]"):119                u = a["href"]120                if u.startswith("http") and u not in current.images:121                    current.images.append(u)122            current.images = current.images[:30]123        _flush()124        return listings125