SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
6.0 KB · 147 lines python
Raw Blame History
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