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/signature_boisbriand.py : connecteur Signature Boisbriand5# (signatureboisbriand.com) — projet locatif au 1040-1050, rue des6# Francs-Bourgeois, Boisbriand (Faubourg Boisbriand, ~266 unités).7# WordPress + FacetWP rendu serveur : /unites-disponibles/ liste des lignes8# .unit-row (numéro #1040-101, pièces, étage, superficie, prix « à partir9# de », disponibilité, plan PDF) paginées via le paramètre ?_paged=N10# (15 lignes/page). On ne garde que les unités NON « Unité louée ».11# Granularité : unité.12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from bs4 import BeautifulSoup1819from ..schema import Listing, normalize_unit_type20from .base import BaseConnector2122BASE = "https://signatureboisbriand.com"23UNITS_URL = f"{BASE}/unites-disponibles/"24MAX_PAGES = 302526ADDRESS = "1040, rue des Francs-Bourgeois, Boisbriand"27CITY = "Boisbriand"2829PRICE_RE = re.compile(r"(\d[\d\s,]*)\s*\$")30IMG_RE = re.compile(31 r"https://signatureboisbriand\.com/wp-content/uploads/"32 r"[^\"'\s\\]+\.(?:jpe?g|png|webp)", re.I)33SKIP_IMG_RE = re.compile(r"logo|favicon|icon|-\d{2,4}x\d{2,4}\.", re.I)343536class SignatureBoisbriandConnector(BaseConnector):37 source_id = "signature_boisbriand"38 request_delay = 0.639 max_images = 104041 def _site_images(self) -> list[str]:42 """Photos du projet (page d'accueil) — les lignes n'ont qu'un plan PDF."""43 try:44 html = self.get(f"{BASE}/").text45 except Exception:46 return []47 return [u for u in dict.fromkeys(IMG_RE.findall(html))48 if not SKIP_IMG_RE.search(u)][: self.max_images]4950 @staticmethod51 def _txt(row, sel: str) -> str:52 el = row.select_one(sel)53 return re.sub(r"\s+", " ", el.get_text(" ", strip=True)) if el else ""5455 def fetch(self) -> list[Listing]:56 images = self._site_images()57 listings: list[Listing] = []58 seen: set[str] = set()5960 for page in range(1, MAX_PAGES + 1):61 url = UNITS_URL if page == 1 else f"{UNITS_URL}?_paged={page}"62 try:63 html = self.get(url).text64 except Exception:65 break66 soup = BeautifulSoup(html, "html.parser")67 rows = soup.select(".unit-row")68 if not rows:69 break7071 new_on_page = 072 for row in rows:73 num = self._txt(row, ".unit-number__text").lstrip("#").strip()74 if not num or num in seen:75 continue76 seen.add(num)77 new_on_page += 17879 availability = self._txt(row, ".unit-availability__text")80 if re.search(r"lou[ée]", availability, re.I):81 continue # « Unité louée »8283 unit_type = normalize_unit_type(84 self._txt(row, ".unit-room__text"))85 floor = self._txt(row, ".unit-floor__text")8687 area = None88 m = re.search(r"(\d[\d\s]*)\s*pi",89 self._txt(row, ".unit-area__text"))90 if m:91 try:92 v = float(m.group(1).replace(" ", "").replace(" ", ""))93 if 100 <= v <= 10000:94 area = v95 except ValueError:96 pass9798 price = None99 price_label = ""100 m = PRICE_RE.search(self._txt(row, ".unit-price__text"))101 if m:102 try:103 val = float(m.group(1).replace(" ", "")104 .replace(" ", "").replace(",", ""))105 if 300 <= val <= 20000:106 price = val107 price_label = f"à partir de {int(val)}$"108 except ValueError:109 pass110111 imgs = list(images)112 plan = row.select_one('.unit-plan a[href$=".pdf"]')113 details: dict = {}114 if plan is not None:115 details["floor_plan"] = plan["href"]116117 listings.append(Listing(118 source=self.source_id,119 external_id=num,120 url=UNITS_URL,121 title=f"Signature Boisbriand — Unité #{num}",122 address=ADDRESS,123 city=CITY,124 unit_type=unit_type,125 price=price,126 price_label=price_label,127 availability=availability or "Disponible",128 area_sqft=area,129 description=f"Unité #{num}, {floor}" if floor else "",130 details=details,131 images=imgs,132 ))133 if new_on_page == 0:134 break135 return listings136