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/novo_stjean.py : connecteur NOVO St-Jean (novostjean.com)5# Condos locatifs tout-inclus au 201, rue Saint-Georges,6# Saint-Jean-sur-Richelieu (projet Atimco). ⚠️ Compagnie distincte du7# connecteur `novelo`. WordPress + Bricks rendu serveur : la page d'accueil8# liste 4 typologies en blocs .units_item — « 3½ », « 3½ + den », « 4½ »,9# « 4½ + den » avec prix plancher (« À partir de 1370$ / mois »). Une10# annonce par typologie (pas d'inventaire unité par unité public) ; les11# inclusions (électroménagers, eau chaude, électricité, clim, câble,12# Wi-Fi) viennent de la grille .inclusion_grid-item et la mention13# « Disponible dès maintenant » sert de disponibilité.14# -----------------------------------------------------------------------------15from __future__ import annotations1617import re1819from bs4 import BeautifulSoup2021from ..schema import Listing, normalize_unit_type, parse_price22from .base import BaseConnector2324BASE = "https://www.novostjean.com"25LIST_URL = f"{BASE}/"2627ADDRESS = "201, rue Saint-Georges, Saint-Jean-sur-Richelieu"28CITY = "Saint-Jean-sur-Richelieu"2930TYPE_RE = re.compile(r"^(\d)\s*\|?\s*1/2$")31PRICE_RE = re.compile(r"(\d[\d\s ]{2,6})\s*\$\s*/\s*mois", re.I)32IMG_RE = re.compile(33 r"https://www\.novostjean\.com/wp-content/uploads/[^\"\s\\']+"34 r"\.(?:jpe?g|png|webp)", re.I)35SKIP_IMG_RE = re.compile(36 r"logo|icon|favicon|cropped-|plan|preview|-\d{2,4}x\d{2,4}\.", re.I)373839class NovoStJeanConnector(BaseConnector):40 source_id = "novo_stjean"41 request_delay = 0.64243 def fetch(self) -> list[Listing]:44 listings: list[Listing] = []45 try:46 html = self.get(LIST_URL).text47 except Exception:48 return listings49 soup = BeautifulSoup(html, "html.parser")5051 # inclusions tout-inclus (grille Bricks)52 amenities = [el.get_text(" ", strip=True)53 for el in soup.select(".inclusion_grid-item")]54 amenities = [re.sub(r"\s+", " ", a) for a in amenities if a][:12]5556 availability = "Disponible dès maintenant" \57 if re.search(r"Disponible dès maintenant", html) else ""5859 images = [u for u in dict.fromkeys(IMG_RE.findall(html))60 if not SKIP_IMG_RE.search(u)][:10]6162 seen: set[str] = set()63 for item in soup.select(".units_item"):64 try:65 h = item.select_one("h3")66 if h is None:67 continue68 m = TYPE_RE.match(h.get_text(" ", strip=True)69 .replace(" | ", " "))70 if not m:71 continue72 unit_type = normalize_unit_type(f"{m.group(1)} 1/2")73 den = bool(re.search(r"\+\s*den", item.get_text(" "), re.I))7475 text = item.get_text(" ", strip=True)76 mp = PRICE_RE.search(text)77 price_label = f"À partir de {mp.group(0)}" if mp else ""78 price = parse_price(mp.group(0)) if mp else None7980 ext_id = unit_type.replace("½", ".5") + ("-den" if den else "")81 if ext_id in seen:82 continue83 seen.add(ext_id)8485 suffix = " + den" if den else ""86 listings.append(Listing(87 source=self.source_id,88 external_id=ext_id,89 url=LIST_URL,90 title=f"{unit_type}{suffix} — NOVO St-Jean",91 address=ADDRESS,92 city=CITY,93 unit_type=unit_type,94 price=price,95 price_label=price_label,96 availability=availability,97 amenities=list(amenities),98 details={"den": True} if den else {},99 images=images,100 ))101 except Exception:102 continue103 return listings104