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.6 KB · 114 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/le_730.py : connecteur Le 730 Notre-Dame (730.ca — 186 condos5#   locatifs luxueux, marina et club privé au 730, rue Notre-Dame à6#   Repentigny, au bord du Saint-Laurent). WordPress/Elementor une-page7#   rendu SERVEUR : l'accueil publie les prix « à partir de » par typologie8#   (3½/4½), l'occupation (« Occupation immédiate » pour les phases I-II)9#   et un plan PDF par variante d'unité (« 3 1/2 – Unité 1 »).10#   Granularité TYPOLOGIE (external_id = « le730-N.5 », stable) ; aucune11#   liste d'unités individuelles publiée. Les phases III-IV (livraison12#   2027) ne créent pas d'annonce.13# -----------------------------------------------------------------------------14from __future__ import annotations1516import re1718from bs4 import BeautifulSoup1920from ..schema import Listing, normalize_unit_type21from .base import BaseConnector2223PAGE_URL = "https://730.ca/"24ADDRESS = "730, rue Notre-Dame"25CITY = "Repentigny"2627# « 3 ½ à partir de 1750$ »28PRICE_RE = re.compile(r"(\d)\s*½\s*à partir de\s*([\d\s  ]+)\s*\$", re.I)29# liens de plans PDF « 3 1/2 – Unité 5 »30PLAN_RE = re.compile(31    r'<a href="(https://730\.ca/wp-content/uploads/[^"]+\.pdf)"[^>]*>'32    r'\s*(\d)\s*<sup>1/2</sup>\s*&#8211;\s*Unité\s*(\d+)', re.I)33OCCUP_RE = re.compile(r"Occupation immédiate[^!.]*[!.]?")34IMG_RE = re.compile(r"https://730\.ca/wp-content/uploads/"35                    r"[^\"\s\\]+?\.(?:jpe?g|png|webp)", re.I)36IMG_SIZE_RE = re.compile(r"-\d+x\d+(?=\.(?:jpe?g|png|webp))", re.I)3738BUILDING_AMENITIES = ["Toit terrasse", "Jardin communautaire", "BBQ",39                      "Piscine", "Gym", "Bornes pour voitures électriques",40                      "Club nautique et marina", "Ascenseurs"]414243class Le730Connector(BaseConnector):44    source_id = "le_730"45    request_delay = 0.84647    def fetch(self) -> list[Listing]:48        try:49            html = self.get(PAGE_URL).text50        except Exception:51            return []52        soup = BeautifulSoup(html, "html.parser")53        text = re.sub(r"\s+", " ", soup.get_text(" ", strip=True))5455        # prix « à partir de » par typologie56        prices: dict[str, float] = {}57        for m in PRICE_RE.finditer(text):58            try:59                prices.setdefault(f"{m.group(1)}½", float(60                    re.sub(r"[\s  ]", "", m.group(2))))61            except ValueError:62                pass6364        # plans PDF par variante -> typologies réellement offertes65        plans: dict[str, list[str]] = {}66        for m in PLAN_RE.finditer(html):67            lst = plans.setdefault(f"{m.group(2)}½", [])68            if m.group(1) not in lst:      # sections desktop/mobile dupliquées69                lst.append(m.group(1))70        if not plans and not prices:71            return []7273        m = OCCUP_RE.search(text)74        occupation = m.group(0).strip() if m else ""7576        images = []77        for u in dict.fromkeys(IMG_RE.findall(html)):78            u = IMG_SIZE_RE.sub("", u)79            if not re.search(r"logo|icon|favicon|Img_Plans", u, re.I) \80                    and u not in images:81                images.append(u)8283        desc_base = ("Le 730 Notre-Dame : plus de 180 condos locatifs, une "84                     "marina et un club privé sur les berges du Saint-Laurent "85                     "à Repentigny. Projet écoresponsable avec vue sur le "86                     "fleuve.")87        listings: list[Listing] = []88        for typo in sorted(set(plans) | set(prices)):89            unit_type = normalize_unit_type(typo)90            price = prices.get(typo)91            nb_plans = len(plans.get(typo, []))92            details: dict = {}93            if plans.get(typo):94                details["floor_plans"] = plans[typo][:12]95            listings.append(Listing(96                source=self.source_id,97                external_id=f"le730-{typo.replace('½', '.5')}",98                url=PAGE_URL,99                title=f"Condo {typo} — Le 730 Notre-Dame",100                address=ADDRESS,101                city=CITY,102                unit_type=unit_type,103                price=price,104                price_label=f"à partir de {price:.0f} $ /mois" if price else "",105                availability=occupation,106                description=(desc_base107                             + (f" {nb_plans} plans d'unité {typo} offerts."108                                if nb_plans else ""))[:1200],109                amenities=BUILDING_AMENITIES,110                details=details,111                images=images[:20],112            ))113        return listings114