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/logispro_mauricie.py : connecteur LogisPro Mauricie (logispro.ca)5# Trois-Rivières, Cap-de-la-Madeleine, Shawinigan, Bécancour, Nicolet.6# ⚠️ Compagnie DISTINCTE de LogisPro Rimouski (logisprorimouski.com,7# connecteur `logispro`). WordPress thème maison rendu serveur :8# /appartements-a-louer/ = cartes (prix, adresse, typologie, lien9# /apartments/<slug>/) ; page détail = liste « Secteur / Prix / Grandeur /10# Étage / … », description riche et galerie fancybox.11# Granularité : une annonce par logement disponible.12# -----------------------------------------------------------------------------13from __future__ import annotations1415import hashlib16import re1718from bs4 import BeautifulSoup1920from ..schema import Listing, normalize_unit_type21from .base import BaseConnector2223BASE = "https://logispro.ca"24LIST_URL = f"{BASE}/appartements-a-louer/"2526PRICE_RE = re.compile(r"([\d\s ,]+)\s*\$")27# villes desservies, pour extraire la ville depuis l'adresse de la carte28CITY_RE = re.compile(29 r"(Trois-Rivi[èe]res|Shawinigan|B[ée]cancour|Nicolet|"30 r"Cap-de-la-Madeleine|Gentilly|Saint-Maurice)", re.I)313233class LogisProMauricieConnector(BaseConnector):34 source_id = "logispro_mauricie"35 request_delay = 0.73637 def fetch(self) -> list[Listing]:38 listings: list[Listing] = []39 try:40 html = self.get(LIST_URL).text41 except Exception:42 return listings43 soup = BeautifulSoup(html, "html.parser")4445 seen: set[str] = set()46 for card in soup.select("div.apartments-item"):47 try:48 a = card.select_one('a[href*="/apartments/"]')49 if a is None:50 continue51 url = a.get("href", "").split("?")[0]52 slug = url.rstrip("/").split("/")[-1]53 if not slug or slug in seen:54 continue55 seen.add(slug)5657 price = None58 price_label = ""59 price_el = card.select_one("p.price")60 if price_el is not None:61 price_label = re.sub(r"\s+", " ",62 price_el.get_text(" ", strip=True))63 m = PRICE_RE.search(price_label)64 if m:65 try:66 price = float(m.group(1).replace(" ", "")67 .replace(" ", "").replace(",", ""))68 except ValueError:69 pass7071 addr_el = card.select_one("p.adress, p.address")72 address = (re.sub(r"\s+", " ", addr_el.get_text(" ", strip=True))73 if addr_el else "")74 room_el = card.select_one("p.room")75 unit_type = normalize_unit_type(76 room_el.get_text(" ", strip=True) if room_el else "")7778 city = ""79 m = CITY_RE.search(address)80 if m:81 city = m.group(1)82 if city.lower() in ("cap-de-la-madeleine", "gentilly"):83 city = ("Trois-Rivières"84 if "cap" in m.group(1).lower() else "Bécancour")8586 card_key = hashlib.sha1(87 f"{price_label}|{address}|{unit_type}"88 .encode("utf-8")).hexdigest()89 d = self.detail(slug, card_key,90 lambda url=url: self._fetch_detail(url))9192 listings.append(Listing(93 source=self.source_id,94 external_id=slug,95 url=url,96 title=address or slug,97 address=d.get("address") or address,98 sector=d.get("sector", ""),99 city=city or d.get("sector", ""),100 unit_type=unit_type or d.get("unit_type", ""),101 price=price,102 price_label=price_label,103 availability=d.get("availability", ""),104 description=d.get("description", ""),105 amenities=d.get("amenities") or [],106 images=d.get("images") or [],107 ))108 except Exception:109 continue110 return listings111112 def _fetch_detail(self, url: str) -> dict:113 """Page détail : liste top_descript (Secteur/Prix/Grandeur/Étage/…),114 description riche (dispo « dès maintenant » incluse) et galerie."""115 out: dict = {}116 try:117 html = self.get(url).text118 except Exception:119 return out120 soup = BeautifulSoup(html, "html.parser")121122 amenities: list[str] = []123 for li in soup.select("ul.top_descript li"):124 txt = re.sub(r"\s+", " ", li.get_text(" ", strip=True))125 if not txt or ":" not in txt:126 continue127 label, _, value = txt.partition(":")128 label, value = label.strip(), value.strip()129 if not value:130 continue131 low = label.lower()132 if low == "secteur":133 out["sector"] = value134 elif low == "prix":135 pass # déjà sur la carte136 elif low == "grandeur":137 out["unit_type"] = normalize_unit_type(value)138 else:139 amenities.append(f"{label} : {value}")140 out["amenities"] = amenities[:20]141142 desc_el = soup.select_one("div.description")143 if desc_el is not None:144 desc = desc_el.get_text("\n", strip=True)145 out["description"] = re.sub(r"\n{2,}", "\n", desc).strip()[:1200]146 m = re.search(r"Disponible[^\n•]*", desc, re.I)147 if m:148 out["availability"] = m.group(0).strip()149 # adresse complète avec code postal (dernier paragraphe)150 m = re.search(r"\d[\w#\-]*[^\n]*,\s*(?:QC|Qu[ée]bec)[^\n]*", desc)151 if m:152 out["address"] = m.group(0).replace(", Canada", "").strip()153154 out["images"] = [155 a.get("href", "").strip()156 for a in soup.select(".apartment__gallery a.fancybox[href]")157 if a.get("href", "").strip()][:25]158 return out159