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/square_philippe.py : connecteur Square Philippe (squarephilippe.com)5# Complexe locatif neuf de Gatineau (secteur est, J8P) — 4 immeubles6# (25 rue du Galion, 75/91/115 rue Campagnard), 119 unités au total.7# WordPress rendu serveur : /a-louer/unites/ liste les unités DISPONIBLES en8# cartes `.theUnits` (data-rel = « 1 chambre »/« 2 chambres ») avec bandeau9# adresse (`.coin span`), prix (`.prix` « 1 700$ »), numéro d'unité + pi²10# (`.infoAll`), étage + chambres + « Disponible » (`.info2`) et photo du plan.11# external_id = <immeuble en slug>-<numéro d'unité> — stable par unité.12# -----------------------------------------------------------------------------13from __future__ import annotations1415import re1617from bs4 import BeautifulSoup1819from ..schema import Listing, strip_accents20from .base import BaseConnector2122BASE = "https://squarephilippe.com"23LIST_URL = f"{BASE}/a-louer/unites/"2425PRICE_RE = re.compile(r"([\d\s ]{3,7})\$")26UNIT_NO_RE = re.compile(r"Unité\s*(\w+)", re.I)27AREA_RE = re.compile(r"Total\s*:\s*([\d\s ]+)\s*pi", re.I)28BEDS_RE = re.compile(r"(\d+)\s*chambres?", re.I)29FLOOR_RE = re.compile(r"(Rez-de-chaussée|\d+e\s*étage)", re.I)303132def _slug(text: str) -> str:33 s = strip_accents(text.lower())34 return re.sub(r"[^a-z0-9]+", "-", s).strip("-")353637def _num(txt: str) -> float | None:38 n = re.sub(r"[\s ]", "", txt or "")39 try:40 return float(n)41 except ValueError:42 return None434445class SquarePhilippeConnector(BaseConnector):46 source_id = "square_philippe"47 request_delay = 0.84849 def fetch(self) -> list[Listing]:50 soup = BeautifulSoup(self.get(LIST_URL).text, "html.parser")51 listings: dict[str, Listing] = {}52 for card in soup.select(".theUnits"):53 try:54 self._parse_card(card, listings)55 except Exception:56 continue57 return list(listings.values())5859 def _parse_card(self, card, listings: dict[str, Listing]) -> None:60 coin = card.select_one(".coin span")61 building = re.sub(r"\s+", " ", coin.get_text(" ", strip=True)) \62 if coin else ""63 info = card.select_one(".infoAll")64 info_txt = re.sub(r"\s+", " ", info.get_text(" ", strip=True)) \65 if info else ""66 m = UNIT_NO_RE.search(info_txt)67 if not building or not m:68 return69 unit_no = m.group(1)70 ext_id = f"{_slug(building)}-{unit_no}"71 if ext_id in listings:72 return7374 price = None75 price_label = ""76 prix_el = card.select_one(".prix")77 if prix_el:78 price_label = re.sub(r"\s+", " ", prix_el.get_text(" ", strip=True))79 pm = PRICE_RE.search(price_label)80 if pm:81 price = _num(pm.group(1))8283 area = None84 am = AREA_RE.search(info_txt)85 if am:86 area = _num(am.group(1))8788 info2 = card.select_one(".info2")89 info2_txt = re.sub(r"\s+", " ", info2.get_text(" ", strip=True)) \90 if info2 else ""91 bedrooms = None92 bm = BEDS_RE.search(info2_txt + " " + (card.get("data-rel") or ""))93 if bm:94 bedrooms = float(bm.group(1))95 details: dict = {}96 fm = FLOOR_RE.search(info2_txt)97 if fm:98 details["floor"] = fm.group(1)99100 availability = ""101 av = card.find(string=re.compile(r"Disponible|Loué", re.I))102 if av:103 availability = re.sub(r"\s+", " ", str(av)).strip()104 if re.search(r"lou[ée]", availability, re.I):105 return # unité déjà louée : on saute106107 images: list[str] = []108 for im in card.select("img[src]"):109 u = im["src"]110 if not u.startswith("http"):111 u = BASE + u112 if u not in images and not re.search(r"logo|icon", u, re.I):113 images.append(u)114115 desc_bits = [b for b in (details.get("floor", ""),116 f"{int(area)} pi²" if area else "") if b]117 listings[ext_id] = Listing(118 source=self.source_id,119 external_id=ext_id,120 url=f"{LIST_URL}#{ext_id}",121 title=f"Unité {unit_no} — {building}",122 address=f"{building}, Gatineau",123 sector="",124 city="Gatineau",125 bedrooms=bedrooms,126 price=price,127 price_label=price_label,128 availability=availability,129 area_sqft=area,130 description=" | ".join(desc_bits),131 details=details,132 images=images,133 )134