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.7 KB · 134 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/square_philippe.py : connecteur Square Philippe (squarephilippe.com)5#   Complexe locatif neuf de Gatineau (secteur est, J8P) — 4 immeubles6#   (25 rue du Galion, 75/91/115 rue Campagnard), 119 unités au total.7#   WordPress rendu serveur : /a-louer/unites/ liste les unités DISPONIBLES en8#   cartes `.theUnits` (data-rel = « 1 chambre »/« 2 chambres ») avec bandeau9#   adresse (`.coin span`), prix (`.prix` « 1 700$ »), numéro d'unité + pi²10#   (`.infoAll`), étage + chambres + « Disponible » (`.info2`) et photo du plan.11#   external_id = <immeuble en slug>-<numéro d'unité> — stable par unité.12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from bs4 import BeautifulSoup1819from ..schema import Listing, strip_accents20from .base import BaseConnector2122BASE = "https://squarephilippe.com"23LIST_URL = f"{BASE}/a-louer/unites/"2425PRICE_RE = re.compile(r"([\d\s  ]{3,7})\$")26UNIT_NO_RE = re.compile(r"Unité\s*(\w+)", re.I)27AREA_RE = re.compile(r"Total\s*:\s*([\d\s  ]+)\s*pi", re.I)28BEDS_RE = re.compile(r"(\d+)\s*chambres?", re.I)29FLOOR_RE = re.compile(r"(Rez-de-chaussée|\d+e\s*étage)", re.I)303132def _slug(text: str) -> str:33    s = strip_accents(text.lower())34    return re.sub(r"[^a-z0-9]+", "-", s).strip("-")353637def _num(txt: str) -> float | None:38    n = re.sub(r"[\s  ]", "", txt or "")39    try:40        return float(n)41    except ValueError:42        return None434445class SquarePhilippeConnector(BaseConnector):46    source_id = "square_philippe"47    request_delay = 0.84849    def fetch(self) -> list[Listing]:50        soup = BeautifulSoup(self.get(LIST_URL).text, "html.parser")51        listings: dict[str, Listing] = {}52        for card in soup.select(".theUnits"):53            try:54                self._parse_card(card, listings)55            except Exception:56                continue57        return list(listings.values())5859    def _parse_card(self, card, listings: dict[str, Listing]) -> None:60        coin = card.select_one(".coin span")61        building = re.sub(r"\s+", " ", coin.get_text(" ", strip=True)) \62            if coin else ""63        info = card.select_one(".infoAll")64        info_txt = re.sub(r"\s+", " ", info.get_text(" ", strip=True)) \65            if info else ""66        m = UNIT_NO_RE.search(info_txt)67        if not building or not m:68            return69        unit_no = m.group(1)70        ext_id = f"{_slug(building)}-{unit_no}"71        if ext_id in listings:72            return7374        price = None75        price_label = ""76        prix_el = card.select_one(".prix")77        if prix_el:78            price_label = re.sub(r"\s+", " ", prix_el.get_text(" ", strip=True))79            pm = PRICE_RE.search(price_label)80            if pm:81                price = _num(pm.group(1))8283        area = None84        am = AREA_RE.search(info_txt)85        if am:86            area = _num(am.group(1))8788        info2 = card.select_one(".info2")89        info2_txt = re.sub(r"\s+", " ", info2.get_text(" ", strip=True)) \90            if info2 else ""91        bedrooms = None92        bm = BEDS_RE.search(info2_txt + " " + (card.get("data-rel") or ""))93        if bm:94            bedrooms = float(bm.group(1))95        details: dict = {}96        fm = FLOOR_RE.search(info2_txt)97        if fm:98            details["floor"] = fm.group(1)99100        availability = ""101        av = card.find(string=re.compile(r"Disponible|Loué", re.I))102        if av:103            availability = re.sub(r"\s+", " ", str(av)).strip()104        if re.search(r"lou[ée]", availability, re.I):105            return                              # unité déjà louée : on saute106107        images: list[str] = []108        for im in card.select("img[src]"):109            u = im["src"]110            if not u.startswith("http"):111                u = BASE + u112            if u not in images and not re.search(r"logo|icon", u, re.I):113                images.append(u)114115        desc_bits = [b for b in (details.get("floor", ""),116                                 f"{int(area)} pi²" if area else "") if b]117        listings[ext_id] = Listing(118            source=self.source_id,119            external_id=ext_id,120            url=f"{LIST_URL}#{ext_id}",121            title=f"Unité {unit_no} — {building}",122            address=f"{building}, Gatineau",123            sector="",124            city="Gatineau",125            bedrooms=bedrooms,126            price=price,127            price_label=price_label,128            availability=availability,129            area_sqft=area,130            description=" | ".join(desc_bits),131            details=details,132            images=images,133        )134