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.2 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/viventi.py : connecteur Complexe Viventi Laval (viventilaval.com)5#   Immeuble unique pour 45/50 ans et plus au 1100, boul. Vanier (secteur6#   Saint-Vincent-de-Paul, Laval). WordPress/Elementor rendu serveur : la page7#   /nos-unites/ affiche une section par TYPOLOGIE (3½, 4½) avec prix8#   « À partir de X$/mois » et liste d'inclusions. Granularité TYPOLOGIE,9#   pas de disponibilité par unité.10# -----------------------------------------------------------------------------11from __future__ import annotations1213import re1415from bs4 import BeautifulSoup1617from ..schema import Listing, normalize_unit_type18from .base import BaseConnector1920BASE = "https://viventilaval.com"21PAGE_URL = f"{BASE}/nos-unites/"2223ADDRESS = "1100, boul. Vanier"24CITY = "Laval"25SECTOR = "Saint-Vincent-de-Paul"2627TYPE_RE = re.compile(r"^\d\s*(?:1/2|½)$")28PRICE_RE = re.compile(r"À partir de\s*([\d\s ,]+)\s*\$\s*/\s*mois", re.I)29IMG_RE = re.compile(30    r"https://viventilaval\.com/wp-content/uploads/[^\"'\s\\)]+?"31    r"\.(?:jpg|jpeg|png|webp)", re.I)323334class ViventiConnector(BaseConnector):35    source_id = "viventi"36    request_delay = 0.63738    def fetch(self) -> list[Listing]:39        listings: list[Listing] = []40        try:41            html = self.get(PAGE_URL).text42        except Exception:43            return listings44        soup = BeautifulSoup(html, "html.parser")4546        # photos communes de l'immeuble (les sections typologie n'ont pas de47        # galerie propre) — on écarte logos/favicons et miniatures redimensionnées48        images = [u for u in dict.fromkeys(IMG_RE.findall(html))49                  if not re.search(r"logo|favicon|icon|-\d+x\d+\.", u, re.I)][:20]5051        # une section par typologie : <h2>3 1/2</h2> suivi d'un widget texte52        # avec « À partir de X$/mois » + <ul> d'inclusions53        for heading in soup.select(".elementor-heading-title"):54            label = heading.get_text(" ", strip=True)55            if not TYPE_RE.match(label):56                continue57            unit_type = normalize_unit_type(label)58            container = heading.find_parent(59                "div", class_="elementor-widget-heading")60            price_label, description, amenities = "", "", []61            node = container62            # widgets suivants de la même section (jusqu'à la prochaine typologie)63            while node is not None:64                node = node.find_next_sibling("div")65                if node is None:66                    break67                text = node.get_text(" ", strip=True)68                if TYPE_RE.match(text):69                    break70                m = PRICE_RE.search(text)71                if m and not price_label:72                    price_label = ("À partir de "73                                   + re.sub(r"[\s ]", "", m.group(1))74                                   + " $/mois")75                for li in node.select("li"):76                    item = li.get_text(" ", strip=True)77                    if item and item not in amenities:78                        amenities.append(item)79                if not description:80                    for p in node.select("p"):81                        t = p.get_text(" ", strip=True)82                        if len(t) > 60 and "partir de" not in t.lower():83                            description = t84                            break85            if not price_label:86                continue87            ext_id = f"vanier-{unit_type.replace('½', '.5')}"88            listings.append(Listing(89                source=self.source_id,90                external_id=ext_id,91                url=PAGE_URL,92                title=f"{unit_type} — Complexe Viventi Laval (50 ans et plus)",93                address=ADDRESS,94                sector=SECTOR,95                city=CITY,96                unit_type=unit_type,97                price_label=price_label,98                description=description[:1500],99                amenities=amenities,100                details={"clientele": "50 ans et plus"},101                images=images,102            ))103        return listings104