SPB Git

spb/lou-ka Public

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

HTML 99.7%
5.0 KB · 124 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/atlas_immo.py : connecteur Atlas société immobilière5#   (atlasimmo.ca — Beauce : Saint-Georges, Saint-Victor, Notre-Dame-des-Pins,6#   ~50-100 logements 2½–6½). CMS custom « webchef », pages .php statiques7#   servies en HTTP (pas de TLS). Page /logements-a-louer-en-beauce.php : toutes8#   les unités disponibles sont rendues serveur (cartes `div.resultat` — nom9#   d'immeuble, rue + ville, description « 4X 4 et demi moderne », drapeaux10#   Disponible/Chauffé/Éclairé avec icônes ok/cancel, photo) ; le filtre11#   ville/grandeur/prix est purement client (JS). Quand aucun logement n'est12#   libre, seule la mention « Aucun appartement n'est disponible… » s'affiche13#   et le connecteur retourne [] — situation normale (parc beauceron plein).14#   Prix jamais affiché sur les cartes -> None (rien d'inventé) ; granularité15#   immeuble assumée (précédent immeubles_guillot). Les fiches16#   /logement-a-louer/<id>-<slug>.php existent mais leur structure n'a jamais17#   été observée (aucune archive) : pas d'appel détail pour l'instant.18#   robots.txt : Disallow répertoires internes (/webchef, /inc…) seulement.19# -----------------------------------------------------------------------------20from __future__ import annotations2122import re2324from bs4 import BeautifulSoup2526from ..schema import Listing, normalize_unit_type27from .base import BaseConnector2829BASE = "http://www.atlasimmo.ca"30LIST_URL = f"{BASE}/logements-a-louer-en-beauce.php"3132# type d'unité dans la description d'immeuble (« 4X 4 et demi moderne »)33_TYPE_RE = re.compile(r"(\d)\s*(?:½|1/2|et\s+demi)", re.I)343536class AtlasImmoConnector(BaseConnector):37    source_id = "atlas_immo"38    request_delay = 0.63940    def fetch(self) -> list[Listing]:41        html = self.get(LIST_URL).text42        soup = BeautifulSoup(html, "html.parser")4344        listings: dict[str, Listing] = {}45        for card in soup.select("div.resultat"):46            try:47                self._parse_card(card, listings)48            except Exception:49                continue50        return list(listings.values())5152    # -- carte (div.resultat, id bloc-<n>) -----------------------------------------53    def _parse_card(self, card, listings: dict[str, Listing]) -> None:54        link = card.select_one('a[href*="/logement-a-louer/"]')55        if not link:56            return57        url = link["href"]58        if url.startswith("/"):59            url = BASE + url60        m = re.search(r"/logement-a-louer/([^/?#]+?)(?:\.php)?$", url)61        if not m:62            return63        ext_id = m.group(1)64        if not ext_id or ext_id in listings:65            return6667        title_el = card.select_one("h2")68        title = title_el.get_text(strip=True) if title_el else ""6970        # adresse : <li>Rue Ambroise<br><strong>Saint-Victor</strong>, ---</li>71        address, city = "", ""72        li = card.select_one(".description ul li")73        if li:74            strong = li.select_one("strong")75            city = strong.get_text(strip=True) if strong else ""76            street = li.get_text("\n", strip=True).split("\n")[0].strip()77            street = street.rstrip(",")78            address = f"{street}, {city}".strip(", ") if city else street7980        # description libre de l'immeuble (« 4X 4 et demi moderne »)81        desc_el = card.select_one(".description p:not(.aligneDroit)")82        description = desc_el.get_text(" ", strip=True) if desc_el else ""8384        m_type = _TYPE_RE.search(description)85        unit_type = normalize_unit_type(f"{m_type.group(1)} ½") if m_type else ""8687        # drapeaux : <span><i class="icon-ok"/> Disponible</span> — icon-cancel = non88        availability = ""89        amenities: list[str] = []90        for span in card.select(".options span"):91            label = span.get_text(" ", strip=True)92            icon = span.select_one("i")93            ok = bool(icon and "icon-ok" in (icon.get("class") or []))94            if re.search(r"disponible", label, re.I):95                if ok:96                    availability = label97            elif ok and label:98                amenities.append(label)99100        images: list[str] = []101        img = card.select_one("img[src]")102        if img:103            u = img["src"]104            if u.startswith("/"):105                u = BASE + u106            if u.startswith("http"):107                images.append(u)108109        listings[ext_id] = Listing(110            source=self.source_id,111            external_id=ext_id,112            url=url,113            title=title,114            address=address,115            city=city,116            unit_type=unit_type,117            price=None,                    # jamais affiché sur les cartes118            price_label="",119            availability=availability,120            description=description,121            amenities=amenities,122            images=images,123        )124