spb/lou-ka Public
Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 99.7%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/elements.py : connecteur Quartier les Éléments5# (quartierleselements.com — Lévis, secteur Saint-Romuald, 5 phases).6# Navigation : phase -> étage (plan interactif <area data-available>)7# -> fiche d'unité (type, superficie, prix).8# -----------------------------------------------------------------------------9from __future__ import annotations1011import re1213from bs4 import BeautifulSoup1415from ..schema import Listing, infer_city, normalize_unit_type, parse_price16from .base import BaseConnector1718BASE = "https://www.quartierleselements.com"19ROOT = f"{BASE}/appartements-condos-locatifs-levis"20SECTOR = "Saint-Romuald"21ADDRESS = "1432, rue de Jupiter, Lévis"22# Bureau de location affiché sur chaque fiche d'unité23CONTACT = {"phone": "581-922-3334", "email": "info@quartierleselements.com"}242526class ElementsConnector(BaseConnector):27 source_id = "elements"28 request_delay = 0.529 max_floor_pages = 40 # garde-fou3031 def fetch(self) -> list[Listing]:32 # 1) Découvrir les pages d'étages de chaque phase33 floor_urls: list[str] = []34 for phase in range(1, 6):35 try:36 html = self.get(f"{ROOT}/phase-{phase}/").text37 except Exception:38 continue39 for path in sorted(set(re.findall(40 rf'href="(/appartements-condos-locatifs-levis/'41 rf'phase-{phase}/etage-\d+/)"', html))):42 floor_urls.append(BASE + path)4344 # 2) Repérer les unités disponibles sur les plans d'étages45 unit_urls: list[str] = []46 for url in floor_urls[:self.max_floor_pages]:47 try:48 html = self.get(url).text49 except Exception:50 continue51 for tag in re.findall(r"<area\b[^>]*>", html, re.S):52 if 'data-available="1"' not in tag:53 continue54 m = re.search(r'href="([^"]+)"', tag)55 if m and m.group(1) not in unit_urls:56 unit_urls.append(m.group(1))5758 # 3) Fiche de chaque unité disponible59 listings: list[Listing] = []60 for path in unit_urls:61 full_url = path if path.startswith("http") else BASE + path62 try:63 html = self.get(full_url).text64 except Exception:65 continue66 try:67 soup = BeautifulSoup(html, "html.parser")68 text = soup.get_text("\n", strip=True)6970 num = re.search(r"Unité\s+(\w+)", text)71 unit_no = num.group(1) if num else path.strip("/").split("/")[-1]72 phase_m = re.search(r"phase-(\d)", path)73 phase = phase_m.group(1) if phase_m else "?"7475 type_m = re.search(r"Grandeur\s*:\s*([^\n]+)", text)76 unit_type = normalize_unit_type(type_m.group(1)) if type_m else ""7778 prix_line = ""79 pm = re.search(r"Prix\s*:\s*([^\n]+)", text)80 price = None81 if pm:82 prix_line = pm.group(1).strip()83 price = parse_price(prix_line)8485 availability = "Disponible"86 am = re.search(r"pour\s+([a-zû]+\s+20\d\d)", prix_line, re.I)87 if am:88 availability = f"Disponible ({am.group(1)})"8990 # superficies structurées : la brute = superficie du logement91 # (le parsing générique prendrait le min, donc la terrasse)92 area_sqft = None93 desc_parts = []94 for label in ("Superficie brute", "Superficie terrasse",95 "Superficie totale"):96 dm = re.search(rf"{label}\s*:\s*([^\n]+)", text)97 if dm:98 desc_parts.append(f"{label} : {dm.group(1).strip()}")99 if label == "Superficie brute":100 nm = re.search(r"(\d[\d\s]*(?:[.,]\d+)?)\s*pi",101 dm.group(1))102 if nm:103 area_sqft = float(104 nm.group(1).replace(" ", "").replace(",", "."))105 extra = re.search(r"Avec boudoir|Avec bureau", text)106 amenities = [extra.group(0)] if extra else []107 tm = re.search(r"Superficie terrasse", text)108 if tm:109 amenities.append("Terrasse")110111 # étage structuré (segment /etage-N/ de l'URL)112 details: dict = {"contact": dict(CONTACT)}113 em = re.search(r"/etage-(\d+)/", full_url)114 if em:115 details["floor"] = int(em.group(1))116117 imgs = re.findall(118 r'(?:src|href)="((?:https?://[^"]+|/)?uploads/[^"]+'119 r'\.(?:jpg|jpeg|png|webp))"', html, re.I)120 images = []121 for u in dict.fromkeys(imgs):122 if not u.startswith("http"):123 u = BASE + ("/" + u.lstrip("/"))124 images.append(u)125126 listings.append(Listing(127 source=self.source_id,128 external_id=f"phase-{phase}-unite-{unit_no}",129 url=full_url,130 title=f"Quartier les Éléments — Phase {phase}, "131 f"unité {unit_no} ({unit_type})",132 address=ADDRESS,133 sector=SECTOR,134 city=infer_city(SECTOR),135 unit_type=unit_type,136 price=price,137 price_label=f"Prix : {prix_line}" if prix_line else "",138 availability=availability,139 area_sqft=area_sqft,140 description=" | ".join(desc_parts),141 amenities=amenities,142 details=details,143 images=images,144 ))145 except Exception:146 continue147148 return listings149