Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
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