Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/louis_alexandre.py : connecteur Le Louis-Alexandre5# (condoslouisalexandre.ca — Longueuil, 2100, rue René, près du6# Vieux-Longueuil). WordPress + Elementor rendu serveur : la page7# « Appartements disponibles » liste chaque unité en blocs Elementor8# « Condo #NNN | Superficie de X pi² | n chambres, n sdb | PRIX $ / mois »9# avec image du plan et fiche PDF par unité. Le numéro d'unité (#NNN)10# sert d'identifiant stable.11# -----------------------------------------------------------------------------12from __future__ import annotations1314import re1516from bs4 import BeautifulSoup1718from ..schema import Listing19from .base import BaseConnector2021BASE = "https://condoslouisalexandre.ca"22LIST_URL = f"{BASE}/appartements-disponibles-longueuil/"2324ADDRESS = "2100, rue René, Longueuil"25CITY = "Longueuil"2627UNIT_RE = re.compile(r"^Condo\s*#(\d+)\s*$")28SQFT_RE = re.compile(r"Superficie\s+de\s+([\d\s]+)\s*pi", re.I)29ROOMS_RE = re.compile(r"(Studio|\d\s*chambres?)\s*,\s*(\d)\s*sdb", re.I)30PRICE_RE = re.compile(r"(\d[\d\s]{2,6})\s*\$\s*/\s*mois", re.I)31IMG_RE = re.compile(32 r"https://condoslouisalexandre\.ca/wp-content/uploads/"33 r"[^\"\s\\']+\.(?:jpe?g|png|webp)", re.I)34SKIP_IMG_RE = re.compile(r"logo|icon|favicon|-\d{2,3}x\d{2,3}\.", re.I)353637class LouisAlexandreConnector(BaseConnector):38 source_id = "louis_alexandre"39 request_delay = 0.64041 def fetch(self) -> list[Listing]:42 listings: list[Listing] = []43 try:44 html = self.get(LIST_URL).text45 except Exception:46 return listings47 soup = BeautifulSoup(html, "html.parser")4849 # contact de l'immeuble (mailto:/tel: structurés dans la page)50 contact: dict = {}51 m = re.search(r'href="mailto:([^"?]+)"', html)52 if m:53 contact["email"] = m.group(1).strip().lower()54 m = re.search(r'href="tel:\+?1?[\s.\-]?(\d{3})[\s.\-]?(\d{3})'55 r'[\s.\-]?(\d{4})"', html)56 if m:57 contact["phone"] = f"{m.group(1)}-{m.group(2)}-{m.group(3)}"5859 seen: set[str] = set()60 for node in soup.find_all(string=UNIT_RE):61 try:62 unit_no = UNIT_RE.match(node.strip()).group(1)63 if unit_no in seen:64 continue6566 # bloc de l'unité : on remonte jusqu'au conteneur Elementor67 # qui porte le prix (texte « NNN $ / mois »)68 block = node.parent69 while block is not None and "$" not in block.get_text():70 block = block.parent71 if block is None:72 continue73 text = block.get_text(" | ", strip=True)74 if len(re.findall(r"Condo\s*#", text)) != 1:75 continue # conteneur trop large (plusieurs unités)76 seen.add(unit_no)7778 price = None79 price_label = ""80 m = PRICE_RE.search(text)81 if m:82 price_label = m.group(0)83 try:84 val = float(m.group(1).replace(" ", "")85 .replace(" ", "").replace("\xa0", ""))86 if 300 <= val <= 20000:87 price = val88 except ValueError:89 pass9091 area = None92 m = SQFT_RE.search(text)93 if m:94 try:95 area = float(m.group(1).replace(" ", "")96 .replace("\xa0", ""))97 except ValueError:98 area = None99100 unit_type = ""101 bedrooms = None102 bathrooms = None103 m = ROOMS_RE.search(text)104 if m:105 rooms = m.group(1)106 bathrooms = float(m.group(2))107 if re.match(r"studio", rooms, re.I):108 unit_type = "Studio"109 bedrooms = 0.0110 else:111 n = int(re.match(r"\d+", rooms).group(0))112 bedrooms = float(n)113 unit_type = f"{n + 2}½"114115 # image du plan + fiche PDF : conteneur parent (colonne voisine)116 images: list[str] = []117 pdf = ""118 wrap = block.parent119 while wrap is not None and not wrap.find("img"):120 wrap = wrap.parent121 if wrap is not None and \122 len(re.findall(r"Condo\s*#", wrap.get_text())) == 1:123 for u in dict.fromkeys(IMG_RE.findall(str(wrap))):124 if not SKIP_IMG_RE.search(u):125 images.append(u)126 a = wrap.find("a", href=re.compile(r"\.pdf$", re.I))127 if a:128 pdf = a.get("href", "")129130 details: dict = {}131 if contact:132 details["contact"] = dict(contact)133 if pdf:134 details["plan_pdf"] = pdf135136 amenities = []137 if area:138 amenities.append(f"Superficie de {int(area)} pi²")139 if bathrooms:140 amenities.append(f"{int(bathrooms)} salle(s) de bain")141142 listings.append(Listing(143 source=self.source_id,144 external_id=f"condo-{unit_no}",145 url=LIST_URL,146 title=f"Condo #{unit_no} — Le Louis-Alexandre",147 address=ADDRESS,148 city=CITY,149 unit_type=unit_type,150 bedrooms=bedrooms,151 bathrooms=bathrooms,152 price=price,153 price_label=price_label,154 area_sqft=area,155 amenities=amenities,156 details=details,157 images=images[:10],158 ))159 except Exception:160 continue161 return listings162