# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/square_philippe.py : connecteur Square Philippe (squarephilippe.com) # Complexe locatif neuf de Gatineau (secteur est, J8P) — 4 immeubles # (25 rue du Galion, 75/91/115 rue Campagnard), 119 unités au total. # WordPress rendu serveur : /a-louer/unites/ liste les unités DISPONIBLES en # cartes `.theUnits` (data-rel = « 1 chambre »/« 2 chambres ») avec bandeau # adresse (`.coin span`), prix (`.prix` « 1 700$ »), numéro d'unité + pi² # (`.infoAll`), étage + chambres + « Disponible » (`.info2`) et photo du plan. # external_id = - — stable par unité. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, strip_accents from .base import BaseConnector BASE = "https://squarephilippe.com" LIST_URL = f"{BASE}/a-louer/unites/" PRICE_RE = re.compile(r"([\d\s ]{3,7})\$") UNIT_NO_RE = re.compile(r"Unité\s*(\w+)", re.I) AREA_RE = re.compile(r"Total\s*:\s*([\d\s ]+)\s*pi", re.I) BEDS_RE = re.compile(r"(\d+)\s*chambres?", re.I) FLOOR_RE = re.compile(r"(Rez-de-chaussée|\d+e\s*étage)", re.I) def _slug(text: str) -> str: s = strip_accents(text.lower()) return re.sub(r"[^a-z0-9]+", "-", s).strip("-") def _num(txt: str) -> float | None: n = re.sub(r"[\s ]", "", txt or "") try: return float(n) except ValueError: return None class SquarePhilippeConnector(BaseConnector): source_id = "square_philippe" request_delay = 0.8 def fetch(self) -> list[Listing]: soup = BeautifulSoup(self.get(LIST_URL).text, "html.parser") listings: dict[str, Listing] = {} for card in soup.select(".theUnits"): try: self._parse_card(card, listings) except Exception: continue return list(listings.values()) def _parse_card(self, card, listings: dict[str, Listing]) -> None: coin = card.select_one(".coin span") building = re.sub(r"\s+", " ", coin.get_text(" ", strip=True)) \ if coin else "" info = card.select_one(".infoAll") info_txt = re.sub(r"\s+", " ", info.get_text(" ", strip=True)) \ if info else "" m = UNIT_NO_RE.search(info_txt) if not building or not m: return unit_no = m.group(1) ext_id = f"{_slug(building)}-{unit_no}" if ext_id in listings: return price = None price_label = "" prix_el = card.select_one(".prix") if prix_el: price_label = re.sub(r"\s+", " ", prix_el.get_text(" ", strip=True)) pm = PRICE_RE.search(price_label) if pm: price = _num(pm.group(1)) area = None am = AREA_RE.search(info_txt) if am: area = _num(am.group(1)) info2 = card.select_one(".info2") info2_txt = re.sub(r"\s+", " ", info2.get_text(" ", strip=True)) \ if info2 else "" bedrooms = None bm = BEDS_RE.search(info2_txt + " " + (card.get("data-rel") or "")) if bm: bedrooms = float(bm.group(1)) details: dict = {} fm = FLOOR_RE.search(info2_txt) if fm: details["floor"] = fm.group(1) availability = "" av = card.find(string=re.compile(r"Disponible|Loué", re.I)) if av: availability = re.sub(r"\s+", " ", str(av)).strip() if re.search(r"lou[ée]", availability, re.I): return # unité déjà louée : on saute images: list[str] = [] for im in card.select("img[src]"): u = im["src"] if not u.startswith("http"): u = BASE + u if u not in images and not re.search(r"logo|icon", u, re.I): images.append(u) desc_bits = [b for b in (details.get("floor", ""), f"{int(area)} pi²" if area else "") if b] listings[ext_id] = Listing( source=self.source_id, external_id=ext_id, url=f"{LIST_URL}#{ext_id}", title=f"Unité {unit_no} — {building}", address=f"{building}, Gatineau", sector="", city="Gatineau", bedrooms=bedrooms, price=price, price_label=price_label, availability=availability, area_sqft=area, description=" | ".join(desc_bits), details=details, images=images, )