# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/alias_appartements.py : connecteur Alias Appartements # (aliasappartements.com — rue des Mouettes, Jonquière, ville de Saguenay ; # Groupe Boudreault). Projet locatif neuf UNIQUE de 80 unités 4½ livré en # 4 phases (2023-2025). WordPress + Elementor + JetElements rendu serveur, # page projet unique SANS fiches d'unités : le site publie le nombre # d'unités (« 80 unités »), le type (« 4 1/2 »), le loyer plancher # (« À partir de 1575$ »), l'adresse (h3 « Rue des Mouettes, Jonquière »), # les caractéristiques (carrousel JetElements) et des photos — disponibilités # par formulaire/contact. -> 1 annonce « catalogue » à granularité PROJET # (précédent girs/immeubles_guillot), availability vide, rien d'inventé ; # price via price_label (« à partir de », plus bas loyer). # external_id stable : rue-des-mouettes. # robots.txt Yoast ouvert (Disallow vide), sitemap public. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type from .base import BaseConnector BASE = "https://www.aliasappartements.com" _UNITS_RE = re.compile(r"^\d+\s+unités$", re.I) _TYPE_RE = re.compile(r"^([1-6])\s*(?:½|1/2)\s*$") _PRICE_RE = re.compile(r"^à\s+partir\s+de\s+[\d\s]+\$", re.I) _SKIP_IMG = re.compile(r"\.svg$|logo|icon|Group-\d+", re.I) class AliasAppartementsConnector(BaseConnector): source_id = "alias_appartements" request_delay = 0.6 max_images = 10 def fetch(self) -> list[Listing]: soup = BeautifulSoup(self.get(BASE + "/").text, "html.parser") units_txt, unit_type, price_label, address = "", "", "", "" for h in soup.select(".elementor-heading-title"): t = re.sub(r"\s+", " ", h.get_text(" ", strip=True)) if not t: continue if _UNITS_RE.match(t): units_txt = t elif _TYPE_RE.match(t): unit_type = normalize_unit_type(t) elif _PRICE_RE.match(t) and not price_label: price_label = t elif h.name == "h3" and re.search(r"(?i)mouettes", t): address = t if not address and not units_txt: return [] # page méconnaissable : ne rien émettre # caractéristiques : carrousel JetElements amenities: list[str] = [] if units_txt: amenities.append(units_txt) for h5 in soup.select("h5.jet-carousel__item-title"): t = re.sub(r"\s+", " ", h5.get_text(" ", strip=True)) if t and t not in amenities: amenities.append(t) # paragraphes descriptifs (hors pied de page) paras: list[str] = [] for p in soup.select(".elementor-widget-text-editor p"): t = re.sub(r"\s+", " ", p.get_text(" ", strip=True)) if len(t) >= 100 and "©" not in t and len(paras) < 5: paras.append(t) description = "\n".join(paras)[:1600] images: list[str] = [] for img in soup.select("img[src*='/wp-content/uploads/']"): u = img["src"] if (u.startswith("http") and not _SKIP_IMG.search(u) and u not in images): images.append(u) if len(images) >= self.max_images: break return [Listing( source=self.source_id, external_id="rue-des-mouettes", url=BASE + "/", title=f"Alias Appartements — {address or 'Rue des Mouettes, Jonquière'}", address=address or "Rue des Mouettes, Jonquière", sector="Jonquière", city="Saguenay", unit_type=unit_type, price=None, # déduit de price_label (« à partir de ») price_label=price_label, availability="", # disponibilités par formulaire/contact description=description, amenities=amenities[:30], images=images, )]