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/obohec.py : connecteur O'bohec (obohec.ca) — Voyer & Tremblay5# Trois immeubles locatifs sur le boulevard Michèle-Bohec, Blainville6# (27 unités affichées : studio, 1 et 2 chambres). WordPress (thème maison7# « trema ») rendu serveur : /projets-locatifs/obohec/ liste des cartes8# .item-listing-unite groupées par en-tête .item-unites-filter9# (data-immeuble-id → Immeuble 1/2/3, classe filter-<id> sur la carte).10# Carte : .title-unite (Studio/1 chambre/2 chambres), .number-unite11# (« #105 Tout meublé Disponible septembre 2026 »), .disponiblite-unite12# (Loué/Disponible), caractéristiques .car-* (chambres, bain, balcon,13# stationnement, orientation, superficie) et plan image + PDF. AUCUN prix14# publié (« Prix sur demande »). On exclut « BUREAU DE LOCATION » et les15# unités louées. Granularité : unité. ⚠️ Le volet VENTE du site est ignoré.16# -----------------------------------------------------------------------------17from __future__ import annotations1819import re2021from bs4 import BeautifulSoup2223from ..schema import Listing, normalize_unit_type2425from .base import BaseConnector2627BASE = "https://obohec.ca"28LIST_URL = f"{BASE}/projets-locatifs/obohec/"2930ADDRESS = "Boulevard Michèle-Bohec, Blainville"31CITY = "Blainville"3233FILTER_RE = re.compile(r"^filter-(\d+)$")34AREA_RE = re.compile(r"(\d[\d\s ]*)\s*pi", re.I)35AVAIL_RE = re.compile(r"Disponible[^#<]*", re.I)363738class ObohecConnector(BaseConnector):39 source_id = "obohec"40 request_delay = 0.641 max_images = 104243 @staticmethod44 def _txt(node, sel: str) -> str:45 el = node.select_one(sel)46 return re.sub(r"\s+", " ", el.get_text(" ", strip=True)) if el else ""4748 def fetch(self) -> list[Listing]:49 html = self.get(LIST_URL).text50 soup = BeautifulSoup(html, "html.parser")5152 # data-immeuble-id -> « Immeuble N »53 buildings: dict[str, str] = {}54 for h in soup.select(".item-unites-filter[data-immeuble-id]"):55 buildings[str(h["data-immeuble-id"])] = re.sub(56 r"\s+", " ", h.get_text(" ", strip=True))5758 listings: list[Listing] = []59 seen: set[str] = set()60 for it in soup.select(".item-listing-unite"):61 title_u = self._txt(it, ".title-unite")62 if re.search(r"bureau\s+de\s+location", title_u, re.I):63 continue # local commercial64 dispo = self._txt(it, ".disponiblite-unite")65 if re.search(r"lou[ée]", dispo, re.I):66 continue6768 num_raw = self._txt(it, ".number-unite")69 m = re.search(r"#\s*([\w-]+)", num_raw)70 if m is None:71 continue72 num = m.group(1)7374 bid = ""75 for cls in it.get("class", []):76 fm = FILTER_RE.match(cls)77 if fm:78 bid = fm.group(1)79 break80 ext_id = f"imm{bid}-{num}" if bid else num81 if ext_id in seen:82 continue83 seen.add(ext_id)84 building = buildings.get(bid, "")8586 m = AVAIL_RE.search(num_raw)87 availability = m.group(0).strip() if m else (dispo or "Disponible")88 furnished = bool(re.search(r"meubl[ée]", num_raw, re.I))8990 beds = None91 v = self._txt(it, ".car-chambres span")92 if v.isdigit():93 beds = float(v)94 baths = None95 v = self._txt(it, ".car-bain span")96 if v.replace(".", "", 1).isdigit():97 baths = float(v)9899 area = None100 m = AREA_RE.search(self._txt(it, ".car-superficie span"))101 if m:102 try:103 v2 = float(re.sub(r"[\s ]", "", m.group(1)))104 if 100 <= v2 <= 10000:105 area = v2106 except ValueError:107 pass108109 amenities = [a for a in (110 "Tout meublé" if furnished else "",111 f"Balcon/terrasse : {self._txt(it, '.car-balcon-terrasse span')}"112 if self._txt(it, ".car-balcon-terrasse span") else "",113 f"Stationnement {self._txt(it, '.car-stationnement span')}"114 if self._txt(it, ".car-stationnement span") else "",115 f"Orientation {self._txt(it, '.car-orientation span')}"116 if self._txt(it, ".car-orientation span") else "",117 ) if a]118119 images: list[str] = []120 img = it.select_one(".image-plan-unite img[src]")121 if img is not None and str(img["src"]).startswith("http"):122 images.append(str(img["src"]))123 details: dict = {}124 pdf = it.select_one('a.btn-imprimable[href], a[href$=".pdf"]')125 if pdf is not None:126 details["floor_plan"] = pdf["href"]127128 desc_bits = [f"Unité #{num}", building, title_u,129 "Tout meublé" if furnished else ""]130 listings.append(Listing(131 source=self.source_id,132 external_id=ext_id,133 url=LIST_URL,134 title=f"O'bohec — {building or 'Blainville'} — Unité #{num}"135 f" ({title_u})",136 address=ADDRESS,137 city=CITY,138 unit_type=normalize_unit_type(title_u),139 bedrooms=beds,140 bathrooms=baths,141 price=None,142 price_label="Prix sur demande",143 availability=availability,144 area_sqft=area,145 description=" — ".join(b for b in desc_bits if b),146 amenities=amenities,147 details=details,148 images=images,149 ))150 return listings151