SPB Git forge

spb/lou-ka

Public

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

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
6.4 KB · 154 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_1139.py : connecteur Gestion 1139 (gestion1139.com)5#   Drummondville et environs (Centre-du-Québec). WordPress + FacetWP rendu6#   serveur : la page d'accueil liste les 40+ immeubles (cartes .fwpl-result :7#   titre, adresse, typologies, badge .archive-tag « Disponible … » sur les8#   immeubles qui louent). AUCUN prix ni inventaire par unité sur le site —9#   seules les cartes portant un badge « Disponible » sont retenues.10#   Granularité : une annonce par TYPOLOGIE d'un immeuble disponible11#   (ex. projet-horace-3.5, projet-horace-4.5).12# -----------------------------------------------------------------------------13from __future__ import annotations1415import hashlib16import re1718from bs4 import BeautifulSoup1920from ..schema import Listing, normalize_unit_type21from .base import BaseConnector2223BASE = "https://gestion1139.com"2425DISPO_RE = re.compile(r"disponible", re.I)26# villes plausibles du parc (détection dans le texte de la page immeuble)27CITY_RE = re.compile(28    r"(Drummondville|Saint-Nic[ée]phore|Saint-Charles-de-Drummond|"29    r"Saint-Cyrille[\w-]*|Saint-Germain[\w-]*|Notre-Dame-du-Bon-Conseil|"30    r"Wickham|L'Avenir)", re.I)31IMG_RE = re.compile(r"https://gestion1139\.com/wp-content/uploads/"32                    r'[^"\'\s\\)]+\.(?:jpe?g|png|webp)', re.I)333435class Gestion1139Connector(BaseConnector):36    source_id = "gestion_1139"37    request_delay = 0.73839    def fetch(self) -> list[Listing]:40        listings: list[Listing] = []41        try:42            html = self.get(BASE + "/").text43        except Exception:44            return listings45        soup = BeautifulSoup(html, "html.parser")4647        seen: set[str] = set()48        for result in soup.select("div.fwpl-result"):49            try:50                tag_el = result.select_one(".archive-tag")51                tag = tag_el.get_text(" ", strip=True) if tag_el else ""52                if not tag or not DISPO_RE.search(tag):53                    continue  # immeuble sans badge de disponibilité54                a = result.select_one('a[href*="/immeubles/"]')55                if a is None:56                    continue57                url = a.get("href", "").split("?")[0]58                slug = url.rstrip("/").split("/")[-1]59                if not slug or slug in seen:60                    continue61                seen.add(slug)6263                title_el = result.select_one(".listing_title a")64                name = (title_el.get_text(" ", strip=True)65                        if title_el else slug)66                addr_el = result.select_one("p.adresse")67                address = (re.sub(r"\s+", " ",68                                  addr_el.get_text(" ", strip=True))69                           if addr_el else "")7071                card_key = hashlib.sha1(72                    f"{tag}|{name}|{address}".encode("utf-8")).hexdigest()73                d = self.detail(slug, card_key,74                                lambda url=url: self._fetch_detail(url))75                if d.get("address"):76                    address = d["address"]77                city = d.get("city") or "Drummondville"7879                units = d.get("units") or [{"unit_type": "", "extra": []}]80                for u in units:81                    ut = u.get("unit_type", "")82                    suffix = ut.replace("½", ".5").replace(" ", "") or "imm"83                    listings.append(Listing(84                        source=self.source_id,85                        external_id=f"{slug}-{suffix}",86                        url=url,87                        title=f"{ut + ' — ' if ut else ''}{name}",88                        address=address,89                        city=city,90                        unit_type=ut,91                        availability=tag,92                        description=d.get("description", ""),93                        amenities=(u.get("extra") or [])94                        + (d.get("amenities") or []),95                        images=d.get("images") or [],96                    ))97            except Exception:98                continue99        return listings100101    def _fetch_detail(self, url: str) -> dict:102        """Page immeuble : description, adresse civique, avantages,103        typologies (« Nos unités » : n ½ / chambres / salles de bain),104        ville (détectée dans le texte) et photos."""105        out: dict = {}106        try:107            html = self.get(url).text108        except Exception:109            return out110        soup = BeautifulSoup(html, "html.parser")111112        # typologies113        units: list[dict] = []114        for bloc in soup.select("div.liste_appartement"):115            ps = [p.get_text(" ", strip=True) for p in bloc.find_all("p")116                  if p.get_text(strip=True)]117            if not ps:118                continue119            units.append({"unit_type": normalize_unit_type(ps[0]),120                          "extra": ps[1:3]})121        out["units"] = units122123        # description : premiers paragraphes de la section d'intro124        paras = [p.get_text(" ", strip=True)125                 for p in soup.select(".uncode_text_column p")126                 if len(p.get_text(strip=True)) > 40]127        if paras:128            out["description"] = re.sub(r"\s+", " ", paras[0]).strip()[:900]129130        # adresse civique (ligne « 1645 et 1655 Boulevard Lemire »)131        text = soup.get_text("\n", strip=True)132        m = re.search(r"^\d{1,5}[^\n]{3,70}(?:rue|boulevard|boul\.|avenue|"133                      r"chemin|route)[^\n]{0,50}$", text, re.I | re.M)134        if m:135            out["address"] = re.sub(r"\s+", " ", m.group(0)).strip()136        m = CITY_RE.search(text)137        if m:138            out["city"] = m.group(1)139        else:  # souvent seulement dans les noms de fichiers photo140            m = CITY_RE.search(html)141            if m:142                out["city"] = m.group(1).capitalize()143144        # avantages (liste « Les avantages »)145        out["amenities"] = [146            li.get_text(" ", strip=True)147            for li in soup.select(".uncode_text_column li")148            if 0 < len(li.get_text(strip=True)) <= 80][:15]149150        out["images"] = [u for u in dict.fromkeys(IMG_RE.findall(html))151                         if not re.search(r"logo|icon|favicon|-\d{2,3}x\d{2,3}\.",152                                          u, re.I)][:20]153        return out154