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%
4.5 KB · 116 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/le_pilier.py : connecteur Le Pilier (lepilierlevis.com) — condos5#   locatifs neufs au 1275, rue J.-B.-Demers à Lévis (sections 3 et 6 étages).6#   WordPress + plugin maison « themelio-real-estate » : la page /projet/7#   rend côté serveur un <table class="units-table"> (Unité, Étage,8#   Superficie, Pièces, Prix, Disponibilité, lien /units/unite-NNN/), filtré9#   par défaut sur les unités disponibles ; la page unité fournit le plan.10#   Granularité : unité, avec prix.11# -----------------------------------------------------------------------------12from __future__ import annotations1314import hashlib15import re1617from bs4 import BeautifulSoup1819from ..schema import Listing, normalize_unit_type, parse_price20from .base import BaseConnector2122BASE = "https://lepilierlevis.com"23LIST_URL = f"{BASE}/projet/"2425SQFT_RE = re.compile(r"([\d\s]+)\s*pi²", re.I)26FLOOR_RE = re.compile(r"(\d+)")27PLAN_RE = re.compile(r'src="(https://lepilierlevis\.com/wp-content/uploads/'28                     r'[^"]+\.(?:avif|png|jpe?g|webp))"', re.I)2930ADDRESS = "1275, rue J.-B.-Demers, Lévis"31CONTACT = {"phone": "418-836-7666"}323334class LePilierConnector(BaseConnector):35    source_id = "le_pilier"36    request_delay = 0.63738    def fetch(self) -> list[Listing]:39        listings: list[Listing] = []40        try:41            html = self.get(LIST_URL).text42        except Exception:43            return listings44        soup = BeautifulSoup(html, "html.parser")45        table = soup.select_one("table.units-table")46        if table is None:47            return listings4849        seen: set[str] = set()50        for row in table.select("tbody tr"):51            try:52                cells = row.find_all("td")53                if len(cells) < 7:54                    continue55                unit_no, floor, area_txt, rooms, price_txt, avail = \56                    [c.get_text(" ", strip=True) for c in cells[:6]]57                if not unit_no or not re.match(r"^disponible", avail, re.I):58                    continue        # réservé / loué59                ext_id = f"unite-{unit_no}"60                if ext_id in seen:61                    continue62                seen.add(ext_id)6364                link = cells[6].find("a")65                url = link["href"] if link and link.get("href") else LIST_URL6667                area = None68                m = SQFT_RE.search(area_txt)69                if m:70                    area = float(m.group(1).replace(" ", "")71                                 .replace(" ", "").replace(" ", ""))72                unit_type = normalize_unit_type(rooms)7374                # plan de l'unité sur sa page /units/unite-NNN/ (cache détail75                # invalidé quand la ligne du tableau change)76                key = hashlib.sha1(77                    "|".join([unit_no, floor, area_txt, rooms, price_txt,78                              avail]).encode()).hexdigest()79                d = self.detail(ext_id, key,80                                lambda u=url: self._fetch_unit(u))8182                listings.append(Listing(83                    source=self.source_id,84                    external_id=ext_id,85                    url=url,86                    title=f"Unité {unit_no} — Le Pilier Lévis",87                    address=ADDRESS,88                    city="Lévis",89                    unit_type=unit_type,90                    price=parse_price(price_txt),91                    price_label=price_txt,92                    availability=avail,93                    area_sqft=area,94                    description=f"Unité {unit_no} ({unit_type}), {floor} — "95                                "Le Pilier, condos locatifs neufs au cœur "96                                "de Lévis.",97                    details={"floor": floor, "contact": dict(CONTACT)},98                    images=d.get("images") or [],99                ))100            except Exception:101                continue102        return listings103104    def _fetch_unit(self, url: str) -> dict:105        """Plan/photos de la page unité (/units/unite-NNN/)."""106        out: dict = {}107        try:108            html = self.get(url).text109        except Exception:110            return out111        imgs = [u for u in PLAN_RE.findall(html)112                if "logo" not in u.lower()]113        if imgs:114            out["images"] = list(dict.fromkeys(imgs))[:5]115        return out116