Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
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