SPB Git

spb/lou-ka Public

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

HTML 99.7%
4.7 KB · 104 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/cite_immobilier.py : connecteur Cité Immobilier (citeimmobilier.com)5#   224 logements à Victoriaville. Site GoDaddy Website Builder 8 (même famille6#   que capital_rdr) mais SANS annonces par unité : la page /immeubles liste les7#   immeubles (cartes data-ux="ContentBasic", titres data-aid ABOUT_HEADLINE_*)8#   avec adresse civique, description et un bouton d'état — « APPARTEMENT À9#   LOUER » (vacance) ou « - COMPLET - ». On publie une annonce par immeuble10#   résidentiel affichant une vacance ; prix/type ne sont pas publiés.11#   robots.txt : « User-agent: * » sans Disallow (tout permis).12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from bs4 import BeautifulSoup1819from ..schema import Listing, strip_accents20from .base import BaseConnector2122BASE = "https://citeimmobilier.com"23LIST_URL = f"{BASE}/immeubles"242526def _slug(text: str) -> str:27    """« 540 rue Notre-Dame Est » -> « 540-rue-notre-dame-est » (id stable)."""28    s = strip_accents(text.lower())29    return re.sub(r"-{2,}", "-", re.sub(r"[^a-z0-9]+", "-", s)).strip("-")303132class CiteImmobilierConnector(BaseConnector):33    source_id = "cite_immobilier"34    request_delay = 0.73536    def fetch(self) -> list[Listing]:37        html = self.get(LIST_URL).text38        soup = BeautifulSoup(html, "html.parser")3940        listings: list[Listing] = []41        seen: set[str] = set()42        for section in soup.select('section[data-ux="Section"]'):43            sec_title = section.select_one('[data-aid="ABOUT_SECTION_TITLE_RENDERED"]')44            sec_name = sec_title.get_text(" ", strip=True) if sec_title else ""45            # seules les sections résidentielles nous intéressent46            # (« IMMEUBLES RÉSIDENTIELS », « IMMEUBLES COMMERCIAUX ET RÉSIDENTIELS »)47            if "RÉSIDENTIEL" not in sec_name.upper():48                continue4950            for card in section.select('[data-ux="ContentBasic"]'):51                head = card.select_one('[data-aid^="ABOUT_HEADLINE"]')52                if not head:53                    continue54                address = head.get_text(" ", strip=True)55                ext_id = _slug(address)56                if not ext_id or ext_id in seen:57                    continue5859                card_text = card.get_text(" ", strip=True)60                ctas = [a.get_text(" ", strip=True)61                        for a in card.find_all("a") if a.get_text(strip=True)]62                # vacance affichée = bouton « APPARTEMENT À LOUER » ;63                # « - COMPLET - » ou absence du bouton → immeuble plein, ignoré64                avail = next((c for c in ctas65                              if re.search(r"appartement.*louer", c, re.I)), "")66                if not avail or re.search(r"-\s*COMPLET\s*-", card_text):67                    continue68                seen.add(ext_id)6970                # description : paragraphes de la carte (texte source),71                # sans le libellé du bouton72                lines = []73                for p in card.find_all(["p", "h5"]):74                    t = re.sub(r"\s+", " ", p.get_text(" ", strip=True))75                    if t and t not in lines and t.upper() != avail.upper():76                        lines.append(t)7778                images: list[str] = []79                img = card.select_one("img[src], img[srcset]")80                if img:81                    src = img.get("src") or (img.get("srcset") or "").split(" ")[0]82                    if src.startswith("//"):83                        src = "https:" + src84                    if src.startswith("http"):85                        images.append(src)8687                listings.append(Listing(88                    source=self.source_id,89                    external_id=ext_id,      # slug de l'adresse de l'immeuble90                    url=f"{LIST_URL}#{ext_id}",   # pas de fiche individuelle91                    title=f"Appartement à louer — {address}",92                    address=address,93                    sector="",94                    # tout le parc Cité Immobilier (224 logements + Le Quartz)95                    # est à Victoriaville (centre-ville, cf. page Immeubles)96                    city="Victoriaville",97                    unit_type="",            # jamais publié par la source98                    price_label="",          # jamais publié par la source99                    availability=avail,      # texte du bouton (source)100                    description="\n".join(lines)[:900],101                    images=images,102                ))103        return listings104