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/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