# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/miner.py : connecteur Le Miner (leminer.ca) # Unités locatives neuves au 354, rue Principale à Granby (reconversion du # site Miner). Webflow statique rendu serveur : la page /unites affiche # trois cartes de typologie (« Unités d'une chambre / à partir de # 1 470 $/mois »). Granularité : typologie (pas d'unités individuelles). # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from .base import BaseConnector from ..schema import Listing BASE = "https://leminer.ca" LIST_URL = f"{BASE}/unites" ADDRESS = "354, rue Principale, Granby" CITY = "Granby" # « Unités d'une chambre » / « Unités de deux chambres avec deux salles de bain » CARD_RE = re.compile(r"^Unités?\s+(?:d'une|de\s+(?:une|deux|trois))\s+chambres?" r"(?:\s+avec\s+(?:une|deux)\s+salles?\s+de\s+bain)?$", re.I) PRICE_RE = re.compile(r"à\s*partir\s*de\s*([\d\s ]{4,9})\$\s*/\s*mois", re.I) IMG_RE = re.compile(r'(?:src|href)="(images/[^"]+\.(?:jpe?g|png|webp))"', re.I) WORD_NUM = {"une": 1, "deux": 2, "trois": 3} class MinerConnector(BaseConnector): source_id = "miner" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: list[Listing] = [] try: r = self.get(LIST_URL) r.encoding = "utf-8" # le serveur n'annonce pas le charset html = r.text except Exception: return listings soup = BeautifulSoup(html, "html.parser") lines = soup.get_text("\n", strip=True).split("\n") images = [f"{BASE}/{u}" for u in dict.fromkeys(IMG_RE.findall(html)) if not re.search(r"favicon|webclip|facebook|logo|icon|" r"vector", u, re.I)][:15] for i, line in enumerate(lines): raw = line.strip() if not CARD_RE.match(raw): continue m = PRICE_RE.search(" ".join(lines[i + 1:i + 3])) if not m: continue try: price = float(m.group(1).replace(" ", "") .replace(" ", "").replace(" ", "")) except ValueError: continue bedrooms = None mb = re.search(r"(?:d')?(une|deux|trois)\s+chambre", raw, re.I) if mb: bedrooms = WORD_NUM.get(mb.group(1).lower()) bathrooms = None ms = re.search(r"avec\s+(une|deux)\s+salles?\s+de\s+bain", raw, re.I) if ms: bathrooms = WORD_NUM.get(ms.group(1).lower()) ext_id = f"granby-{bedrooms or 'x'}cc" + ( f"-{bathrooms}sdb" if bathrooms else "") if any(l.external_id == ext_id for l in listings): continue listings.append(Listing( source=self.source_id, external_id=ext_id, url=LIST_URL, title=f"{raw} — Le Miner", address=ADDRESS, city=CITY, bedrooms=bedrooms, bathrooms=bathrooms, price=price, price_label=f"À partir de {m.group(1).strip()} $/mois", description="Unités locatives neuves du projet Le Miner à " "Granby : chambres spacieuses, espaces réfléchis " "et lumineux, au cœur du centre-ville.", images=images, )) return listings