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/ferrovia.py : connecteur Ferrovia (ferroviamirabel.com)5# Projet de condos locatifs neufs à Mirabel (secteur Saint-Janvier), 4 phases.6# WordPress + wpDataTables rendues côté serveur : chaque page « Disponibilités »7# contient un <table.wpDataTable> (UNITÉ, MODÈLE, ÉTAGE, PIÈCES, SUPERFICIE,8# SALLE D'EAU SUPP., DISPONIBILITÉ, PRIX, STATUT, PLAN). On ne retient que les9# lignes STATUT = « Disponible » ; external_id = phase + numéro d'unité.10# Le site précise que les logements sont non-fumeurs et sans animaux.11# Prix : colonne présente mais vide à ce jour — jamais inventé.12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from bs4 import BeautifulSoup1819from ..schema import Listing, normalize_unit_type, parse_price20from .base import BaseConnector2122BASE = "https://www.ferroviamirabel.com"23PHASES = [24 ("1", f"{BASE}/disponibilites-prix-plans-phase1/"),25 ("3", f"{BASE}/disponibilites-phase-3/"),26 ("4", f"{BASE}/disponibilites-phase-4/"),27]282930class FerroviaConnector(BaseConnector):31 source_id = "ferrovia"32 request_delay = 0.73334 def fetch(self) -> list[Listing]:35 listings: dict[str, Listing] = {}36 for phase, url in PHASES:37 try:38 html = self.get(url).text39 except Exception:40 continue41 self._parse_phase(phase, url, html, listings)42 return list(listings.values())4344 def _parse_phase(self, phase: str, url: str, html: str,45 listings: dict[str, Listing]) -> None:46 soup = BeautifulSoup(html, "html.parser")47 table = soup.select_one("table.wpDataTable")48 if table is None:49 return50 heads = [th.get_text(" ", strip=True).lower()51 for th in table.select("thead th")]5253 def col(row_cells, *keys):54 for key in keys:55 for i, h in enumerate(heads):56 if key in h and i < len(row_cells):57 return row_cells[i]58 return None5960 for tr in table.select("tbody tr"):61 cells = tr.find_all("td")62 texts = [re.sub(r"\s+", " ", td.get_text(" ", strip=True))63 for td in cells]64 statut = col(texts, "statut") or ""65 if not re.match(r"(?i)disponible", statut):66 continue # loué / réservé : on saute67 unit = col(texts, "unité", "unite") or ""68 if not unit:69 continue70 pieces = col(texts, "pièces", "pieces") or ""71 unit_type = normalize_unit_type(pieces)72 if not re.fullmatch(r"\d½|Studio|Loft", unit_type or ""):73 continue # ligne non résidentielle74 modele = col(texts, "modèle", "modele") or ""75 etage = col(texts, "étage", "etage") or ""76 superficie = col(texts, "superficie") or ""77 demi_sdb = col(texts, "salle d'eau", "salle d’eau") or ""78 dispo = col(texts, "disponibilité", "disponibilite") or ""79 prix = col(texts, "prix") or "" # vide à ce jour sur le site8081 area = None82 m = re.match(r"([\d\s,.]+)$", superficie)83 if m:84 try:85 area = float(m.group(1).replace(" ", "").replace(",", ""))86 except ValueError:87 area = None8889 plan_td = col(cells, "plan")90 plan_url = ""91 if plan_td is not None:92 a = plan_td.find("a", href=True)93 if a:94 plan_url = a["href"]9596 details = {"phase": phase, "smoking": False}97 if modele:98 details["model"] = modele99 if etage:100 details["floor"] = etage101 if plan_url:102 details["plan_pdf"] = plan_url103 amenities = []104 if re.match(r"(?i)oui", demi_sdb):105 amenities.append("Salle d'eau supplémentaire")106107 ext = f"phase{phase}-{unit}"108 listings[ext] = Listing(109 source=self.source_id,110 external_id=ext,111 url=url,112 title=f"Ferrovia phase {phase} — unité {unit} ({pieces})",113 sector="Saint-Janvier",114 city="Mirabel",115 unit_type=unit_type,116 price=parse_price(prix),117 price_label=prix,118 availability=dispo, # « Automne 2026 », « ÉTÉ 2026 »119 area_sqft=area,120 pets="non", # note du site : animaux non admis121 description=("Condos neufs à louer du projet Ferrovia, à Mirabel "122 "dans le secteur de Saint-Janvier. Les logements sont "123 "non-fumeurs et les animaux ne sont pas admis."),124 amenities=amenities,125 details=details,126 )127