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/trams.py : connecteur T.R.A.M.S Property Management (tramsmgmt.com)5# Gestionnaire montréalais : 3 immeubles locatifs résidentiels au Québec —6# Chateau Ste-Marie (Kirkland), L'Ambiance Ouest (Dollard-des-Ormeaux),7# Palacio D'Anjou (Anjou). WordPress rendu serveur : la page8# /residential-properties-for-rent/ liste les cartes (les propriétés de9# Floride sont EXCLUES) ; chaque page /property/<slug>/ donne l'adresse,10# la description, les caractéristiques (ul) et le contact. AUCUN prix ni11# inventaire par unité publié (le lien securecafe/Yardi n'est que le12# portail des résidents) → une annonce par immeuble, comme brochu.py.13# -----------------------------------------------------------------------------14from __future__ import annotations1516import hashlib17import re1819from bs4 import BeautifulSoup2021from ..schema import Listing22from .base import BaseConnector2324BASE = "https://tramsmgmt.com"25LIST_URL = f"{BASE}/residential-properties-for-rent/"2627QC_RE = re.compile(r",\s*(?:QC|Qu[ée]bec)\b", re.I)28CITY_RE = re.compile(r"([A-Za-zÀ-ÿ'’.\- ]{3,40}),\s*(?:QC|Qu[ée]bec)\b")29IMG_RE = re.compile(r"https://tramsmgmt\.com/wp-content/uploads/"30 r'[^"\s\\)]+\.(?:jpe?g|png|webp)', re.I)313233class TramsConnector(BaseConnector):34 source_id = "trams"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 a in soup.select('a[href*="/property/"]'):47 url = (a.get("href") or "").split("?")[0]48 slug = url.rstrip("/").split("/")[-1]49 if not slug or slug in seen:50 continue51 # carte englobante : ne garder que les immeubles québécois52 card = a53 for _ in range(5):54 if card.parent is None:55 break56 card = card.parent57 if len(card.select('a[href*="/property/"]')) > 3:58 card = None59 break60 text = card.get_text(" ", strip=True)61 if QC_RE.search(text):62 break63 else:64 continue65 if card is None or not QC_RE.search(66 card.get_text(" ", strip=True)):67 continue68 seen.add(slug)69 card_key = hashlib.sha1(70 card.get_text(" ", strip=True).encode("utf-8")).hexdigest()71 try:72 d = self.detail(slug, card_key,73 lambda url=url: self._fetch_detail(url))74 except Exception:75 continue76 if not d.get("title"):77 continue78 city = d.get("city", "")79 listings.append(Listing(80 source=self.source_id,81 external_id=slug,82 url=url,83 title=f"{d['title']} — appartements à louer"84 f"{' (' + city + ')' if city else ''}",85 address=d.get("address", ""),86 city=city,87 availability="Prix et visites : communiquer avec l'immeuble",88 description=d.get("description", ""),89 amenities=d.get("amenities") or [],90 details={"contact": d["contact"]} if d.get("contact") else {},91 images=d.get("images") or [],92 ))93 return listings9495 def _fetch_detail(self, url: str) -> dict:96 out: dict = {}97 html = self.get(url).text98 soup = BeautifulSoup(html, "html.parser")99 article = soup.find("article") or soup100101 h1 = article.find("h1")102 if h1:103 out["title"] = h1.get_text(" ", strip=True)104105 # bloc adresse sous le titre (rue / ville, QC / code postal)106 addr_div = article.select_one("div.text-center.pb-1")107 if addr_div:108 addr = re.sub(r"\s+", " ",109 addr_div.get_text(", ", strip=True))110 out["address"] = re.sub(r"\s*,\s*(,\s*)+", ", ", addr).strip(" ,")111 m = CITY_RE.search(out["address"])112 if m:113 out["city"] = m.group(1).strip()114115 # description : paragraphes substantiels ; caractéristiques : 1re liste116 paras = [p.get_text(" ", strip=True) for p in article.find_all("p")117 if len(p.get_text(strip=True)) > 60118 and "call" not in p.get_text().lower()[:40]]119 out["description"] = re.sub(r"\s+", " ",120 " ".join(paras[:3])).strip()[:1200]121 amenities: list[str] = []122 for li in article.select("ul li"):123 t = re.sub(r"\s+", " ", li.get_text(" ", strip=True)).rstrip(".")124 if 0 < len(t) <= 80 and t not in amenities:125 amenities.append(t)126 out["amenities"] = amenities[:15]127128 contact: dict = {}129 m = re.search(r'href="tel:([\d\- ()]{7,})"', html)130 if m:131 contact["phone"] = m.group(1).strip()132 m = re.search(r'href="mailto:([^"?]+)"', html)133 if m:134 contact["email"] = m.group(1).strip().lower()135 out["contact"] = contact136137 out["images"] = [138 u for u in dict.fromkeys(IMG_RE.findall(html))139 if not re.search(r"logo|icon|favicon|cropped-|-\d{2,3}x\d{2,3}\.",140 u, re.I)][:20]141 return out142