# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/le_pilier.py : connecteur Le Pilier (lepilierlevis.com) — condos # locatifs neufs au 1275, rue J.-B.-Demers à Lévis (sections 3 et 6 étages). # WordPress + plugin maison « themelio-real-estate » : la page /projet/ # rend côté serveur un (Unité, Étage, # Superficie, Pièces, Prix, Disponibilité, lien /units/unite-NNN/), filtré # par défaut sur les unités disponibles ; la page unité fournit le plan. # Granularité : unité, avec prix. # ----------------------------------------------------------------------------- from __future__ import annotations import hashlib import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type, parse_price from .base import BaseConnector BASE = "https://lepilierlevis.com" LIST_URL = f"{BASE}/projet/" SQFT_RE = re.compile(r"([\d\s]+)\s*pi²", re.I) FLOOR_RE = re.compile(r"(\d+)") PLAN_RE = re.compile(r'src="(https://lepilierlevis\.com/wp-content/uploads/' r'[^"]+\.(?:avif|png|jpe?g|webp))"', re.I) ADDRESS = "1275, rue J.-B.-Demers, Lévis" CONTACT = {"phone": "418-836-7666"} class LePilierConnector(BaseConnector): source_id = "le_pilier" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: list[Listing] = [] try: html = self.get(LIST_URL).text except Exception: return listings soup = BeautifulSoup(html, "html.parser") table = soup.select_one("table.units-table") if table is None: return listings seen: set[str] = set() for row in table.select("tbody tr"): try: cells = row.find_all("td") if len(cells) < 7: continue unit_no, floor, area_txt, rooms, price_txt, avail = \ [c.get_text(" ", strip=True) for c in cells[:6]] if not unit_no or not re.match(r"^disponible", avail, re.I): continue # réservé / loué ext_id = f"unite-{unit_no}" if ext_id in seen: continue seen.add(ext_id) link = cells[6].find("a") url = link["href"] if link and link.get("href") else LIST_URL area = None m = SQFT_RE.search(area_txt) if m: area = float(m.group(1).replace(" ", "") .replace(" ", "").replace(" ", "")) unit_type = normalize_unit_type(rooms) # plan de l'unité sur sa page /units/unite-NNN/ (cache détail # invalidé quand la ligne du tableau change) key = hashlib.sha1( "|".join([unit_no, floor, area_txt, rooms, price_txt, avail]).encode()).hexdigest() d = self.detail(ext_id, key, lambda u=url: self._fetch_unit(u)) listings.append(Listing( source=self.source_id, external_id=ext_id, url=url, title=f"Unité {unit_no} — Le Pilier Lévis", address=ADDRESS, city="Lévis", unit_type=unit_type, price=parse_price(price_txt), price_label=price_txt, availability=avail, area_sqft=area, description=f"Unité {unit_no} ({unit_type}), {floor} — " "Le Pilier, condos locatifs neufs au cœur " "de Lévis.", details={"floor": floor, "contact": dict(CONTACT)}, images=d.get("images") or [], )) except Exception: continue return listings def _fetch_unit(self, url: str) -> dict: """Plan/photos de la page unité (/units/unite-NNN/).""" out: dict = {} try: html = self.get(url).text except Exception: return out imgs = [u for u in PLAN_RE.findall(html) if "logo" not in u.lower()] if imgs: out["images"] = list(dict.fromkeys(imgs))[:5] return out