spb/lou-ka Public
Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 99.7%
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