SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
4.1 KB · 106 lines python
Raw Blame History
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