SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
5.9 KB · 151 lines python
Raw Blame History
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