# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/luxuor.py : connecteur Les Immeubles Luxuor # (lesimmeublesluxuor.com — ville de Saguenay). Condos locatifs neufs haut de # gamme rue des Golfeurs (arrondissement Chicoutimi, projets « 42 logements » # et « 16 logements »), plex « Quadrex » et résidence étudiante « La # Fabrique » (3821 rue de la Fabrique, Jonquière). Site Wix rendu serveur, # 4 pages projet FIXES sans fiches d'unités ni prix ni disponibilités # (visites par formulaire) -> 1 annonce « catalogue » par projet, granularité # PROJET (précédent girs/immeubles_guillot/alias_appartements), price=None, # rien d'inventé. Texte extrait des blocs Wix `div[data-testid= # "richTextElement"]` du
(les menus, légendes de galerie et pieds de # page sont hors de ces blocs) : lignes courtes = pictogrammes de commodités # (« Ascenseur », « Gym »…), lignes longues = description. Types multiples # (3½-5½) cités en prose -> unit_type vide, la description fait foi. # external_id stable : slug de page. robots.txt Wix standard (ouvert). # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing from .base import BaseConnector BASE = "https://www.lesimmeublesluxuor.com" # (slug, secteur affiché par la page, extraire les pictogrammes ?) PAGES = [ ("42logements", "Chicoutimi", True), ("16logements", "Chicoutimi", True), ("quadrex", "", True), ("lafabrique", "Jonquière", False), # sections Chambre 1/Cuisine… : prose ] _ADDR_RE = re.compile(r"\d{2,5},?\s+rue de la Fabrique[^,]*,\s*Jonquière" r"(?:,\s*QC(?:\s+\w\d\w\s*\d\w\d)?)?", re.I) _GOLFEURS_RE = re.compile(r"rue des Golfeurs", re.I) _TYPE_RE = re.compile(r"([1-6])\s*(?:½|1/2)") # lignes courtes à ne PAS traiter comme commodités (titres, CTA…) _SKIP_LINE = re.compile( r"(?i)^(photos?\s*&\s*plans?|un projet qui|réservez|laissez|découvrez|" r"venez|nos logements|type de logement|contact)", re.I) def _full_size(url: str) -> str: """URL wixstatic redimensionnée -> média pleine taille (coupe au /v1/).""" return url.split("/v1/")[0] if "/v1/" in url else url class LuxuorConnector(BaseConnector): source_id = "luxuor" request_delay = 0.6 max_images = 10 def fetch(self) -> list[Listing]: listings: list[Listing] = [] for slug, sector, with_amenities in PAGES: try: lst = self._parse_page(slug, sector, with_amenities) except Exception: continue if lst is not None: listings.append(lst) return listings # -- page projet Wix ----------------------------------------------------------- def _parse_page(self, slug: str, sector: str, with_amenities: bool) -> Listing | None: url = f"{BASE}/{slug}" soup = BeautifulSoup(self.get(url).text, "html.parser") main = soup.select_one("main") or soup.body if main is None: return None blocks = main.select('div[data-testid="richTextElement"]') if not blocks: return None # lignes de texte au niveau des

(une commodité Wix par

) lines: list[str] = [] for blk in blocks: ps = blk.select("p") if ps: lines += [re.sub(r"\s+", " ", p.get_text(" ", strip=True)) .replace("", "").strip() for p in ps] else: lines.append(re.sub(r"\s+", " ", blk.get_text(" ", strip=True)) .replace("", "").strip()) lines = [t for t in lines if t] if not lines: return None # titre = premier(s) bloc(s) court(s) (« 42 LOGEMENTS », « Résidence # étudiante / LA FABRIQUE ») head = re.sub(r"\s+", " ", blocks[0].get_text(" ", strip=True) ).replace("", "").strip() if slug == "lafabrique" and len(blocks) > 1: head = f"{head} {blocks[1].get_text(' ', strip=True)}".strip() name = head.title() if head.isupper() else head paras: list[str] = [] amenities: list[str] = [] for t in lines: if t == head or _SKIP_LINE.match(t): continue if len(t) >= 45: if len(paras) < 10: paras.append(t) elif (with_amenities and 3 <= len(t) < 45 and not t.endswith((".", "!", "?", ":")) and t not in amenities): amenities.append(t) description = "\n".join(paras)[:1600] if not description and not amenities: return None # page méconnaissable text = " ".join(lines) m_addr = _ADDR_RE.search(text) if m_addr: address = m_addr.group(0) elif _GOLFEURS_RE.search(text): address = "Rue des Golfeurs, Chicoutimi" else: address = "" # type d'unités : seulement si UN seul type est cité en prose types = sorted(set(_TYPE_RE.findall(text))) unit_type = f"{types[0]}½" if len(types) == 1 else "" images: list[str] = [] for img in main.select("img[src]"): u = _full_size(img["src"]) if (u.startswith("https://static.wixstatic.com") and not u.lower().endswith(".svg") and u not in images): images.append(u) if len(images) >= self.max_images: break return Listing( source=self.source_id, external_id=slug, url=url, title=f"Les Immeubles Luxuor — {name}", address=address, sector=sector, city="Saguenay", # tous les projets du site sont à Ville Saguenay unit_type=unit_type, price=None, # aucun loyer publié sur le site price_label="", availability="", # visites par formulaire : rien d'inventé description=description, amenities=amenities[:30], images=images, )