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/immogex.py : connecteur Immogex (immogex.com)5# GoDaddy Website Builder 8. La page « À louer » expose chaque logement en6# bloc ABOUT_* (data-aid) : HEADLINE = « Nom - prix$ », DESCRIPTION = texte7# brut (Disponible…, Grandeur : 3 ½, Adresse : …, caractéristiques),8# IMAGE (data-srclazy, img1.wsimg.com). 1 seule requête par sync ;9# ville fixe Drummondville (tout le parc Immogex y est — voir rapport).10# -----------------------------------------------------------------------------11from __future__ import annotations1213import re1415from bs4 import BeautifulSoup1617from ..schema import Listing, normalize_unit_type, parse_price, strip_accents18from .base import BaseConnector1920BASE = "https://immogex.com"21LIST_URL = f"{BASE}/%C3%A0-louer"2223_GRANDEUR_RE = re.compile(r"Grandeur\s*:\s*([^\n]+)", re.I)24_ADRESSE_RE = re.compile(r"Adresse\s*:\s*([^\n]+)", re.I)25_DISPO_RE = re.compile(r"(Disponible[^\n]*)", re.I)262728def _slug(txt: str) -> str:29 """Slug stable dérivé du texte source (nom + n° civique)."""30 s = strip_accents(txt.lower())31 return re.sub(r"-{2,}", "-", re.sub(r"[^a-z0-9]+", "-", s)).strip("-")323334class ImmogexConnector(BaseConnector):35 source_id = "immogex"36 request_delay = 0.63738 def fetch(self) -> list[Listing]:39 soup = BeautifulSoup(self.get(LIST_URL).text, "html.parser")40 listings: dict[str, Listing] = {}41 # blocs ABOUT_HEADLINE_RENDERED<n> / ABOUT_DESCRIPTION_RENDERED<n> /42 # ABOUT_IMAGE_RENDERED<n> : appariés par le suffixe du data-aid43 for head in soup.select("[data-aid^=ABOUT_HEADLINE_RENDERED]"):44 try:45 self._parse_block(soup, head, listings)46 except Exception:47 continue48 return list(listings.values())4950 def _parse_block(self, soup, head, listings: dict[str, Listing]) -> None:51 suffix = head["data-aid"].replace("ABOUT_HEADLINE_RENDERED", "")52 title = head.get_text(" ", strip=True)53 if not title:54 return55 # exclusions : locaux commerciaux, stationnements, rangements56 if re.search(r"commercial|bureau|stationnement|rangement|entrep[ôo]t",57 title, re.I):58 return5960 desc_el = soup.select_one(f'[data-aid="ABOUT_DESCRIPTION_RENDERED{suffix}"]')61 description = desc_el.get_text("\n", strip=True) if desc_el else ""6263 # « Jardins de la Rivia I - 1355$ » -> nom + étiquette de prix64 price_label = ""65 name = title66 m = re.search(r"^(.*?)[\s–-]+(\d[\d\s,.]*\$)\s*$", title)67 if m:68 name, price_label = m.group(1).strip(" -–"), m.group(2).strip()6970 grandeur = _GRANDEUR_RE.search(description)71 unit_type = normalize_unit_type(grandeur.group(1).strip()) if grandeur else ""72 adresse = _ADRESSE_RE.search(description)73 address = adresse.group(1).strip() if adresse else ""74 dispo = _DISPO_RE.search(description)75 availability = dispo.group(1).strip() if dispo else ""7677 # id stable : slug du nom + n° civique (pas d'id ni de fiche chez GoDaddy)78 civic = re.match(r"(\d+)", address)79 ext_id = _slug(f"{name}-{civic.group(1) if civic else ''}")80 if not ext_id or ext_id in listings:81 return8283 images = []84 img = soup.select_one(f'[data-aid="ABOUT_IMAGE_RENDERED{suffix}"]')85 if img:86 src = img.get("data-srclazy") or img.get("src") or ""87 if src.startswith("//"):88 src = "https:" + src89 if src.startswith("http"):90 images = [src]9192 listings[ext_id] = Listing(93 source=self.source_id,94 external_id=ext_id,95 url=f"{LIST_URL}#{ext_id}", # pas de fiche individuelle96 title=title,97 address=address,98 city="Drummondville",99 unit_type=unit_type,100 price=parse_price(price_label),101 price_label=price_label,102 availability=availability,103 description=description[:2000],104 images=images,105 )106