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.9 KB · 122 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/quartier_horizon.py : connecteur Quartier Horizon5#   (quartierhorizon.com — Lévis, secteur Saint-Jean-Chrysostome : 1802, rue6#    des Fines-Herbes). WordPress/Divi rendu serveur : la page /plans/ liste7#    ~114 cartes <article class="ps-card"> dont TOUTES les données sont des8#    attributs data-* (unité, bâtiment, étage, type 3½-5½, chambres,9#    superficie, balcon, prix « à partir de », disponibilité, plan PDF, image10#    du plan, état). États : disponible / attente / indisponible / a-venir —11#    on n'ingère que « disponible » (comme aula exclut « attente »).12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from bs4 import BeautifulSoup1819from ..schema import Listing, normalize_unit_type20from .base import BaseConnector2122BASE = "https://quartierhorizon.com"23PLANS_URL = f"{BASE}/plans/"2425ADDRESS = "1802, rue des Fines-Herbes, Lévis, G6Z 2L1"26SECTOR = "Saint-Jean-Chrysostome"27CITY = "Lévis"2829PHONE_RE = re.compile(r"(\d{3})[\s.\-](\d{3})[\s.\-](\d{4})")303132class QuartierHorizonConnector(BaseConnector):33    source_id = "quartier_horizon"34    request_delay = 0.63536    def fetch(self) -> list[Listing]:37        listings: list[Listing] = []38        html = self.get(PLANS_URL).text39        soup = BeautifulSoup(html, "html.parser")4041        # contact du projet (affiché dans le pied de page / bandeau)42        details_common: dict = {}43        m = PHONE_RE.search(soup.get_text(" ", strip=True))44        if m:45            details_common["contact"] = {46                "phone": f"{m.group(1)}-{m.group(2)}-{m.group(3)}"}4748        for card in soup.select("article.ps-card"):49            try:50                if (card.get("data-etat") or "").strip() != "disponible":51                    continue52                unit = (card.get("data-unite") or "").strip()53                bkey = (card.get("data-bkey") or "").strip()54                if not unit:55                    continue56                ext_id = f"{bkey}-{unit}" if bkey else unit5758                unit_type = normalize_unit_type(card.get("data-type") or "")59                price = None60                montant = (card.get("data-montant") or "").replace(" ", "")61                if montant.isdigit() and 100 <= int(montant) <= 20000:62                    price = float(montant)63                libre = (card.get("data-libre") or "").strip()64                phase = (card.get("data-phase") or "").strip()65                etage = (card.get("data-etagenom") or "").strip()66                bldg = (card.get("data-bldg") or "").strip()6768                bedrooms = None69                ch = (card.get("data-chambres") or "").strip()70                if ch.isdigit():71                    bedrooms = float(ch)72                area = None73                sup = (card.get("data-superficie") or "").replace(" ", "")74                if sup.isdigit():75                    area = float(sup)7677                amenities: list[str] = []78                if etage:79                    amenities.append(f"Étage : {etage}")80                balcon = (card.get("data-balcon") or "").strip()81                if balcon.isdigit() and int(balcon) > 0:82                    amenities.append(f"Balcon de {balcon} pi²")8384                details = dict(details_common)85                pdf = (card.get("data-pdf") or "").strip()86                if pdf:87                    details["plan_pdf"] = pdf88                if phase:89                    details["phase"] = phase9091                images: list[str] = []92                img = (card.get("data-image") or "").strip()93                if img.startswith("http"):94                    images.append(img)9596                loc_bits = [b for b in97                            (f"Bâtiment {bkey.upper() or bldg}", etage) if b]98                listings.append(Listing(99                    source=self.source_id,100                    external_id=ext_id,101                    url=PLANS_URL,102                    title=f"{unit_type} — unité {unit}, Quartier Horizon",103                    address=ADDRESS,104                    sector=SECTOR,105                    city=CITY,106                    unit_type=unit_type,107                    bedrooms=bedrooms,108                    price=price,109                    price_label=(f"À partir de {montant} $ /mois"110                                 if price else ""),111                    availability=(f"Libre : {libre}" if libre112                                  else "Disponible"),113                    area_sqft=area,114                    description=" · ".join(loc_bits),115                    amenities=amenities,116                    details=details,117                    images=images,118                ))119            except Exception:120                continue121        return listings122