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