spb/lou-ka Public
Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 99.7%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/novelo.py : connecteur Les Appartements Novelo inc.5# (appartementsnovelo.com — Saguenay, secteur Jonquière/Arvida ; projet du6# Groupe Dumont, ~50-100 appartements rénovés). WordPress + Elementor + WPML,7# tout rendu serveur sur une page unique /louer-un-appartement/ : une section8# par type d'unité (« Appartement — Loft », « — 3 1/2 », « — 4 1/2 »,9# « — 5 1/2 ») avec prix « À partir de 901$/ mois », description, liste des10# services offerts et galerie. Granularité par TYPE (pas d'unités11# individuelles ni de dates) — types, prix et photos réels, précédent12# immeubles_guillot. Adresse du complexe : rue Langelier, Saguenay.13# robots.txt WP standard (wp-admin seulement), sitemap.14# -----------------------------------------------------------------------------15from __future__ import annotations1617import re1819from bs4 import BeautifulSoup2021from ..schema import Listing, normalize_unit_type, parse_price, strip_accents22from .base import BaseConnector2324BASE = "https://appartementsnovelo.com"25LIST_URL = f"{BASE}/louer-un-appartement/"2627_HEAD_RE = re.compile(r"^Appartement\s*[—–-]\s*(.+)$", re.I)28_PRICE_RE = re.compile(r"À partir de\s*\d[\d\s]*\$\s*(?:/\s*mois)?", re.I)29# suffixe de redimensionnement WordPress (« -300x200.jpg » -> pleine taille)30_SIZE_SUFFIX = re.compile(r"-\d{2,4}x\d{2,4}(?=\.(?:jpg|jpeg|png|webp)$)", re.I)313233def _slugify(label: str) -> str:34 """« 4 1/2 » -> « 4-1-2 » ; « Loft » -> « loft » (identifiant stable)."""35 return re.sub(r"[^a-z0-9]+", "-", strip_accents(label.lower())).strip("-")363738class NoveloConnector(BaseConnector):39 source_id = "novelo"40 request_delay = 0.64142 def fetch(self) -> list[Listing]:43 html = self.get(LIST_URL).text44 soup = BeautifulSoup(html, "html.parser")4546 listings: dict[str, Listing] = {}47 # une section Elementor de premier niveau par type d'appartement48 for h in soup.find_all("h2"):49 m = _HEAD_RE.match(h.get_text(" ", strip=True))50 if not m:51 continue52 try:53 self._parse_section(h, m.group(1).strip(), listings)54 except Exception:55 continue56 return list(listings.values())5758 # -- section « Appartement — <type> » ---------------------------------------------59 def _parse_section(self, heading, unit_label: str,60 listings: dict[str, Listing]) -> None:61 ext_id = _slugify(unit_label)62 if not ext_id or ext_id in listings:63 return6465 # remonter à la section Elementor de premier niveau66 sec = heading67 while sec.parent is not None and sec.name != "section":68 sec = sec.parent69 if sec.name != "section":70 return7172 # prix « À partir de 901$/ mois » (le plus bas du type)73 price_label = ""74 m = _PRICE_RE.search(sec.get_text(" ", strip=True))75 if m:76 price_label = re.sub(r"\s+", " ", m.group(0)).strip()7778 # description : paragraphes de la section (hors mentions de contact)79 paras: list[str] = []80 for p in sec.find_all("p"):81 txt = re.sub(r"\s+", " ", p.get_text(" ", strip=True))82 if not txt or _PRICE_RE.search(txt):83 continue84 if re.search(r"contactez|enquête de crédit|418-|courriel", txt, re.I):85 continue86 paras.append(txt)87 description = "\n".join(paras)[:1500]8889 # services offerts / caractéristiques : items de liste de la section90 amenities: list[str] = []91 for li in sec.find_all("li"):92 txt = re.sub(r"\s+", " ", li.get_text(" ", strip=True))93 if txt and txt not in amenities:94 amenities.append(txt)9596 # galerie de la section (photos réelles, pleine taille)97 images: list[str] = []98 for img in sec.find_all("img", src=True):99 u = img["src"]100 if not u.startswith("http") or "/wp-content/uploads/" not in u:101 continue102 u = _SIZE_SUFFIX.sub("", u)103 if u not in images:104 images.append(u)105106 listings[ext_id] = Listing(107 source=self.source_id,108 external_id=ext_id,109 url=f"{LIST_URL}#{ext_id}",110 title=f"Appartement {unit_label} — Les Appartements Novelo",111 address="2469, rue Langelier", # complexe (bureau de location)112 city="Saguenay",113 unit_type=normalize_unit_type(unit_label),114 price=parse_price(price_label.replace("À partir de", "").strip()),115 price_label=price_label,116 description=description,117 amenities=amenities[:20],118 images=images[:25],119 )120