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/habitations_jeanne.py : connecteur Les Habitations Jeanne inc.5# (leshabitationsjeanne.com — Baie-Comeau, Côte-Nord, 200+ appartements).6# Site Wix rendu serveur : la page /appartements-meubles présente des cartes7# d'unités réelles (adresse civique, numéro d'appartement « App. 12 », type8# « appartement 5 ½ », galerie de photos Wix) — location mensuelle, meublé.9# La page /appartements-non-meubles est une vitrine (texte + adresses du parc,10# aucune carte d'unité) : elle est parcourue avec le même parseur et11# contribuera des annonces si des cartes y apparaissent un jour.12# Aucun prix ni disponibilité publiés (granularité vitrine assumée, comme13# immeubles_guillot) ; les photos et types sont réels, unité par unité.14# robots.txt Wix ouvert (Disallow *?lightbox= seulement), sitemap XML.15# -----------------------------------------------------------------------------16from __future__ import annotations1718import re1920from bs4 import BeautifulSoup2122from ..schema import Listing, normalize_unit_type, strip_accents23from .base import BaseConnector2425BASE = "https://www.leshabitationsjeanne.com"26# (url, meublé) — les deux pages du site ; seuls les meublés ont des cartes27PAGES = [28 (f"{BASE}/appartements-meubles", True),29 (f"{BASE}/appartements-non-meubles", False),30]3132_APP_RE = re.compile(r"App\.\s*(\d+[A-Za-z]?)", re.I)33_TYPE_RE = re.compile(r"([1-9])\s*(?:½|1/2)")34# adresse civique : numéro puis nom de voie (« 3, avenue du Père-Arnaud »,35# « 20 Roberval ») — le vrai filtre est l'appariement avec le bloc « App. » suivant36_ADDR_RE = re.compile(r"^\d+[A-Za-z]?,?\s+\D")373839def _slug(text: str) -> str:40 """Texte -> slug stable ascii (« 3, avenue du Père-Arnaud » -> « 3-avenue-du-pere-arnaud »)."""41 s = strip_accents(text.lower())42 return re.sub(r"-{2,}", "-", re.sub(r"[^a-z0-9]+", "-", s)).strip("-")434445def _full_size(url: str) -> str:46 """URL wixstatic redimensionnée -> média pleine taille (coupe au /v1/)."""47 return url.split("/v1/")[0] if "/v1/" in url else url484950class HabitationsJeanneConnector(BaseConnector):51 source_id = "habitations_jeanne"52 request_delay = 0.65354 def fetch(self) -> list[Listing]:55 listings: dict[str, Listing] = {}56 for url, furnished in PAGES:57 try:58 html = self.get(url).text59 except Exception:60 continue61 self._parse_page(html, url, furnished, listings)62 return list(listings.values())6364 # -- cartes d'unités (Wix SSR) --------------------------------------------------65 def _parse_page(self, html: str, page_url: str, furnished: bool,66 listings: dict[str, Listing]) -> None:67 soup = BeautifulSoup(html, "html.parser")68 # séquence des blocs de texte riche : une adresse civique suivie du bloc69 # « App. N / appartement X ½ » = une carte d'unité70 blocks = soup.select('div[data-testid="richTextElement"]')71 for i, blk in enumerate(blocks):72 addr = re.sub(r"\s+", " ", blk.get_text(" ", strip=True))73 if not _ADDR_RE.match(addr) or _APP_RE.search(addr):74 continue75 if i + 1 >= len(blocks):76 continue77 info = re.sub(r"\s+", " ", blocks[i + 1].get_text(" ", strip=True))78 m_app = _APP_RE.search(info)79 if not m_app:80 continue81 app_no = m_app.group(1)8283 m_type = _TYPE_RE.search(info)84 unit_label = f"{m_type.group(1)} ½" if m_type else ""8586 ext_id = f"{_slug(addr)}--app-{app_no.lower()}"87 if not ext_id or ext_id in listings:88 continue8990 # galerie : images wixstatic de la carte (ancêtre contenant adresse + App.)91 images: list[str] = []92 card = blk93 while card is not None and "App." not in card.get_text():94 card = card.parent95 if card is not None:96 for im in card.select("img[src]"):97 u = _full_size(im["src"])98 if u.startswith("https://static.wixstatic.com") and u not in images:99 images.append(u)100101 title = f"{addr} — App. {app_no}"102 if unit_label:103 title += f" ({unit_label}{' meublé' if furnished else ''})"104105 listings[ext_id] = Listing(106 source=self.source_id,107 external_id=ext_id,108 url=page_url,109 title=title,110 address=addr,111 city="Baie-Comeau", # tout le parc affiché est à Baie-Comeau112 unit_type=normalize_unit_type(unit_label),113 price=None, # jamais publié sur le site114 price_label="",115 availability="",116 furnished=furnished,117 images=images[:15],118 )119