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/sofia.py : connecteur Le Sofia — Groupe Lacombe5# (sofiavarennes.com — 48 condos locatifs au 1845, rue du Souvenir,6# Varennes, structure béton, face au fleuve). Site PHP/Perch CMS rendu7# serveur : la page plans.php contient le tableau COMPLET des unités8# (numéro, pi², chambres, modèle 3½/4½, plan JPG, loyer, disponibilité).9# Seules les lignes « Date -> OUI » (badge Disponible) sont retenues —10# les autres n'affichent ni loyer ni date. Granularité = UNITÉ.11# external_id = numéro d'unité (stable, adresse unique).12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from ..schema import Listing18from .base import BaseConnector1920BASE = "https://sofiavarennes.com"21LIST_URL = f"{BASE}/plans.php"22ADDRESS = "1845, rue du Souvenir, Varennes"23CITY = "Varennes"2425# une ligne d'unité = <div data-eventtype1="…" … class="row"> … </div>26ROW_SPLIT_RE = re.compile(r'<div data-eventtype1="[^"]*"')27FIELD_RE = re.compile(28 r'fw-700 mb-12">(Emplacement|Unité|Pi2|Pièces|Modèle|Loyer)</p>'29 r'<p class="fs-12">([^<]*)</p>', re.I)30AVAILABLE_RE = re.compile(r'>Date</p>\s*OUI\b')31DATE_TXT_RE = re.compile(32 r'Loyer</p><p class="fs-12">[^<]*</p></span>\s*'33 r'<span class="col-md-1"><p class="fs-12">([^<]*)</p>', re.I)34PLAN_RE = re.compile(r'href="(perch/resources/[^"]+\.(?:jpe?g|png|webp))"', re.I)353637def _price(txt: str) -> float | None:38 m = re.search(r"(\d[\d\s ]{2,8})\$", txt or "")39 if not m:40 return None41 try:42 return float(re.sub(r"[\s ]", "", m.group(1)))43 except ValueError:44 return None454647class SofiaConnector(BaseConnector):48 source_id = "sofia"49 request_delay = 0.85051 def fetch(self) -> list[Listing]:52 try:53 html = self.get(LIST_URL).text54 except Exception:55 return []5657 listings: list[Listing] = []58 seen: set[str] = set()59 for chunk in ROW_SPLIT_RE.split(html)[1:]:60 chunk = chunk.split("<h6", 1)[0] # fin de la ligne61 if not AVAILABLE_RE.search(chunk):62 continue # unité louée63 fields = {k: v.strip() for k, v in FIELD_RE.findall(chunk)}64 unit = fields.get("Unité", "")65 if not unit or unit in seen:66 continue67 seen.add(unit)6869 price = _price(fields.get("Loyer", ""))70 area = None71 if fields.get("Pi2", "").isdigit():72 area = float(fields["Pi2"])73 bedrooms = None74 bm = re.match(r"(\d+)\s*ch", fields.get("Pièces", ""), re.I)75 if bm:76 bedrooms = float(bm.group(1))77 unit_type = fields.get("Modèle", "") # « 3 1/2 » -> finalize7879 availability = "Disponible"80 dm = DATE_TXT_RE.search(chunk)81 if dm and dm.group(1).strip():82 availability = f"Disponible : {dm.group(1).strip()}"8384 images = [f"{BASE}/{p}" for p in dict.fromkeys(PLAN_RE.findall(chunk))]8586 listings.append(Listing(87 source=self.source_id,88 external_id=unit,89 url=LIST_URL,90 title=f"Le Sofia — unité {unit}",91 address=ADDRESS,92 city=CITY,93 unit_type=unit_type,94 bedrooms=bedrooms,95 price=price,96 price_label=f"{int(price)} $ /mois" if price else "",97 availability=availability,98 area_sqft=area,99 description=f"Unité {unit} au Sofia ({ADDRESS}) — immeuble "100 "locatif du Groupe Lacombe, structure de béton, "101 "ascenseur, climatisation, garage et borne de "102 "recharge.",103 images=images,104 ))105 return listings106