# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/sig.py : connecteur Société d'investissement Gilbert (immo-sig.ca) # Résidences étudiantes en colocation à Québec (Université Laval, Cégep # Sainte-Foy) : Le Sommet, Le Pavillon, La FAC, L'UNI. La page # /residences-etudiantes/ (Elementor, rendue serveur) affiche pour chaque # résidence le prix « à partir de » par chambre, l'adresse et une photo. # Granularité = résidence (aucune unité détaillée publiée). Les fiches # détaillées vivent sur residences-etudiantes.com, protégé par une # vérification anti-bot reCAPTCHA -> immo-sig.ca est la source primaire ; # le lien « DÉCOUVRIR » sert d'URL d'annonce pour l'utilisateur. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, strip_accents from .base import BaseConnector BASE = "https://immo-sig.ca" INDEX_URL = f"{BASE}/residences-etudiantes/" # variantes responsives WordPress (-300x200.jpg) à écarter _VARIANT_IMG = re.compile(r"-\d{2,4}x\d{2,4}\.(?:jpe?g|png|webp)$", re.I) def _slug(name: str) -> str: s = strip_accents((name or "").strip().lower()) return re.sub(r"[^a-z0-9]+", "-", s).strip("-") class SigConnector(BaseConnector): source_id = "sig" request_delay = 0.6 def fetch(self) -> list[Listing]: html = self.get(INDEX_URL).text soup = BeautifulSoup(html, "html.parser") listings: list[Listing] = [] seen: set[str] = set() # une colonne Elementor par résidence : photo +

nom + texte # « COLOCATION ÉTUDIANTE / Unités de N chambres à partir de X$/mois / # adresse » + bouton DÉCOUVRIR for col in soup.select(".elementor-column"): h3 = col.select_one("h3.elementor-heading-title") text_w = col.select_one(".elementor-widget-text-editor") if not h3 or not text_w: continue name = re.sub(r"\s+", " ", h3.get_text(" ", strip=True)) sid = _slug(name) if not name or sid in seen: continue paras = [re.sub(r"[ \t]+", " ", p.get_text("\n", strip=True)) for p in text_w.find_all("p")] tagline = paras[0] if paras else "" body = paras[1] if len(paras) > 1 else "" if "à partir de" not in body.lower(): continue # pas un bloc résidence seen.add(sid) # « Unités de 3 chambres à partir de 650$/mois\n2333 Chapdelaine » lines = [l.strip() for l in body.split("\n") if l.strip()] price_line = lines[0] if lines else "" address = lines[1] if len(lines) > 1 else "" img = col.select_one("img[src]") images = [] if img: src = (img.get("src") or "").strip() if src.startswith("http") and not _VARIANT_IMG.search(src): images.append(src) link = col.select_one("a.elementor-button[href]") url = (link["href"] if link and str(link["href"]).startswith("http") else INDEX_URL) # « meublées » n'est indiqué que pour L'UNI furnished = True if "meublé" in price_line.lower() else None m = re.search(r"unités?\s+de\s+([\d\s aà-]+chambres?)", price_line, re.I) desc = " — ".join(x for x in [ tagline.capitalize(), price_line, "Fiche détaillée : " + url if url != INDEX_URL else ""] if x) details = {"residence": name} listings.append(Listing( source=self.source_id, external_id=sid, url=url, title=f"{name} — Colocation étudiante", address=address, sector="", # non précisé à la source city="Québec", unit_type="Chambre", # colocation : location à la chambre price_label=price_line, # « ... à partir de 545$/mois » availability="", # aucune disponibilité affichée furnished=furnished, description=desc[:600], amenities=[t for t in [ "Colocation étudiante", f"Unités de {m.group(1).strip()}" if m else ""] if t], details=details, images=images, )) return listings