# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/immogex.py : connecteur Immogex (immogex.com) # GoDaddy Website Builder 8. La page « À louer » expose chaque logement en # bloc ABOUT_* (data-aid) : HEADLINE = « Nom - prix$ », DESCRIPTION = texte # brut (Disponible…, Grandeur : 3 ½, Adresse : …, caractéristiques), # IMAGE (data-srclazy, img1.wsimg.com). 1 seule requête par sync ; # ville fixe Drummondville (tout le parc Immogex y est — voir rapport). # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type, parse_price, strip_accents from .base import BaseConnector BASE = "https://immogex.com" LIST_URL = f"{BASE}/%C3%A0-louer" _GRANDEUR_RE = re.compile(r"Grandeur\s*:\s*([^\n]+)", re.I) _ADRESSE_RE = re.compile(r"Adresse\s*:\s*([^\n]+)", re.I) _DISPO_RE = re.compile(r"(Disponible[^\n]*)", re.I) def _slug(txt: str) -> str: """Slug stable dérivé du texte source (nom + n° civique).""" s = strip_accents(txt.lower()) return re.sub(r"-{2,}", "-", re.sub(r"[^a-z0-9]+", "-", s)).strip("-") class ImmogexConnector(BaseConnector): source_id = "immogex" request_delay = 0.6 def fetch(self) -> list[Listing]: soup = BeautifulSoup(self.get(LIST_URL).text, "html.parser") listings: dict[str, Listing] = {} # blocs ABOUT_HEADLINE_RENDERED / ABOUT_DESCRIPTION_RENDERED / # ABOUT_IMAGE_RENDERED : appariés par le suffixe du data-aid for head in soup.select("[data-aid^=ABOUT_HEADLINE_RENDERED]"): try: self._parse_block(soup, head, listings) except Exception: continue return list(listings.values()) def _parse_block(self, soup, head, listings: dict[str, Listing]) -> None: suffix = head["data-aid"].replace("ABOUT_HEADLINE_RENDERED", "") title = head.get_text(" ", strip=True) if not title: return # exclusions : locaux commerciaux, stationnements, rangements if re.search(r"commercial|bureau|stationnement|rangement|entrep[ôo]t", title, re.I): return desc_el = soup.select_one(f'[data-aid="ABOUT_DESCRIPTION_RENDERED{suffix}"]') description = desc_el.get_text("\n", strip=True) if desc_el else "" # « Jardins de la Rivia I - 1355$ » -> nom + étiquette de prix price_label = "" name = title m = re.search(r"^(.*?)[\s–-]+(\d[\d\s,.]*\$)\s*$", title) if m: name, price_label = m.group(1).strip(" -–"), m.group(2).strip() grandeur = _GRANDEUR_RE.search(description) unit_type = normalize_unit_type(grandeur.group(1).strip()) if grandeur else "" adresse = _ADRESSE_RE.search(description) address = adresse.group(1).strip() if adresse else "" dispo = _DISPO_RE.search(description) availability = dispo.group(1).strip() if dispo else "" # id stable : slug du nom + n° civique (pas d'id ni de fiche chez GoDaddy) civic = re.match(r"(\d+)", address) ext_id = _slug(f"{name}-{civic.group(1) if civic else ''}") if not ext_id or ext_id in listings: return images = [] img = soup.select_one(f'[data-aid="ABOUT_IMAGE_RENDERED{suffix}"]') if img: src = img.get("data-srclazy") or img.get("src") or "" if src.startswith("//"): src = "https:" + src if src.startswith("http"): images = [src] listings[ext_id] = Listing( source=self.source_id, external_id=ext_id, url=f"{LIST_URL}#{ext_id}", # pas de fiche individuelle title=title, address=address, city="Drummondville", unit_type=unit_type, price=parse_price(price_label), price_label=price_label, availability=availability, description=description[:2000], images=images, )