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 · 119 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/martin_roy.py : connecteur Construction Martin Roy5#   (constructionmartinroy.com — Saint-Georges, Beauce). WordPress (WP Engine)6#   rendu serveur : la page /logement-a-louer/ liste 18 fiches par adresse7#   (cartes .v-logements-louer__logements-modeles-item) avec statut8#   Loué/Disponible, prix ($/mois, « À partir de », « Sur demande »),9#   chambres, salles de bain, superficie et photo (background-image).10#   On n'ingère que les unités NON louées (statut ≠ Loué). Les pages détail11#   /logement/<slug>/ sont derrière un WAF Cloudflare agressif : on s'en12#   tient à la liste, qui contient déjà tous les champs utiles.13# -----------------------------------------------------------------------------14from __future__ import annotations1516import re1718from bs4 import BeautifulSoup1920from ..schema import Listing, parse_price21from .base import BaseConnector2223BASE = "https://constructionmartinroy.com"24LIST_URL = f"{BASE}/logement-a-louer/"2526CITY = "Saint-Georges"27P = "v-logements-louer__logements-modeles-"   # préfixe des classes du thème2829BG_IMG_RE = re.compile(r"url\((['\"]?)(https?://[^'\")]+)\1\)", re.I)30AREA_RE = re.compile(r"(\d[\d\s]{2,5})\s*pi", re.I)31PHONE_RE = re.compile(r"(\d{3})[\s.\-](\d{3})[\s.\-](\d{4})")323334class MartinRoyConnector(BaseConnector):35    source_id = "martin_roy"36    request_delay = 0.63738    def fetch(self) -> list[Listing]:39        listings: list[Listing] = []40        html = self.get(LIST_URL).text41        soup = BeautifulSoup(html, "html.parser")4243        # téléphone du promoteur (lien tel: global du site)44        details_common: dict = {}45        tel = soup.select_one('a[href^="tel:"]')46        if tel:47            digits = re.sub(r"\D", "", tel.get("href", ""))[-10:]48            if len(digits) == 10:49                details_common["contact"] = {"phone": "-".join(50                    (digits[:3], digits[3:6], digits[6:]))}5152        for item in soup.select(f"li.{P}item"):53            try:54                statut_el = item.select_one(f".{P}statut")55                statut = statut_el.get_text(strip=True) if statut_el else ""56                # on n'ingère que les unités encore offertes57                if re.search(r"lou[ée]", statut, re.I):58                    continue5960                a = item.select_one(f"a.{P}lien")61                if not a or not a.get("href"):62                    continue63                url = a["href"].split("?")[0]64                slug = url.rstrip("/").split("/")[-1]65                if not slug:66                    continue6768                titre_el = item.select_one(f".{P}image-titre")69                street = titre_el.get_text(strip=True) if titre_el else slug70                address = street if re.search(r"saint-georges", street, re.I) \71                    else f"{street}, Saint-Georges"7273                prix_el = item.select_one(f".{P}contenu-prix")74                price_label = re.sub(r"\s+", " ", prix_el.get_text(75                    " ", strip=True)) if prix_el else ""76                price = parse_price(price_label) \77                    if "demande" not in price_label.lower() else None7879                def _num(cls: str) -> float | None:80                    el = item.select_one(f".{P}contenu-{cls} span")81                    v = el.get_text(strip=True) if el else ""82                    return float(v) if re.fullmatch(r"\d+(\.\d+)?", v) else None8384                bedrooms = _num("chambre")85                bathrooms = _num("salle-bain")86                area = None87                sup_el = item.select_one(f".{P}contenu-superficie span")88                sup = sup_el.get_text(strip=True) if sup_el else ""89                am = AREA_RE.search(sup)90                if am:91                    val = float(am.group(1).replace(" ", ""))92                    if 200 <= val <= 3000:     # le site affiche parfois le lot93                        area = val9495                images: list[str] = []96                bg = BG_IMG_RE.search(a.get("style") or "")97                if bg:98                    images.append(bg.group(2))99100                listings.append(Listing(101                    source=self.source_id,102                    external_id=slug,103                    url=url,104                    title=address,105                    address=address,106                    city=CITY,107                    bedrooms=bedrooms,108                    bathrooms=bathrooms,109                    price=price,110                    price_label=price_label if "$" in price_label else "",111                    availability=statut or "Disponible",112                    area_sqft=area,113                    details=dict(details_common),114                    images=images,115                ))116            except Exception:117                continue118        return listings119