SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
4.1 KB · 104 lines python
Raw Blame History
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