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.4 KB · 113 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/linea.py : connecteur LINÉA (linea.ca) — Trois-Rivières5#   Complexe locatif neuf du District 55 (6900, boulevard des Athlètes, secteur6#   Trois-Rivières-Ouest), 3 phases. WordPress/WPBakery rendu serveur : chaque7#   page /unites/phase-N/ présente les typologies disponibles sous forme de8#   colonnes « <type> » (Loft, 3 ½, 4 ½, 5 ½) suivies d'un bloc texte9#   « De 1 008 pi² à 1 071 pi² / À partir de 1 750 $ / mois ». Granularité =10#   typologie par phase (le site ne publie pas d'inventaire unité par unité).11#   external_id = phaseN-<type> — stable tant que la typologie est offerte.12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from bs4 import BeautifulSoup1819from ..schema import Listing, normalize_unit_type2021BASE = "https://linea.ca"22PHASES = ("phase-1", "phase-2", "phase-3")2324ADDRESS = "6900, boulevard des Athlètes, Trois-Rivières"2526UNIT_RE = re.compile(r"^\s*(Loft|Studio|\d\s*½)\s*$")27PRICE_RE = re.compile(r"À\s+partir\s+de\s+([\d\s  ]+)\s*\$\s*/\s*mois", re.I)28AREA_RE = re.compile(r"(?:De\s+)?([\d\s  ]+)\s*pi²(?:\s+à\s+([\d\s  ]+)\s*pi²)?",29                     re.I)30IMG_RE = re.compile(          # /wp-content/uploads/ ou /dist/uploads/ (RocketCDN)31    r"https?://[\w./-]+/uploads/[^\"'\s)\\]+?\.(?:jpg|jpeg|png|webp)", re.I)32IMG_SKIP_RE = re.compile(r"favicon|logo|icon|texture|-\d+x\d+\.", re.I)3334from .base import BaseConnector  # noqa: E402  (après les constantes, style maison)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 LineaConnector(BaseConnector):46    source_id = "linea"47    request_delay = 0.84849    def fetch(self) -> list[Listing]:50        listings: list[Listing] = []51        for phase in PHASES:52            url = f"{BASE}/unites/{phase}/"53            try:54                html = self.get(url).text55            except Exception:56                continue57            listings.extend(self._parse_phase(phase, url, html))58        return listings5960    def _parse_phase(self, phase: str, url: str, html: str) -> list[Listing]:61        soup = BeautifulSoup(html, "html.parser")62        phase_label = phase.replace("phase-", "Phase ")6364        # Photos de la page (les typologies n'ont pas de galerie propre)65        images = [u for u in dict.fromkeys(IMG_RE.findall(html))66                  if not IMG_SKIP_RE.search(u)][:15]6768        listings: list[Listing] = []69        current_type = ""70        # Parcours en ordre du document : un <p> « 4 ½ » annonce la typologie,71        # le <p> suivant porte superficie + « À partir de … $ / mois ».72        for p in soup.select(".wpb_text_column p"):73            text = re.sub(r"\s+", " ", p.get_text(" ", strip=True))74            if not text:75                continue76            m = UNIT_RE.match(text)77            if m:78                current_type = normalize_unit_type(m.group(1))79                continue80            if not current_type:81                continue82            pm = PRICE_RE.search(text)83            if not pm:84                continue85            price = _num(pm.group(1))86            area = None87            desc_bits = [f"{phase_label} du LINÉA (District 55)"]88            am = AREA_RE.search(text)89            if am:90                area = _num(am.group(1))91                if am.group(2):92                    desc_bits.append(f"Superficies de {am.group(1).strip()} à "93                                     f"{am.group(2).strip()} pi²")94            ext_id = f"{phase}-{current_type.replace('½', '.5').lower()}"95            ext_id = re.sub(r"[^\w.\-]", "-", ext_id)96            listings.append(Listing(97                source=self.source_id,98                external_id=ext_id,99                url=url,100                title=f"{current_type} — LINÉA {phase_label}",101                address=ADDRESS,102                sector="Trois-Rivières-Ouest",103                city="Trois-Rivières",104                unit_type=current_type,105                price=price,106                price_label=pm.group(0),107                area_sqft=area,108                description=" | ".join(desc_bits),109                images=images,110            ))111            current_type = ""112        return listings113