# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/gestion_1139.py : connecteur Gestion 1139 (gestion1139.com) # Drummondville et environs (Centre-du-Québec). WordPress + FacetWP rendu # serveur : la page d'accueil liste les 40+ immeubles (cartes .fwpl-result : # titre, adresse, typologies, badge .archive-tag « Disponible … » sur les # immeubles qui louent). AUCUN prix ni inventaire par unité sur le site — # seules les cartes portant un badge « Disponible » sont retenues. # Granularité : une annonce par TYPOLOGIE d'un immeuble disponible # (ex. projet-horace-3.5, projet-horace-4.5). # ----------------------------------------------------------------------------- from __future__ import annotations import hashlib import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type from .base import BaseConnector BASE = "https://gestion1139.com" DISPO_RE = re.compile(r"disponible", re.I) # villes plausibles du parc (détection dans le texte de la page immeuble) CITY_RE = re.compile( r"(Drummondville|Saint-Nic[ée]phore|Saint-Charles-de-Drummond|" r"Saint-Cyrille[\w-]*|Saint-Germain[\w-]*|Notre-Dame-du-Bon-Conseil|" r"Wickham|L'Avenir)", re.I) IMG_RE = re.compile(r"https://gestion1139\.com/wp-content/uploads/" r'[^"\'\s\\)]+\.(?:jpe?g|png|webp)', re.I) class Gestion1139Connector(BaseConnector): source_id = "gestion_1139" request_delay = 0.7 def fetch(self) -> list[Listing]: listings: list[Listing] = [] try: html = self.get(BASE + "/").text except Exception: return listings soup = BeautifulSoup(html, "html.parser") seen: set[str] = set() for result in soup.select("div.fwpl-result"): try: tag_el = result.select_one(".archive-tag") tag = tag_el.get_text(" ", strip=True) if tag_el else "" if not tag or not DISPO_RE.search(tag): continue # immeuble sans badge de disponibilité a = result.select_one('a[href*="/immeubles/"]') if a is None: continue url = a.get("href", "").split("?")[0] slug = url.rstrip("/").split("/")[-1] if not slug or slug in seen: continue seen.add(slug) title_el = result.select_one(".listing_title a") name = (title_el.get_text(" ", strip=True) if title_el else slug) addr_el = result.select_one("p.adresse") address = (re.sub(r"\s+", " ", addr_el.get_text(" ", strip=True)) if addr_el else "") card_key = hashlib.sha1( f"{tag}|{name}|{address}".encode("utf-8")).hexdigest() d = self.detail(slug, card_key, lambda url=url: self._fetch_detail(url)) if d.get("address"): address = d["address"] city = d.get("city") or "Drummondville" units = d.get("units") or [{"unit_type": "", "extra": []}] for u in units: ut = u.get("unit_type", "") suffix = ut.replace("½", ".5").replace(" ", "") or "imm" listings.append(Listing( source=self.source_id, external_id=f"{slug}-{suffix}", url=url, title=f"{ut + ' — ' if ut else ''}{name}", address=address, city=city, unit_type=ut, availability=tag, description=d.get("description", ""), amenities=(u.get("extra") or []) + (d.get("amenities") or []), images=d.get("images") or [], )) except Exception: continue return listings def _fetch_detail(self, url: str) -> dict: """Page immeuble : description, adresse civique, avantages, typologies (« Nos unités » : n ½ / chambres / salles de bain), ville (détectée dans le texte) et photos.""" out: dict = {} try: html = self.get(url).text except Exception: return out soup = BeautifulSoup(html, "html.parser") # typologies units: list[dict] = [] for bloc in soup.select("div.liste_appartement"): ps = [p.get_text(" ", strip=True) for p in bloc.find_all("p") if p.get_text(strip=True)] if not ps: continue units.append({"unit_type": normalize_unit_type(ps[0]), "extra": ps[1:3]}) out["units"] = units # description : premiers paragraphes de la section d'intro paras = [p.get_text(" ", strip=True) for p in soup.select(".uncode_text_column p") if len(p.get_text(strip=True)) > 40] if paras: out["description"] = re.sub(r"\s+", " ", paras[0]).strip()[:900] # adresse civique (ligne « 1645 et 1655 Boulevard Lemire ») text = soup.get_text("\n", strip=True) m = re.search(r"^\d{1,5}[^\n]{3,70}(?:rue|boulevard|boul\.|avenue|" r"chemin|route)[^\n]{0,50}$", text, re.I | re.M) if m: out["address"] = re.sub(r"\s+", " ", m.group(0)).strip() m = CITY_RE.search(text) if m: out["city"] = m.group(1) else: # souvent seulement dans les noms de fichiers photo m = CITY_RE.search(html) if m: out["city"] = m.group(1).capitalize() # avantages (liste « Les avantages ») out["amenities"] = [ li.get_text(" ", strip=True) for li in soup.select(".uncode_text_column li") if 0 < len(li.get_text(strip=True)) <= 80][:15] out["images"] = [u for u in dict.fromkeys(IMG_RE.findall(html)) if not re.search(r"logo|icon|favicon|-\d{2,3}x\d{2,3}\.", u, re.I)][:20] return out