Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
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