# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/lemay_fleury.py : connecteur Lemay Fleury Immobilier # (lemayfleury.com) — Victoriaville, Kingsey Falls, Val-des-Sources # (Centre-du-Québec). Appartements récents de type condo en formule TOUT # INCLUS. Site Wix très lourd (fiches de ~6,4 Mo) MAIS rendu serveur : le # HTML brut contient les cartes d'unités en

« Unité 4 1/2 | à partir # de 1725$ » suivies des inclusions « (électricité, internet et garage # inclus) » — un simple GET suffit, pas de Scrapfly. On parcourt les fiches # immeubles de Victoriaville (/monfette /rheault /anemone /gaby /marcoux) # et les pages villes (/kingsey-falls /val-des-sources). ⚠️ Chaque page # embarque AUSSI des blocs partagés « … TOUT INCLUS / COMPLET » identiques # sur tout le site : seuls les h3 « | à partir de » (spécifiques à la page) # sont retenus. La page /warwick est vide (aucune carte) — ignorée. # Granularité = typologie par immeuble. external_id = -- # (même convention que trylon : le prix disambiguë deux plans homonymes). # ----------------------------------------------------------------------------- from __future__ import annotations import html as _html import re from ..schema import Listing, normalize_unit_type, strip_accents from .base import BaseConnector BASE = "https://www.lemayfleury.com" # (slug de page, ville, adresse/repère) PAGES: tuple[tuple[str, str, str], ...] = ( ("monfette", "Victoriaville", "66, rue Monfette, Victoriaville"), ("rheault", "Victoriaville", "rue Rheault, Victoriaville"), ("anemone", "Victoriaville", "rue de l'Anémone, Victoriaville"), ("gaby", "Victoriaville", "rue Gaby, Victoriaville"), ("marcoux", "Victoriaville", "rue Marcoux, Victoriaville"), ("kingsey-falls", "Kingsey Falls", "Kingsey Falls"), ("val-des-sources", "Val-des-Sources", "604, boulevard Simoneau, Val-des-Sources"), ) H3_RE = re.compile(r"]*>([\s\S]*?)

", re.I) TAG_RE = re.compile(r"<[^>]+>") CARD_RE = re.compile(r"^(.{2,40}?)\s*\|\s*à\s+partir\s+de\s*([\d\s ]{3,7})\$", re.I) INCL_RE = re.compile(r"^\((.+)\)$") UNIT_RE = re.compile(r"(\d)\s*(?:1/2|½)") DELIVERY_RE = re.compile(r"Livraison\s+\w+\s+20\d\d", re.I) IMG_RE = re.compile( r"https://static\.wixstatic\.com/media/[\w~%]+\.(?:jpg|jpeg|webp)", re.I) def _slug(text: str) -> str: s = strip_accents(text.lower()) return re.sub(r"[^a-z0-9]+", "-", s).strip("-") def _num(txt: str) -> float | None: n = re.sub(r"[\s ]", "", txt or "") try: return float(n) except ValueError: return None class LemayFleuryConnector(BaseConnector): source_id = "lemay_fleury" request_delay = 1.2 timeout = 60 # fiches Wix de ~6,4 Mo def fetch(self) -> list[Listing]: listings: list[Listing] = [] for page, city, address in PAGES: url = f"{BASE}/{page}" try: html = self.get(url).text except Exception: continue listings.extend(self._parse_page(page, city, address, url, html)) return listings def _parse_page(self, page: str, city: str, address: str, url: str, html: str) -> list[Listing]: # disponibilité de niveau page (ex. « Livraison été 2026 » à l'Anémone) availability = "" dm = DELIVERY_RE.search(_html.unescape(html)) if dm: availability = re.sub(r"\s+", " ", dm.group(0)) images = [u for u in dict.fromkeys(IMG_RE.findall(html))][:10] # flux des

en ordre du document : carte prix puis inclusions h3s = [re.sub(r"\s+", " ", _html.unescape(TAG_RE.sub(" ", m))).strip() for m in H3_RE.findall(html)] listings: list[Listing] = [] seen: set[tuple[str, float]] = set() for i, text in enumerate(h3s): m = CARD_RE.match(text) if not m: continue label = re.sub(r"\s+", " ", m.group(1)).strip() price = _num(m.group(2)) if price is None or not (300 <= price <= 5000): continue if (label, price) in seen: # rendus dupliqués (desktop/mobile) continue seen.add((label, price)) amenities: list[str] = [] if i + 1 < len(h3s): im = INCL_RE.match(h3s[i + 1]) if im: amenities = [a.strip(" .").capitalize() for a in re.split(r",|\+| et ", im.group(1)) if a.strip(" .") and "inclus" not in a.lower()] amenities = [a for a in amenities if len(a) > 2] unit_type = "" um = UNIT_RE.search(label) if um: unit_type = normalize_unit_type(f"{um.group(1)} ½") elif re.search(r"studio", label, re.I): unit_type = "Studio" place = address.split(",")[-2].strip() if address.count(",") >= 2 \ else address ext_id = f"{page}-{_slug(label)}-{int(price)}" listings.append(Listing( source=self.source_id, external_id=ext_id, url=url, title=f"{label} — Lemay Fleury ({place})", address=address, sector="", city=city, unit_type=unit_type, price=price, price_label=f"à partir de {int(price)}$ tout inclus", availability=availability, description=f"{label} en formule tout inclus chez Lemay " f"Fleury Immobilier ({address}).", amenities=amenities, images=list(images), )) return listings