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.4 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/westwalk.py : connecteur Westwalk DDO (westwalk.ca)5#   Immeuble neuf au 12, rue Davignon, Dollard-des-Ormeaux (West-Island),6#   à 10 min à pied du REM. WordPress/Elementor statique : la page7#   /en/apartments-for-rent-dollard-des-ormeaux/ affiche un « image-box » par8#   type d'unité (LOFT, 1-BEDROOM, 2-BEDROOMS, 3-BEDROOMS) avec soit un prix9#   « Starting at $2,300 », soit « 100% rented » — seuls les types avec un10#   prix réel sont annoncés (une annonce par type, id stable ww-ddo-<type>).11#   Le site liste aussi Anjou (sans cartes) et Pointe-Claire (prix12#   placeholders $X,XXX) : exclus. Commodités = titres h3 de la page.13# -----------------------------------------------------------------------------14from __future__ import annotations1516import re1718from bs4 import BeautifulSoup1920from ..schema import Listing, strip_accents21from .base import BaseConnector2223BASE = "https://westwalk.ca"24PAGE = f"{BASE}/en/apartments-for-rent-dollard-des-ormeaux/"25ADDRESS = "12, rue Davignon, Dollard-des-Ormeaux, QC H9B 0B6"2627# Libellé du type -> type d'unité QC28_TYPES = {29    "LOFT": "Loft",30    "1-BEDROOM": "3½",31    "2-BEDROOMS": "4½",32    "3-BEDROOMS": "5½",33}34_PRICE_RE = re.compile(r"\$\s*([\d][\d,\s]*\d|\d)")35_IMG_RE = re.compile(36    r'https://westwalk\.ca/wp-content/uploads/[^"\'\s]+?'37    r'\.(?:jpg|jpeg|png|webp)', re.I)38_SKIP_IMG = re.compile(r"logo|icon|favicon|bullet|map-|-\d{2,4}x\d{2,4}\.", re.I)39# h3 de la page qui ne sont PAS des commodités40_SKIP_H3 = re.compile(r"^(unit\s|follow|contact|westwalk)", re.I)414243class WestwalkConnector(BaseConnector):44    source_id = "westwalk"45    request_delay = 0.646    max_images = 204748    def fetch(self) -> list[Listing]:49        html = self.get(PAGE).text50        soup = BeautifulSoup(html, "html.parser")5152        # galerie (photos du projet, sans logos/miniatures)53        images: list[str] = []54        for u in _IMG_RE.findall(html):55            if _SKIP_IMG.search(u):56                continue57            if u not in images:58                images.append(u)59        images = images[: self.max_images]6061        # commodités : titres h3 courts de la page (Elevator, Gym, Pool…)62        amenities: list[str] = []63        for h in soup.find_all("h3"):64            t = h.get_text(" ", strip=True)65            if t and len(t) < 60 and not _SKIP_H3.match(t):66                if t not in amenities:67                    amenities.append(t)68        amenities = amenities[:25]6970        # description : méta de la page71        desc = ""72        meta = soup.find("meta", attrs={"name": "description"})73        if meta and meta.get("content"):74            desc = meta["content"].strip()[:600]7576        listings: list[Listing] = []77        for box in soup.select(".elementor-image-box-content"):78            t_el = box.select_one(".elementor-image-box-title")79            d_el = box.select_one(".elementor-image-box-description")80            if not (t_el and d_el):81                continue82            label = t_el.get_text(" ", strip=True).upper()83            if label not in _TYPES:84                continue85            price_txt = d_el.get_text(" ", strip=True)86            pm = _PRICE_RE.search(price_txt)87            if not pm:88                continue             # « 100% rented » ou placeholder : exclu89            try:90                price = float(re.sub(r"[^\d]", "", pm.group(1)))91            except ValueError:92                continue93            if not (100 <= price <= 20000):94                continue95            slug = re.sub(r"[^a-z0-9]+", "-",96                          strip_accents(label.lower())).strip("-")97            listings.append(Listing(98                source=self.source_id,99                external_id=f"ddo-{slug}",100                url=PAGE,101                title=f"Westwalk DDO — {label.title()}",102                address=ADDRESS,103                sector="Dollard-des-Ormeaux",104                city="Dollard-des-Ormeaux",105                unit_type=_TYPES[label],106                price=price,107                price_label=f"À partir de {int(price)} $/mois",108                availability=price_txt,109                description=desc,110                amenities=list(amenities),111                images=list(images),112            ))113        return listings114