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%
5.8 KB · 143 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/cent4.py : connecteur Terrasse Cent4 (terrassecent4.com —5#   condos locatifs neufs 3½/4½ au 1700, chemin Saint-Jean, La Prairie,6#   promoteur Vestric). WordPress (thème Voyou) rendu serveur : la page7#   /les-unites liste les unités disponibles par bâtiment (Terrasse 2/3)8#   dans des <li class="vy_units_listing_item"> — numéro, étage, typologie,9#   pi², loyer, mois de disponibilité + lien « fiche de l'unité ».10#   La fiche (via self.detail, cache BD) ajoute inclusions et options.11#   Granularité = UNITÉ. external_id = slug de la fiche (terrasse-2-unite-202).12# -----------------------------------------------------------------------------13from __future__ import annotations1415import html as _html16import re1718from ..schema import Listing19from .base import BaseConnector2021BASE = "https://terrassecent4.com"22LIST_URL = f"{BASE}/les-unites"23ADDRESS = "1700, chemin Saint-Jean, La Prairie"24CITY = "La Prairie"2526BUILDING_RE = re.compile(27    r"<div class=\"vy_units_list_item\" data-filter-building='([^']+)'")28ITEM_RE = re.compile(29    r'<li class="vy_units_listing_item"[\s\S]*?</li>')30FIELD_RE = re.compile(31    r'vy_units_listing_(unit|floor|type|square|price|availability)[\s\S]*?'32    r'<span>([\s\S]*?)</span>')33FICHE_RE = re.compile(r'href="(https://terrassecent4\.com/units/[^"]+)"')34AVAILABLE_RE = re.compile(r'data-filter-available="1"')35INCL_RE = re.compile(r"Inclusions([\s\S]*?)(?:Options|Imprimez)", re.I)36OPT_RE = re.compile(r"Options([\s\S]*?)Imprimez", re.I)37SCRIPT_RE = re.compile(r"<(script|style)[\s\S]*?</\1>", re.I)38TAG_RE = re.compile(r"<[^>]+>")39IMG_RE = re.compile(          # Bedrock : médias sous /app/uploads/ (data-src)40    r'https://terrassecent4\.com/app/uploads/[^"\s)]+'41    r"\.(?:jpe?g|png|webp)", re.I)424344def _flat(html: str) -> str:45    t = _html.unescape(SCRIPT_RE.sub(" ", html))46    return re.sub(r"[\s  ]+", " ", TAG_RE.sub(" | ", t))474849def _clean(txt: str) -> str:50    return re.sub(r"\s+", " ", _html.unescape(TAG_RE.sub(" ", txt))).strip()515253class Cent4Connector(BaseConnector):54    source_id = "cent4"55    request_delay = 0.85657    def fetch(self) -> list[Listing]:58        try:59            html = self.get(LIST_URL).text60        except Exception:61            return []6263        listings: list[Listing] = []64        seen: set[str] = set()65        # les items sont regroupés par bloc bâtiment ; on suit le bâtiment66        # courant en balayant le HTML dans l'ordre67        pos_buildings = [(m.start(), m.group(1))68                         for m in BUILDING_RE.finditer(html)]69        for m in ITEM_RE.finditer(html):70            chunk = m.group(0)71            if not AVAILABLE_RE.search(chunk):72                continue73            building = ""74            for pos, b in pos_buildings:75                if pos < m.start():76                    building = b.replace("-", " ").title()77            fields = {k: _clean(v) for k, v in FIELD_RE.findall(chunk)}78            unit = fields.get("unit", "")79            fm = FICHE_RE.search(chunk)80            url = fm.group(1) if fm else LIST_URL81            ext_id = (url.rsplit("/", 1)[-1] if fm82                      else f"{building.lower().replace(' ', '-')}-{unit}")83            if not unit or ext_id in seen:84                continue85            seen.add(ext_id)8687            price = None88            pm = re.search(r"(\d[\d\s  ]{2,8})\$", fields.get("price", ""))89            if pm:90                price = float(re.sub(r"[\s  ]", "", pm.group(1)))91            area = None92            am = re.search(r"(\d{3,5})\s*pi", fields.get("square", ""))93            if am:94                area = float(am.group(1))95            availability = re.sub(r"^Disponibilité\s*\|?\s*", "",96                                  fields.get("availability", "")).strip()9798            detail = self.detail(ext_id, f"{price}|{availability}",99                                 lambda u=url: self._detail(u))100101            listings.append(Listing(102                source=self.source_id,103                external_id=ext_id,104                url=url,105                title=f"Terrasse Cent4 ({building}) — unité {unit}",106                address=ADDRESS,107                city=CITY,108                unit_type=fields.get("type", ""),109                price=price,110                price_label=f"{int(price)} $ par mois" if price else "",111                availability=(f"Disponibilité : {availability}"112                              if availability else ""),113                area_sqft=area,114                description=detail.get("description", ""),115                amenities=detail.get("amenities", []),116                details={"building": building,117                         "floor": fields.get("floor", "")},118                images=detail.get("images", []),119            ))120        return listings121122    def _detail(self, url: str) -> dict:123        """Fiche d'unité : inclusions, options, photos."""124        try:125            html = self.get(url).text126        except Exception:127            return {}128        flat = _flat(html)129        amenities: list[str] = []130        for rx in (INCL_RE, OPT_RE):131            fm = rx.search(flat)132            if fm:133                for part in fm.group(1).split("|"):134                    part = part.strip(" .")135                    if 3 <= len(part) <= 120 and part not in amenities:136                        amenities.append(part)137        images = [u for u in dict.fromkeys(IMG_RE.findall(html))138                  if not re.search(r"logo|icon|favicon|hummingbird|-\d+x\d+\.",139                                   u, re.I)][:10]140        desc = ("Condo locatif neuf à la Terrasse Cent4, complexe de "141                f"Vestric à La Prairie ({ADDRESS}).")142        return {"description": desc, "amenities": amenities, "images": images}143