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%
6.2 KB · 162 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/louis_alexandre.py : connecteur Le Louis-Alexandre5#   (condoslouisalexandre.ca — Longueuil, 2100, rue René, près du6#   Vieux-Longueuil). WordPress + Elementor rendu serveur : la page7#   « Appartements disponibles » liste chaque unité en blocs Elementor8#   « Condo #NNN | Superficie de X pi² | n chambres, n sdb | PRIX $ / mois »9#   avec image du plan et fiche PDF par unité. Le numéro d'unité (#NNN)10#   sert d'identifiant stable.11# -----------------------------------------------------------------------------12from __future__ import annotations1314import re1516from bs4 import BeautifulSoup1718from ..schema import Listing19from .base import BaseConnector2021BASE = "https://condoslouisalexandre.ca"22LIST_URL = f"{BASE}/appartements-disponibles-longueuil/"2324ADDRESS = "2100, rue René, Longueuil"25CITY = "Longueuil"2627UNIT_RE = re.compile(r"^Condo\s*#(\d+)\s*$")28SQFT_RE = re.compile(r"Superficie\s+de\s+([\d\s]+)\s*pi", re.I)29ROOMS_RE = re.compile(r"(Studio|\d\s*chambres?)\s*,\s*(\d)\s*sdb", re.I)30PRICE_RE = re.compile(r"(\d[\d\s]{2,6})\s*\$\s*/\s*mois", re.I)31IMG_RE = re.compile(32    r"https://condoslouisalexandre\.ca/wp-content/uploads/"33    r"[^\"\s\\']+\.(?:jpe?g|png|webp)", re.I)34SKIP_IMG_RE = re.compile(r"logo|icon|favicon|-\d{2,3}x\d{2,3}\.", re.I)353637class LouisAlexandreConnector(BaseConnector):38    source_id = "louis_alexandre"39    request_delay = 0.64041    def fetch(self) -> list[Listing]:42        listings: list[Listing] = []43        try:44            html = self.get(LIST_URL).text45        except Exception:46            return listings47        soup = BeautifulSoup(html, "html.parser")4849        # contact de l'immeuble (mailto:/tel: structurés dans la page)50        contact: dict = {}51        m = re.search(r'href="mailto:([^"?]+)"', html)52        if m:53            contact["email"] = m.group(1).strip().lower()54        m = re.search(r'href="tel:\+?1?[\s.\-]?(\d{3})[\s.\-]?(\d{3})'55                      r'[\s.\-]?(\d{4})"', html)56        if m:57            contact["phone"] = f"{m.group(1)}-{m.group(2)}-{m.group(3)}"5859        seen: set[str] = set()60        for node in soup.find_all(string=UNIT_RE):61            try:62                unit_no = UNIT_RE.match(node.strip()).group(1)63                if unit_no in seen:64                    continue6566                # bloc de l'unité : on remonte jusqu'au conteneur Elementor67                # qui porte le prix (texte « NNN $ / mois »)68                block = node.parent69                while block is not None and "$" not in block.get_text():70                    block = block.parent71                if block is None:72                    continue73                text = block.get_text(" | ", strip=True)74                if len(re.findall(r"Condo\s*#", text)) != 1:75                    continue    # conteneur trop large (plusieurs unités)76                seen.add(unit_no)7778                price = None79                price_label = ""80                m = PRICE_RE.search(text)81                if m:82                    price_label = m.group(0)83                    try:84                        val = float(m.group(1).replace(" ", "")85                                    .replace(" ", "").replace("\xa0", ""))86                        if 300 <= val <= 20000:87                            price = val88                    except ValueError:89                        pass9091                area = None92                m = SQFT_RE.search(text)93                if m:94                    try:95                        area = float(m.group(1).replace(" ", "")96                                     .replace("\xa0", ""))97                    except ValueError:98                        area = None99100                unit_type = ""101                bedrooms = None102                bathrooms = None103                m = ROOMS_RE.search(text)104                if m:105                    rooms = m.group(1)106                    bathrooms = float(m.group(2))107                    if re.match(r"studio", rooms, re.I):108                        unit_type = "Studio"109                        bedrooms = 0.0110                    else:111                        n = int(re.match(r"\d+", rooms).group(0))112                        bedrooms = float(n)113                        unit_type = f"{n + 2}½"114115                # image du plan + fiche PDF : conteneur parent (colonne voisine)116                images: list[str] = []117                pdf = ""118                wrap = block.parent119                while wrap is not None and not wrap.find("img"):120                    wrap = wrap.parent121                if wrap is not None and \122                        len(re.findall(r"Condo\s*#", wrap.get_text())) == 1:123                    for u in dict.fromkeys(IMG_RE.findall(str(wrap))):124                        if not SKIP_IMG_RE.search(u):125                            images.append(u)126                    a = wrap.find("a", href=re.compile(r"\.pdf$", re.I))127                    if a:128                        pdf = a.get("href", "")129130                details: dict = {}131                if contact:132                    details["contact"] = dict(contact)133                if pdf:134                    details["plan_pdf"] = pdf135136                amenities = []137                if area:138                    amenities.append(f"Superficie de {int(area)} pi²")139                if bathrooms:140                    amenities.append(f"{int(bathrooms)} salle(s) de bain")141142                listings.append(Listing(143                    source=self.source_id,144                    external_id=f"condo-{unit_no}",145                    url=LIST_URL,146                    title=f"Condo #{unit_no} — Le Louis-Alexandre",147                    address=ADDRESS,148                    city=CITY,149                    unit_type=unit_type,150                    bedrooms=bedrooms,151                    bathrooms=bathrooms,152                    price=price,153                    price_label=price_label,154                    area_sqft=area,155                    amenities=amenities,156                    details=details,157                    images=images[:10],158                ))159            except Exception:160                continue161        return listings162