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/sig.py : connecteur Société d'investissement Gilbert (immo-sig.ca)5# Résidences étudiantes en colocation à Québec (Université Laval, Cégep6# Sainte-Foy) : Le Sommet, Le Pavillon, La FAC, L'UNI. La page7# /residences-etudiantes/ (Elementor, rendue serveur) affiche pour chaque8# résidence le prix « à partir de » par chambre, l'adresse et une photo.9# Granularité = résidence (aucune unité détaillée publiée). Les fiches10# détaillées vivent sur residences-etudiantes.com, protégé par une11# vérification anti-bot reCAPTCHA -> immo-sig.ca est la source primaire ;12# le lien « DÉCOUVRIR » sert d'URL d'annonce pour l'utilisateur.13# -----------------------------------------------------------------------------14from __future__ import annotations1516import re1718from bs4 import BeautifulSoup1920from ..schema import Listing, strip_accents21from .base import BaseConnector2223BASE = "https://immo-sig.ca"24INDEX_URL = f"{BASE}/residences-etudiantes/"2526# variantes responsives WordPress (-300x200.jpg) à écarter27_VARIANT_IMG = re.compile(r"-\d{2,4}x\d{2,4}\.(?:jpe?g|png|webp)$", re.I)282930def _slug(name: str) -> str:31 s = strip_accents((name or "").strip().lower())32 return re.sub(r"[^a-z0-9]+", "-", s).strip("-")333435class SigConnector(BaseConnector):36 source_id = "sig"37 request_delay = 0.63839 def fetch(self) -> list[Listing]:40 html = self.get(INDEX_URL).text41 soup = BeautifulSoup(html, "html.parser")4243 listings: list[Listing] = []44 seen: set[str] = set()45 # une colonne Elementor par résidence : photo + <h3> nom + texte46 # « COLOCATION ÉTUDIANTE / Unités de N chambres à partir de X$/mois /47 # adresse » + bouton DÉCOUVRIR48 for col in soup.select(".elementor-column"):49 h3 = col.select_one("h3.elementor-heading-title")50 text_w = col.select_one(".elementor-widget-text-editor")51 if not h3 or not text_w:52 continue53 name = re.sub(r"\s+", " ", h3.get_text(" ", strip=True))54 sid = _slug(name)55 if not name or sid in seen:56 continue5758 paras = [re.sub(r"[ \t]+", " ", p.get_text("\n", strip=True))59 for p in text_w.find_all("p")]60 tagline = paras[0] if paras else ""61 body = paras[1] if len(paras) > 1 else ""62 if "à partir de" not in body.lower():63 continue # pas un bloc résidence64 seen.add(sid)6566 # « Unités de 3 chambres à partir de 650$/mois\n2333 Chapdelaine »67 lines = [l.strip() for l in body.split("\n") if l.strip()]68 price_line = lines[0] if lines else ""69 address = lines[1] if len(lines) > 1 else ""7071 img = col.select_one("img[src]")72 images = []73 if img:74 src = (img.get("src") or "").strip()75 if src.startswith("http") and not _VARIANT_IMG.search(src):76 images.append(src)7778 link = col.select_one("a.elementor-button[href]")79 url = (link["href"] if link and80 str(link["href"]).startswith("http") else INDEX_URL)8182 # « meublées » n'est indiqué que pour L'UNI83 furnished = True if "meublé" in price_line.lower() else None84 m = re.search(r"unités?\s+de\s+([\d\s aà-]+chambres?)",85 price_line, re.I)86 desc = " — ".join(x for x in [87 tagline.capitalize(), price_line,88 "Fiche détaillée : " + url if url != INDEX_URL else ""] if x)8990 details = {"residence": name}91 listings.append(Listing(92 source=self.source_id,93 external_id=sid,94 url=url,95 title=f"{name} — Colocation étudiante",96 address=address,97 sector="", # non précisé à la source98 city="Québec",99 unit_type="Chambre", # colocation : location à la chambre100 price_label=price_line, # « ... à partir de 545$/mois »101 availability="", # aucune disponibilité affichée102 furnished=furnished,103 description=desc[:600],104 amenities=[t for t in [105 "Colocation étudiante",106 f"Unités de {m.group(1).strip()}" if m else ""] if t],107 details=details,108 images=images,109 ))110 return listings111