# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/obohec.py : connecteur O'bohec (obohec.ca) — Voyer & Tremblay # Trois immeubles locatifs sur le boulevard Michèle-Bohec, Blainville # (27 unités affichées : studio, 1 et 2 chambres). WordPress (thème maison # « trema ») rendu serveur : /projets-locatifs/obohec/ liste des cartes # .item-listing-unite groupées par en-tête .item-unites-filter # (data-immeuble-id → Immeuble 1/2/3, classe filter- sur la carte). # Carte : .title-unite (Studio/1 chambre/2 chambres), .number-unite # (« #105 Tout meublé Disponible septembre 2026 »), .disponiblite-unite # (Loué/Disponible), caractéristiques .car-* (chambres, bain, balcon, # stationnement, orientation, superficie) et plan image + PDF. AUCUN prix # publié (« Prix sur demande »). On exclut « BUREAU DE LOCATION » et les # unités louées. Granularité : unité. ⚠️ Le volet VENTE du site est ignoré. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type from .base import BaseConnector BASE = "https://obohec.ca" LIST_URL = f"{BASE}/projets-locatifs/obohec/" ADDRESS = "Boulevard Michèle-Bohec, Blainville" CITY = "Blainville" FILTER_RE = re.compile(r"^filter-(\d+)$") AREA_RE = re.compile(r"(\d[\d\s ]*)\s*pi", re.I) AVAIL_RE = re.compile(r"Disponible[^#<]*", re.I) class ObohecConnector(BaseConnector): source_id = "obohec" request_delay = 0.6 max_images = 10 @staticmethod def _txt(node, sel: str) -> str: el = node.select_one(sel) return re.sub(r"\s+", " ", el.get_text(" ", strip=True)) if el else "" def fetch(self) -> list[Listing]: html = self.get(LIST_URL).text soup = BeautifulSoup(html, "html.parser") # data-immeuble-id -> « Immeuble N » buildings: dict[str, str] = {} for h in soup.select(".item-unites-filter[data-immeuble-id]"): buildings[str(h["data-immeuble-id"])] = re.sub( r"\s+", " ", h.get_text(" ", strip=True)) listings: list[Listing] = [] seen: set[str] = set() for it in soup.select(".item-listing-unite"): title_u = self._txt(it, ".title-unite") if re.search(r"bureau\s+de\s+location", title_u, re.I): continue # local commercial dispo = self._txt(it, ".disponiblite-unite") if re.search(r"lou[ée]", dispo, re.I): continue num_raw = self._txt(it, ".number-unite") m = re.search(r"#\s*([\w-]+)", num_raw) if m is None: continue num = m.group(1) bid = "" for cls in it.get("class", []): fm = FILTER_RE.match(cls) if fm: bid = fm.group(1) break ext_id = f"imm{bid}-{num}" if bid else num if ext_id in seen: continue seen.add(ext_id) building = buildings.get(bid, "") m = AVAIL_RE.search(num_raw) availability = m.group(0).strip() if m else (dispo or "Disponible") furnished = bool(re.search(r"meubl[ée]", num_raw, re.I)) beds = None v = self._txt(it, ".car-chambres span") if v.isdigit(): beds = float(v) baths = None v = self._txt(it, ".car-bain span") if v.replace(".", "", 1).isdigit(): baths = float(v) area = None m = AREA_RE.search(self._txt(it, ".car-superficie span")) if m: try: v2 = float(re.sub(r"[\s ]", "", m.group(1))) if 100 <= v2 <= 10000: area = v2 except ValueError: pass amenities = [a for a in ( "Tout meublé" if furnished else "", f"Balcon/terrasse : {self._txt(it, '.car-balcon-terrasse span')}" if self._txt(it, ".car-balcon-terrasse span") else "", f"Stationnement {self._txt(it, '.car-stationnement span')}" if self._txt(it, ".car-stationnement span") else "", f"Orientation {self._txt(it, '.car-orientation span')}" if self._txt(it, ".car-orientation span") else "", ) if a] images: list[str] = [] img = it.select_one(".image-plan-unite img[src]") if img is not None and str(img["src"]).startswith("http"): images.append(str(img["src"])) details: dict = {} pdf = it.select_one('a.btn-imprimable[href], a[href$=".pdf"]') if pdf is not None: details["floor_plan"] = pdf["href"] desc_bits = [f"Unité #{num}", building, title_u, "Tout meublé" if furnished else ""] listings.append(Listing( source=self.source_id, external_id=ext_id, url=LIST_URL, title=f"O'bohec — {building or 'Blainville'} — Unité #{num}" f" ({title_u})", address=ADDRESS, city=CITY, unit_type=normalize_unit_type(title_u), bedrooms=beds, bathrooms=baths, price=None, price_label="Prix sur demande", availability=availability, area_sqft=area, description=" — ".join(b for b in desc_bits if b), amenities=amenities, details=details, images=images, )) return listings