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/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