# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/trams.py : connecteur T.R.A.M.S Property Management (tramsmgmt.com) # Gestionnaire montréalais : 3 immeubles locatifs résidentiels au Québec — # Chateau Ste-Marie (Kirkland), L'Ambiance Ouest (Dollard-des-Ormeaux), # Palacio D'Anjou (Anjou). WordPress rendu serveur : la page # /residential-properties-for-rent/ liste les cartes (les propriétés de # Floride sont EXCLUES) ; chaque page /property// donne l'adresse, # la description, les caractéristiques (ul) et le contact. AUCUN prix ni # inventaire par unité publié (le lien securecafe/Yardi n'est que le # portail des résidents) → une annonce par immeuble, comme brochu.py. # ----------------------------------------------------------------------------- from __future__ import annotations import hashlib import re from bs4 import BeautifulSoup from ..schema import Listing from .base import BaseConnector BASE = "https://tramsmgmt.com" LIST_URL = f"{BASE}/residential-properties-for-rent/" QC_RE = re.compile(r",\s*(?:QC|Qu[ée]bec)\b", re.I) CITY_RE = re.compile(r"([A-Za-zÀ-ÿ'’.\- ]{3,40}),\s*(?:QC|Qu[ée]bec)\b") IMG_RE = re.compile(r"https://tramsmgmt\.com/wp-content/uploads/" r'[^"\s\\)]+\.(?:jpe?g|png|webp)', re.I) class TramsConnector(BaseConnector): source_id = "trams" request_delay = 0.7 def fetch(self) -> list[Listing]: listings: list[Listing] = [] try: html = self.get(LIST_URL).text except Exception: return listings soup = BeautifulSoup(html, "html.parser") seen: set[str] = set() for a in soup.select('a[href*="/property/"]'): url = (a.get("href") or "").split("?")[0] slug = url.rstrip("/").split("/")[-1] if not slug or slug in seen: continue # carte englobante : ne garder que les immeubles québécois card = a for _ in range(5): if card.parent is None: break card = card.parent if len(card.select('a[href*="/property/"]')) > 3: card = None break text = card.get_text(" ", strip=True) if QC_RE.search(text): break else: continue if card is None or not QC_RE.search( card.get_text(" ", strip=True)): continue seen.add(slug) card_key = hashlib.sha1( card.get_text(" ", strip=True).encode("utf-8")).hexdigest() try: d = self.detail(slug, card_key, lambda url=url: self._fetch_detail(url)) except Exception: continue if not d.get("title"): continue city = d.get("city", "") listings.append(Listing( source=self.source_id, external_id=slug, url=url, title=f"{d['title']} — appartements à louer" f"{' (' + city + ')' if city else ''}", address=d.get("address", ""), city=city, availability="Prix et visites : communiquer avec l'immeuble", description=d.get("description", ""), amenities=d.get("amenities") or [], details={"contact": d["contact"]} if d.get("contact") else {}, images=d.get("images") or [], )) return listings def _fetch_detail(self, url: str) -> dict: out: dict = {} html = self.get(url).text soup = BeautifulSoup(html, "html.parser") article = soup.find("article") or soup h1 = article.find("h1") if h1: out["title"] = h1.get_text(" ", strip=True) # bloc adresse sous le titre (rue / ville, QC / code postal) addr_div = article.select_one("div.text-center.pb-1") if addr_div: addr = re.sub(r"\s+", " ", addr_div.get_text(", ", strip=True)) out["address"] = re.sub(r"\s*,\s*(,\s*)+", ", ", addr).strip(" ,") m = CITY_RE.search(out["address"]) if m: out["city"] = m.group(1).strip() # description : paragraphes substantiels ; caractéristiques : 1re liste paras = [p.get_text(" ", strip=True) for p in article.find_all("p") if len(p.get_text(strip=True)) > 60 and "call" not in p.get_text().lower()[:40]] out["description"] = re.sub(r"\s+", " ", " ".join(paras[:3])).strip()[:1200] amenities: list[str] = [] for li in article.select("ul li"): t = re.sub(r"\s+", " ", li.get_text(" ", strip=True)).rstrip(".") if 0 < len(t) <= 80 and t not in amenities: amenities.append(t) out["amenities"] = amenities[:15] contact: dict = {} m = re.search(r'href="tel:([\d\- ()]{7,})"', html) if m: contact["phone"] = m.group(1).strip() m = re.search(r'href="mailto:([^"?]+)"', html) if m: contact["email"] = m.group(1).strip().lower() out["contact"] = contact out["images"] = [ u for u in dict.fromkeys(IMG_RE.findall(html)) if not re.search(r"logo|icon|favicon|cropped-|-\d{2,3}x\d{2,3}\.", u, re.I)][:20] return out