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.5 KB · 116 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/l_laval.py : connecteur Le L Laval (l-laval.com)5#   Projet locatif unique au 1585, boulevard des Laurentides, Laval (104 unités,6#   studio à 5½). WordPress (thème maison) rendu serveur : la page7#   /plans-condos-locatifs-laval/ contient un sélecteur d'étages avec UNE ligne8#   <tr id="rowUnitNNN"> par unité — la classe « not-available » marque les9#   unités louées, et l'attribut onclick=loadPage(unité, prix, pi², étage, état,10#   typologie, sdb, salle d'eau, den, modulable) porte toutes les données.11#   Granularité : unité. Certaines unités libres affichent 0$ (prix sur demande).12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from ..schema import Listing, normalize_unit_type18from .base import BaseConnector1920BASE = "https://l-laval.com"21PLANS_URL = f"{BASE}/plans-condos-locatifs-laval/"2223ADDRESS = "1585, boulevard des Laurentides, Laval"24CITY = "Laval"2526# <tr id="rowUnit104" … class="ch1 not-available" … onclick='loadPage("104",…)'>27ROW_RE = re.compile(28    r'<tr\s+id="rowUnit(\w+)"[^>]*class="([^"]*)"[^>]*'29    r"onclick='loadPage\(([^)]*)\)'", re.I)30ARG_RE = re.compile(r'"([^"]*)"')31IMG_RE = re.compile(32    r"https://l-laval\.com/wp-content/uploads/[^\"'\s\\]+\.(?:jpe?g|png|webp)",33    re.I)34SKIP_IMG_RE = re.compile(r"logo|favicon|icon|plan|-\d{2,4}x\d{2,4}\.", re.I)353637class LLavalConnector(BaseConnector):38    source_id = "l_laval"39    request_delay = 0.640    max_images = 104142    def _site_images(self) -> list[str]:43        """Photos du projet (page d'accueil) — les unités n'ont pas de galerie."""44        try:45            html = self.get(f"{BASE}/").text46        except Exception:47            return []48        return [u for u in dict.fromkeys(IMG_RE.findall(html))49                if not SKIP_IMG_RE.search(u)][: self.max_images]5051    def fetch(self) -> list[Listing]:52        html = self.get(PLANS_URL).text53        images = self._site_images()5455        listings: list[Listing] = []56        seen: set[str] = set()57        for unit, classes, raw_args in ROW_RE.findall(html):58            if "not-available" in classes or unit in seen:59                continue60            seen.add(unit)61            # loadPage(unité, prix, pi², étage, état, typologie, sdb, s. d'eau,62            #          den, modulable) — état : 0 libre, 1 vendu, 2 réservé63            args = ARG_RE.findall(raw_args)64            if len(args) < 6:65                continue66            _, price_raw, sqft_raw, floor, etat, typo = args[:6]67            if etat.strip() not in ("", "0"):68                continue69            sdb = args[6] if len(args) > 6 else ""70            seau = args[7] if len(args) > 7 else ""7172            typo = typo.replace("&frac12;", "½").replace("1/2", "½")73            unit_type = normalize_unit_type(typo)7475            price = None76            price_label = ""77            m = re.search(r"(\d[\d\s,]*)\s*\$", price_raw)78            if m:79                try:80                    val = float(m.group(1).replace(" ", "").replace(",", ""))81                    if 300 <= val <= 20000:82                        price = val83                        price_label = price_raw.strip()84                except ValueError:85                    pass86            if price is None:87                price_label = "Prix sur demande"8889            area = None90            try:91                v = float(sqft_raw)92                if 100 <= v <= 10000:93                    area = v94            except ValueError:95                pass9697            desc_bits = [f"Unité {unit}, {floor}e étage" if floor else "",98                         f"{sdb} salle(s) de bain" if sdb not in ("", "0") else "",99                         "salle d'eau" if seau not in ("", "0") else ""]100            listings.append(Listing(101                source=self.source_id,102                external_id=unit,103                url=f"{PLANS_URL}#unite-{unit}",104                title=f"Le L Laval — Unité {unit}",105                address=ADDRESS,106                city=CITY,107                unit_type=unit_type,108                price=price,109                price_label=price_label,110                availability="Disponible",111                area_sqft=area,112                description=" — ".join(b for b in desc_bits if b),113                images=list(images),114            ))115        return listings116