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.6 KB · 119 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/jardins_nd.py : connecteur Les Jardins Notre Dame5#   (logement-victoriaville.jardinsnotredame.com) — Victoriaville,6#   465, rue Notre-Dame Ouest. Complexe locatif neuf tout inclus (studios7#   meublés à 5½). Landing Unbounce page unique (~580 Ko) rendue serveur :8#   pas de sélecteurs sémantiques (divs lp-pom-*) — on parse le TEXTE en ordre9#   du document : une ligne typologie (« Studios », « 3 1/2 », « 4 1/2 »,10#   « 5 1/2 ») suivie de lignes d'inclusions puis d'une fourchette de prix11#   (« De 1 550 $ à 1 625 $ /mois »). Granularité = typologie (pas12#   d'inventaire par unité publié). external_id = typologie en slug.13# -----------------------------------------------------------------------------14from __future__ import annotations1516import html as _html17import re1819from ..schema import Listing, normalize_unit_type20from .base import BaseConnector2122PAGE_URL = "https://logement-victoriaville.jardinsnotredame.com/"2324ADDRESS = "465, rue Notre-Dame Ouest, Victoriaville"2526TYPE_RE = re.compile(r"^(Studios?|\d\s*(?:1/2|½))$", re.I)27PRICE_RANGE_RE = re.compile(28    r"^(?:De\s+)?([\d\s  ]{3,7})\$\s*(?:à\s*([\d\s  ]{3,7})\$)?\s*/\s*mois",29    re.I)30IMG_RE = re.compile(31    r"//d9hhrg4mnvzow\.cloudfront\.net/[^\"'\s),]+\.(?:jpg|jpeg|png|webp)",32    re.I)33SCRIPT_RE = re.compile(r"<(script|style)[\s\S]*?</\1>", re.I)34TAG_RE = re.compile(r"<[^>]+>")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 JardinsNotreDameConnector(BaseConnector):46    source_id = "jardins_nd"47    request_delay = 0.84849    def fetch(self) -> list[Listing]:50        html = self.get(PAGE_URL).text5152        # Photos (CDN Unbounce) : garder la variante la plus large par visuel53        images: list[str] = []54        for u in dict.fromkeys(IMG_RE.findall(html)):55            u = "https:" + u56            base = re.sub(r"_[\w]+o\.(jpg|jpeg|png|webp)$", "", u)57            if not any(im.startswith(base) for im in images):58                images.append(u)59        images = images[:12]6061        # HTML -> lignes de texte en ordre du document62        txt = SCRIPT_RE.sub(" ", html)63        txt = _html.unescape(TAG_RE.sub("\n", txt))64        lines = [re.sub(r"[\s  ]+", " ", l).strip() for l in txt.split("\n")]65        lines = [l for l in lines if l]6667        listings: list[Listing] = []68        seen: set[str] = set()69        current: str = ""70        amenities: list[str] = []71        for line in lines:72            if TYPE_RE.match(line):73                current = line74                amenities = []75                continue76            if not current:77                continue78            pm = PRICE_RANGE_RE.match(line)79            if not pm:80                # inclusions courtes entre la typologie et le prix81                if len(line) < 60 and not re.search(r"Voir|plan", line, re.I):82                    amenities.append(line)83                elif len(amenities) > 4:        # trop loin : carte abandonnée84                    current = ""85                continue86            unit_type = "Studio" if re.match(r"studio", current, re.I) \87                else normalize_unit_type(current)88            ext_id = "studio" if unit_type == "Studio" \89                else unit_type.replace("½", ".5")90            if ext_id in seen:                  # la page répète les cartes91                current = ""92                continue93            seen.add(ext_id)94            price = _num(pm.group(1))95            price_max = _num(pm.group(2)) if pm.group(2) else None96            details: dict = {}97            if price_max:98                details["price_max"] = price_max99            listings.append(Listing(100                source=self.source_id,101                external_id=ext_id,102                url=PAGE_URL,103                title=f"{unit_type} — Les Jardins Notre Dame",104                address=ADDRESS,105                sector="",106                city="Victoriaville",107                unit_type=unit_type,108                price=price,109                price_label=pm.group(0).strip(),110                description=f"{unit_type} tout inclus aux Jardins Notre Dame, "111                            "complexe locatif neuf de Victoriaville "112                            "(piscine, tennis, espaces communs).",113                amenities=list(dict.fromkeys(amenities)),114                details=details,115                images=list(images),116            ))117            current = ""118        return listings119