SPB Git

spb/lou-ka Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

HTML 99.7%
5.1 KB · 119 lines python
Raw Blame History
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