# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/elements.py : connecteur Quartier les Éléments # (quartierleselements.com — Lévis, secteur Saint-Romuald, 5 phases). # Navigation : phase -> étage (plan interactif ) # -> fiche d'unité (type, superficie, prix). # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, infer_city, normalize_unit_type, parse_price from .base import BaseConnector BASE = "https://www.quartierleselements.com" ROOT = f"{BASE}/appartements-condos-locatifs-levis" SECTOR = "Saint-Romuald" ADDRESS = "1432, rue de Jupiter, Lévis" # Bureau de location affiché sur chaque fiche d'unité CONTACT = {"phone": "581-922-3334", "email": "info@quartierleselements.com"} class ElementsConnector(BaseConnector): source_id = "elements" request_delay = 0.5 max_floor_pages = 40 # garde-fou def fetch(self) -> list[Listing]: # 1) Découvrir les pages d'étages de chaque phase floor_urls: list[str] = [] for phase in range(1, 6): try: html = self.get(f"{ROOT}/phase-{phase}/").text except Exception: continue for path in sorted(set(re.findall( rf'href="(/appartements-condos-locatifs-levis/' rf'phase-{phase}/etage-\d+/)"', html))): floor_urls.append(BASE + path) # 2) Repérer les unités disponibles sur les plans d'étages unit_urls: list[str] = [] for url in floor_urls[:self.max_floor_pages]: try: html = self.get(url).text except Exception: continue for tag in re.findall(r"]*>", html, re.S): if 'data-available="1"' not in tag: continue m = re.search(r'href="([^"]+)"', tag) if m and m.group(1) not in unit_urls: unit_urls.append(m.group(1)) # 3) Fiche de chaque unité disponible listings: list[Listing] = [] for path in unit_urls: full_url = path if path.startswith("http") else BASE + path try: html = self.get(full_url).text except Exception: continue try: soup = BeautifulSoup(html, "html.parser") text = soup.get_text("\n", strip=True) num = re.search(r"Unité\s+(\w+)", text) unit_no = num.group(1) if num else path.strip("/").split("/")[-1] phase_m = re.search(r"phase-(\d)", path) phase = phase_m.group(1) if phase_m else "?" type_m = re.search(r"Grandeur\s*:\s*([^\n]+)", text) unit_type = normalize_unit_type(type_m.group(1)) if type_m else "" prix_line = "" pm = re.search(r"Prix\s*:\s*([^\n]+)", text) price = None if pm: prix_line = pm.group(1).strip() price = parse_price(prix_line) availability = "Disponible" am = re.search(r"pour\s+([a-zû]+\s+20\d\d)", prix_line, re.I) if am: availability = f"Disponible ({am.group(1)})" # superficies structurées : la brute = superficie du logement # (le parsing générique prendrait le min, donc la terrasse) area_sqft = None desc_parts = [] for label in ("Superficie brute", "Superficie terrasse", "Superficie totale"): dm = re.search(rf"{label}\s*:\s*([^\n]+)", text) if dm: desc_parts.append(f"{label} : {dm.group(1).strip()}") if label == "Superficie brute": nm = re.search(r"(\d[\d\s]*(?:[.,]\d+)?)\s*pi", dm.group(1)) if nm: area_sqft = float( nm.group(1).replace(" ", "").replace(",", ".")) extra = re.search(r"Avec boudoir|Avec bureau", text) amenities = [extra.group(0)] if extra else [] tm = re.search(r"Superficie terrasse", text) if tm: amenities.append("Terrasse") # étage structuré (segment /etage-N/ de l'URL) details: dict = {"contact": dict(CONTACT)} em = re.search(r"/etage-(\d+)/", full_url) if em: details["floor"] = int(em.group(1)) imgs = re.findall( r'(?:src|href)="((?:https?://[^"]+|/)?uploads/[^"]+' r'\.(?:jpg|jpeg|png|webp))"', html, re.I) images = [] for u in dict.fromkeys(imgs): if not u.startswith("http"): u = BASE + ("/" + u.lstrip("/")) images.append(u) listings.append(Listing( source=self.source_id, external_id=f"phase-{phase}-unite-{unit_no}", url=full_url, title=f"Quartier les Éléments — Phase {phase}, " f"unité {unit_no} ({unit_type})", address=ADDRESS, sector=SECTOR, city=infer_city(SECTOR), unit_type=unit_type, price=price, price_label=f"Prix : {prix_line}" if prix_line else "", availability=availability, area_sqft=area_sqft, description=" | ".join(desc_parts), amenities=amenities, details=details, images=images, )) except Exception: continue return listings