SPB Git

spb/lou-ka Public

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

HTML 99.7%
6.3 KB · 158 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/luxuor.py : connecteur Les Immeubles Luxuor5#   (lesimmeublesluxuor.com — ville de Saguenay). Condos locatifs neufs haut de6#   gamme rue des Golfeurs (arrondissement Chicoutimi, projets « 42 logements »7#   et « 16 logements »), plex « Quadrex » et résidence étudiante « La8#   Fabrique » (3821 rue de la Fabrique, Jonquière). Site Wix rendu serveur,9#   4 pages projet FIXES sans fiches d'unités ni prix ni disponibilités10#   (visites par formulaire) -> 1 annonce « catalogue » par projet, granularité11#   PROJET (précédent girs/immeubles_guillot/alias_appartements), price=None,12#   rien d'inventé. Texte extrait des blocs Wix `div[data-testid=13#   "richTextElement"]` du <main> (les menus, légendes de galerie et pieds de14#   page sont hors de ces blocs) : lignes courtes = pictogrammes de commodités15#   (« Ascenseur », « Gym »…), lignes longues = description. Types multiples16#   (3½-5½) cités en prose -> unit_type vide, la description fait foi.17#   external_id stable : slug de page. robots.txt Wix standard (ouvert).18# -----------------------------------------------------------------------------19from __future__ import annotations2021import re2223from bs4 import BeautifulSoup2425from ..schema import Listing26from .base import BaseConnector2728BASE = "https://www.lesimmeublesluxuor.com"2930# (slug, secteur affiché par la page, extraire les pictogrammes ?)31PAGES = [32    ("42logements", "Chicoutimi", True),33    ("16logements", "Chicoutimi", True),34    ("quadrex", "", True),35    ("lafabrique", "Jonquière", False),   # sections Chambre 1/Cuisine… : prose36]3738_ADDR_RE = re.compile(r"\d{2,5},?\s+rue de la Fabrique[^,]*,\s*Jonquière"39                      r"(?:,\s*QC(?:\s+\w\d\w\s*\d\w\d)?)?", re.I)40_GOLFEURS_RE = re.compile(r"rue des Golfeurs", re.I)41_TYPE_RE = re.compile(r"([1-6])\s*(?:½|1/2)")42# lignes courtes à ne PAS traiter comme commodités (titres, CTA…)43_SKIP_LINE = re.compile(44    r"(?i)^(photos?\s*&\s*plans?|un projet qui|réservez|laissez|découvrez|"45    r"venez|nos logements|type de logement|contact)", re.I)464748def _full_size(url: str) -> str:49    """URL wixstatic redimensionnée -> média pleine taille (coupe au /v1/)."""50    return url.split("/v1/")[0] if "/v1/" in url else url515253class LuxuorConnector(BaseConnector):54    source_id = "luxuor"55    request_delay = 0.656    max_images = 105758    def fetch(self) -> list[Listing]:59        listings: list[Listing] = []60        for slug, sector, with_amenities in PAGES:61            try:62                lst = self._parse_page(slug, sector, with_amenities)63            except Exception:64                continue65            if lst is not None:66                listings.append(lst)67        return listings6869    # -- page projet Wix -----------------------------------------------------------70    def _parse_page(self, slug: str, sector: str,71                    with_amenities: bool) -> Listing | None:72        url = f"{BASE}/{slug}"73        soup = BeautifulSoup(self.get(url).text, "html.parser")74        main = soup.select_one("main") or soup.body75        if main is None:76            return None7778        blocks = main.select('div[data-testid="richTextElement"]')79        if not blocks:80            return None8182        # lignes de texte au niveau des <p> (une commodité Wix par <p>)83        lines: list[str] = []84        for blk in blocks:85            ps = blk.select("p")86            if ps:87                lines += [re.sub(r"\s+", " ", p.get_text(" ", strip=True))88                          .replace("", "").strip() for p in ps]89            else:90                lines.append(re.sub(r"\s+", " ", blk.get_text(" ", strip=True))91                             .replace("", "").strip())92        lines = [t for t in lines if t]93        if not lines:94            return None9596        # titre = premier(s) bloc(s) court(s) (« 42 LOGEMENTS », « Résidence97        # étudiante / LA FABRIQUE »)98        head = re.sub(r"\s+", " ", blocks[0].get_text(" ", strip=True)99                      ).replace("", "").strip()100        if slug == "lafabrique" and len(blocks) > 1:101            head = f"{head} {blocks[1].get_text(' ', strip=True)}".strip()102        name = head.title() if head.isupper() else head103104        paras: list[str] = []105        amenities: list[str] = []106        for t in lines:107            if t == head or _SKIP_LINE.match(t):108                continue109            if len(t) >= 45:110                if len(paras) < 10:111                    paras.append(t)112            elif (with_amenities and 3 <= len(t) < 45113                    and not t.endswith((".", "!", "?", ":"))114                    and t not in amenities):115                amenities.append(t)116        description = "\n".join(paras)[:1600]117        if not description and not amenities:118            return None                     # page méconnaissable119120        text = " ".join(lines)121        m_addr = _ADDR_RE.search(text)122        if m_addr:123            address = m_addr.group(0)124        elif _GOLFEURS_RE.search(text):125            address = "Rue des Golfeurs, Chicoutimi"126        else:127            address = ""128129        # type d'unités : seulement si UN seul type est cité en prose130        types = sorted(set(_TYPE_RE.findall(text)))131        unit_type = f"{types[0]}½" if len(types) == 1 else ""132133        images: list[str] = []134        for img in main.select("img[src]"):135            u = _full_size(img["src"])136            if (u.startswith("https://static.wixstatic.com")137                    and not u.lower().endswith(".svg") and u not in images):138                images.append(u)139                if len(images) >= self.max_images:140                    break141142        return Listing(143            source=self.source_id,144            external_id=slug,145            url=url,146            title=f"Les Immeubles Luxuor — {name}",147            address=address,148            sector=sector,149            city="Saguenay",       # tous les projets du site sont à Ville Saguenay150            unit_type=unit_type,151            price=None,             # aucun loyer publié sur le site152            price_label="",153            availability="",        # visites par formulaire : rien d'inventé154            description=description,155            amenities=amenities[:30],156            images=images,157        )158