# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/location_sb.py : connecteur Location Résidentielle SB # (locationresidentielle.com — jumelés, appartements et maisons à louer à # Saint-Georges, Beauce). WordPress thème RealHomes rendu serveur : la page # /a-louer/ liste des cartes
(titre, # extrait, vignette, lien /property//). Petit inventaire (2-3 fiches # actives) ; on écarte les cartes « liste d'attente » et « NON # DISPONIBLE ». La page détail (via cache BD) fournit adresse, chambres, # salles de bain, prix ($/mois — souvent « communiquez avec nous »), # disponibilité, caractéristiques (
  • ) et photos. # ----------------------------------------------------------------------------- from __future__ import annotations import hashlib import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type, parse_price from .base import BaseConnector BASE = "https://locationresidentielle.com" LIST_URL = f"{BASE}/a-louer/" CITY = "Saint-Georges" SLUG_RE = re.compile(r"/property/([^/?#]+)") # cartes qui ne sont pas de vraies annonces SKIP_RE = re.compile(r"liste\s+d.attente|inscription|non\s+disponible", re.I) ADDR_RE = re.compile(r"(\d{3,6},?\s+\d{1,3}e\s+(?:Rue|Avenue),?\s+" r"(?:Saint|St)-Georges(?:,?\s+G\d[A-Z]\s*\d[A-Z]\d)?)", re.I) BED_RE = re.compile(r"(\d+)\s*(?:grandes?\s*)?chambres", re.I) BATH_RE = re.compile(r"(\d+)\s*salles? de bain", re.I) PRICE_RE = re.compile(r"(\d[\d\s]{2,6})\s*\$\s*/?\s*mois", re.I) DISPO_RE = re.compile(r"(?:Disponibilit[ée]\s*:\s*([^\n]{2,60})|" r"((?:CESSION DE BAIL\s+)?DISPONIBLE EN [A-ZÉÛ]+))") TYPE_RE = re.compile(r"\d\s*1/2") UNIT_TEL_RE = re.compile(r"\b(\d{3})[\s.\-](\d{3})[\s.\-](\d{4})\b") IMG_RE = re.compile(r'(?:src|href)="(https://locationresidentielle\.com/' r'wp-content/uploads/[^"]+\.(?:jpe?g|png|webp))"', re.I) IMG_SKIP_RE = re.compile(r"logo|signature|carte|a_louer|" r"jumeles-appartements|-\d{2,3}x\d{2,3}\.", re.I) class LocationSbConnector(BaseConnector): source_id = "location_sb" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: list[Listing] = [] html = self.get(LIST_URL).text soup = BeautifulSoup(html, "html.parser") for item in soup.select("article.property-item"): try: a = item.select_one("h4 a[href]") if not a: continue title = re.sub(r"[•\s]+", " ", a.get_text(strip=True)).strip() if not title or SKIP_RE.search(title): continue m = SLUG_RE.search(a["href"]) if not m: continue slug = m.group(1) url = f"{BASE}/property/{slug}/" excerpt = "" p = item.find("p") if p: excerpt = re.sub( r"\s+", " ", p.get_text(" ", strip=True)).replace( "Plus d'informations", "").strip(" …-") key = hashlib.sha1( f"{url}|{title}|{excerpt}".encode()).hexdigest() try: d = self.detail(slug, key, lambda u=url: self._detail(u)) except Exception: d = {} tm = TYPE_RE.search(title) unit_type = normalize_unit_type(tm.group(0)) if tm else "" listings.append(Listing( source=self.source_id, external_id=slug, url=url, title=title, address=d.get("address") or f"{title}, Saint-Georges", city=CITY, unit_type=unit_type, bedrooms=d.get("bedrooms"), bathrooms=d.get("bathrooms"), price=d.get("price"), price_label=d.get("price_label", ""), availability=d.get("availability", ""), description=d.get("description") or excerpt, amenities=d.get("amenities") or [], details=d.get("details") or {}, images=d.get("images") or [], )) except Exception: continue return listings # -- page détail /property// (thème RealHomes) ------------------------ def _detail(self, url: str) -> dict: html = self.get(url).text soup = BeautifulSoup(html, "html.parser") art = soup.find("article") or soup txt = re.sub(r"[ \t]+", " ", art.get_text("\n", strip=True)) flat = re.sub(r"\s+", " ", txt) out: dict = {} m = ADDR_RE.search(flat) if m: addr = re.sub(r"\s+", " ", m.group(1)).strip() if not re.search(r"saint-georges", addr, re.I): addr = re.sub(r"St-Georges", "Saint-Georges", addr, flags=re.I) out["address"] = addr m = BED_RE.search(flat) if m: out["bedrooms"] = float(m.group(1)) m = BATH_RE.search(flat) if m: out["bathrooms"] = float(m.group(1)) m = PRICE_RE.search(flat) if m: out["price"] = parse_price(m.group(0)) out["price_label"] = f"{m.group(1).strip()} $/mois" elif re.search(r"Prix\s*:\s*communiquez", flat, re.I): out["price_label"] = "Prix : communiquez avec nous" m = DISPO_RE.search(txt) if m: out["availability"] = (m.group(1) or m.group(2) or "") \ .strip().capitalize() # caractéristiques / inclusions : listes
  • du contenu amenities: list[str] = [] for li in art.find_all("li"): t = re.sub(r"\s+", " ", li.get_text(" ", strip=True)) t = re.sub(r"^[^\w]+|►.*$", "", t).strip() if (t and 3 <= len(t) < 80 and "ICI" not in t and not re.search(r"estimez|inscrivez", t, re.I) and t not in amenities): amenities.append(t) out["amenities"] = amenities[:20] details: dict = {} pm = UNIT_TEL_RE.search(flat) if pm: details["contact"] = { "phone": f"{pm.group(1)}-{pm.group(2)}-{pm.group(3)}"} if details: out["details"] = details out["images"] = [u for u in dict.fromkeys(IMG_RE.findall(html)) if not IMG_SKIP_RE.search(u)][:15] return out