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/lemay_fleury.py : connecteur Lemay Fleury Immobilier5# (lemayfleury.com) — Victoriaville, Kingsey Falls, Val-des-Sources6# (Centre-du-Québec). Appartements récents de type condo en formule TOUT7# INCLUS. Site Wix très lourd (fiches de ~6,4 Mo) MAIS rendu serveur : le8# HTML brut contient les cartes d'unités en <h3> « Unité 4 1/2 | à partir9# de 1725$ » suivies des inclusions « (électricité, internet et garage10# inclus) » — un simple GET suffit, pas de Scrapfly. On parcourt les fiches11# immeubles de Victoriaville (/monfette /rheault /anemone /gaby /marcoux)12# et les pages villes (/kingsey-falls /val-des-sources). ⚠️ Chaque page13# embarque AUSSI des blocs partagés « … TOUT INCLUS / COMPLET » identiques14# sur tout le site : seuls les h3 « | à partir de » (spécifiques à la page)15# sont retenus. La page /warwick est vide (aucune carte) — ignorée.16# Granularité = typologie par immeuble. external_id = <page>-<type>-<prix>17# (même convention que trylon : le prix disambiguë deux plans homonymes).18# -----------------------------------------------------------------------------19from __future__ import annotations2021import html as _html22import re2324from ..schema import Listing, normalize_unit_type, strip_accents25from .base import BaseConnector2627BASE = "https://www.lemayfleury.com"2829# (slug de page, ville, adresse/repère)30PAGES: tuple[tuple[str, str, str], ...] = (31 ("monfette", "Victoriaville", "66, rue Monfette, Victoriaville"),32 ("rheault", "Victoriaville", "rue Rheault, Victoriaville"),33 ("anemone", "Victoriaville", "rue de l'Anémone, Victoriaville"),34 ("gaby", "Victoriaville", "rue Gaby, Victoriaville"),35 ("marcoux", "Victoriaville", "rue Marcoux, Victoriaville"),36 ("kingsey-falls", "Kingsey Falls", "Kingsey Falls"),37 ("val-des-sources", "Val-des-Sources",38 "604, boulevard Simoneau, Val-des-Sources"),39)4041H3_RE = re.compile(r"<h3[^>]*>([\s\S]*?)</h3>", re.I)42TAG_RE = re.compile(r"<[^>]+>")43CARD_RE = re.compile(r"^(.{2,40}?)\s*\|\s*à\s+partir\s+de\s*([\d\s ]{3,7})\$",44 re.I)45INCL_RE = re.compile(r"^\((.+)\)$")46UNIT_RE = re.compile(r"(\d)\s*(?:1/2|½)")47DELIVERY_RE = re.compile(r"Livraison\s+\w+\s+20\d\d", re.I)48IMG_RE = re.compile(49 r"https://static\.wixstatic\.com/media/[\w~%]+\.(?:jpg|jpeg|webp)", re.I)505152def _slug(text: str) -> str:53 s = strip_accents(text.lower())54 return re.sub(r"[^a-z0-9]+", "-", s).strip("-")555657def _num(txt: str) -> float | None:58 n = re.sub(r"[\s ]", "", txt or "")59 try:60 return float(n)61 except ValueError:62 return None636465class LemayFleuryConnector(BaseConnector):66 source_id = "lemay_fleury"67 request_delay = 1.268 timeout = 60 # fiches Wix de ~6,4 Mo6970 def fetch(self) -> list[Listing]:71 listings: list[Listing] = []72 for page, city, address in PAGES:73 url = f"{BASE}/{page}"74 try:75 html = self.get(url).text76 except Exception:77 continue78 listings.extend(self._parse_page(page, city, address, url, html))79 return listings8081 def _parse_page(self, page: str, city: str, address: str, url: str,82 html: str) -> list[Listing]:83 # disponibilité de niveau page (ex. « Livraison été 2026 » à l'Anémone)84 availability = ""85 dm = DELIVERY_RE.search(_html.unescape(html))86 if dm:87 availability = re.sub(r"\s+", " ", dm.group(0))8889 images = [u for u in dict.fromkeys(IMG_RE.findall(html))][:10]9091 # flux des <h3> en ordre du document : carte prix puis inclusions92 h3s = [re.sub(r"\s+", " ",93 _html.unescape(TAG_RE.sub(" ", m))).strip()94 for m in H3_RE.findall(html)]9596 listings: list[Listing] = []97 seen: set[tuple[str, float]] = set()98 for i, text in enumerate(h3s):99 m = CARD_RE.match(text)100 if not m:101 continue102 label = re.sub(r"\s+", " ", m.group(1)).strip()103 price = _num(m.group(2))104 if price is None or not (300 <= price <= 5000):105 continue106 if (label, price) in seen: # rendus dupliqués (desktop/mobile)107 continue108 seen.add((label, price))109110 amenities: list[str] = []111 if i + 1 < len(h3s):112 im = INCL_RE.match(h3s[i + 1])113 if im:114 amenities = [a.strip(" .").capitalize() for a in115 re.split(r",|\+| et ", im.group(1))116 if a.strip(" .") and "inclus" not in a.lower()]117 amenities = [a for a in amenities if len(a) > 2]118119 unit_type = ""120 um = UNIT_RE.search(label)121 if um:122 unit_type = normalize_unit_type(f"{um.group(1)} ½")123 elif re.search(r"studio", label, re.I):124 unit_type = "Studio"125126 place = address.split(",")[-2].strip() if address.count(",") >= 2 \127 else address128 ext_id = f"{page}-{_slug(label)}-{int(price)}"129 listings.append(Listing(130 source=self.source_id,131 external_id=ext_id,132 url=url,133 title=f"{label} — Lemay Fleury ({place})",134 address=address,135 sector="",136 city=city,137 unit_type=unit_type,138 price=price,139 price_label=f"à partir de {int(price)}$ tout inclus",140 availability=availability,141 description=f"{label} en formule tout inclus chez Lemay "142 f"Fleury Immobilier ({address}).",143 amenities=amenities,144 images=list(images),145 ))146 return listings147