Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/helio.py : connecteur Entreprises Hélio (entrepriseshelio.ca)5# Trois immeubles en Montérégie : 2417 Montarville et 335-345 Place du6# Collège à Longueuil, 1065-1075 boul. de Palerme à La Prairie.7# WordPress + Divi rendu serveur : la page /a-louer/ liste une section par8# typologie (h3 « 3 1/2, Place du Collège », « 4 1/2, Palerme »…) suivie9# d'un encadré « Disponibilité : <valeur> » et des caractéristiques (li).10# Une typologie marquée « COMPLET » n'est PAS ingérée : seules les sections11# avec une vraie disponibilité (date ou « disponible ») deviennent des12# annonces. Il est donc normal que ce connecteur retourne 0 annonce quand13# tout le parc est loué (état observé au 2026-08-25).14# -----------------------------------------------------------------------------15from __future__ import annotations1617import re1819from bs4 import BeautifulSoup2021from ..schema import Listing, normalize_unit_type22from .base import BaseConnector2324BASE = "https://www.entrepriseshelio.ca"25LIST_URL = f"{BASE}/a-louer/"2627# immeuble (mot-clé du titre de section) -> adresse et ville28BUILDINGS = {29 "montarville": ("2417, rue de Montarville, Longueuil", "Longueuil"),30 "college": ("335-345, place du Collège, Longueuil", "Longueuil"),31 "palerme": ("1065-1075, boulevard de Palerme, La Prairie", "La Prairie"),32}3334# titre de section d'unité : « 3 1/2, Place du Collège – Demi Sous-Sol »35TITLE_RE = re.compile(r"^(\d\s*1/2)\s*,\s*(.+)$")36COMPLET_RE = re.compile(r"\bCOMPLET\b", re.I)37IMG_RE = re.compile(38 r"https://www\.entrepriseshelio\.ca/wp-content/uploads/"39 r"[^\"\s\\']+\.(?:jpe?g|png|webp)", re.I)40SKIP_IMG_RE = re.compile(r"logo|icon|favicon|-\d{2,3}x\d{2,3}\.", re.I)414243def _slug(text: str) -> str:44 from ..schema import strip_accents45 s = strip_accents(text.lower())46 return re.sub(r"-{2,}", "-", re.sub(r"[^a-z0-9]+", "-", s)).strip("-")474849class HelioConnector(BaseConnector):50 source_id = "helio"51 request_delay = 0.65253 def fetch(self) -> list[Listing]:54 listings: list[Listing] = []55 try:56 html = self.get(LIST_URL).text57 except Exception:58 return listings59 soup = BeautifulSoup(html, "html.parser")6061 for h3 in soup.find_all("h3"):62 try:63 if "et_pb_gallery_title" in (h3.get("class") or []):64 continue # légendes de galerie Divi65 title = h3.get_text(" ", strip=True)66 m = TITLE_RE.match(title)67 if not m:68 continue69 unit_type = normalize_unit_type(m.group(1))70 where = m.group(2).strip()7172 # section Divi englobante (colonne/row) : dispo + listes73 section = h374 for _ in range(8):75 section = section.parent76 if section is None:77 break78 cls = " ".join(section.get("class") or [])79 txt = section.get_text(" ", strip=True)80 if "et_pb_row" in cls and "Disponibilité" in txt:81 break82 if section is None:83 continue84 stext = section.get_text(" | ", strip=True)8586 # disponibilité : valeur qui suit « Disponibilité : »87 availability = ""88 m2 = re.search(r"Disponibilité\s*\|?\s*:\s*\|?\s*([^|]+)",89 stext)90 if m2:91 availability = m2.group(1).strip()92 if COMPLET_RE.search(availability):93 continue # typologie complète : exclue94 if not availability:95 continue # rien d'affiché : prudence9697 # immeuble -> adresse/ville98 key = _slug(where)99 address, city = "", ""100 for kw, (addr, ct) in BUILDINGS.items():101 if kw in key:102 address, city = addr, ct103 break104105 amenities = [li.get_text(" ", strip=True)106 for li in section.find_all("li")107 if li.get_text(strip=True)108 and "photos sont représentatives"109 not in li.get_text()][:20]110 images = [u for u in dict.fromkeys(IMG_RE.findall(str(section)))111 if not SKIP_IMG_RE.search(u)][:15]112113 listings.append(Listing(114 source=self.source_id,115 external_id=f"{unit_type.replace('½', '.5')}-{key}",116 url=LIST_URL,117 title=title,118 address=address,119 city=city,120 unit_type=unit_type,121 availability=availability,122 amenities=amenities,123 images=images,124 ))125 except Exception:126 continue127 return listings128