Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1# -----------------------------------------------------------------------------2# Lou-Ka — Location court terme3# connectors/accestremblant.py : Accès Tremblant (accestremblant.ca)4#5# Agence de condos à Mont-Tremblant (~17 fiches) — WordPress Avada6# (portfolio) + moteur Guesty (guestybookings.com).7#8# Méthode :9# 1. LISTE : sitemap https://accestremblant.ca/avada_portfolio-sitemap.xml10# → /condos/<slug>/ (lastmod = clé du cache détail).11# 2. DÉTAIL (cache self.detail) : la fiche WP fournit spécs (spans12# icon-condos : « 8 Personnes », « 3 chambres », « 3 salles de bain » +13# extras type « Foyer au gaz »), description (JSON-LD Yoast), photos14# (wp-content/uploads) et l'id Guesty (lien guestybookings.com).15# 3. PRIX : chaque fiche contient un carrousel « autres condos » avec16# « À partir de N $ » pour les AUTRES fiches ; chaque payload détail17# mémorise cette carte slug→prix et fetch() fusionne le tout (le prix18# d'une fiche vient donc des autres pages, même à froid depuis le cache).19# External_id = slug WP (stable) ; l'id Guesty est gardé dans details.20# -----------------------------------------------------------------------------21from __future__ import annotations2223import html as _html24import json25import re2627from ..schema import StListing28from .base import StConnector2930SITE = "https://accestremblant.ca"31SITEMAP = f"{SITE}/avada_portfolio-sitemap.xml"3233_TAG_RE = re.compile(r"<[^>]+>")343536def _text(fragment: str) -> str:37 return _html.unescape(re.sub(r"\s+", " ", _TAG_RE.sub(" ", fragment))).strip()383940class AccesTremblant(StConnector):41 source_id = "accestremblant"4243 # -- liste (sitemap) ----------------------------------------------------44 def _sitemap(self) -> list[tuple[str, str]]:45 """[(url fiche, lastmod)] — dédupliqué, sans la page index."""46 xml = self.get(SITEMAP).text47 seen: dict[str, str] = {}48 for m in re.finditer(r"(?s)<url>\s*<loc>([^<]+)</loc>"49 r"(?:\s*<lastmod>([^<]+)</lastmod>)?", xml):50 url, lastmod = m.group(1).strip(), (m.group(2) or "").strip()51 if re.fullmatch(rf"{re.escape(SITE)}/condos/[^/]+/", url):52 seen.setdefault(url, lastmod)53 return sorted(seen.items())5455 # -- page détail ------------------------------------------------------56 def _detail(self, url: str) -> dict:57 h = self.get(url).text58 d: dict = {}5960 # spécs : <span class="icon-condos">… 8 Personnes / 3 chambres / …61 extras: list[str] = []62 for raw in re.findall(r'(?s)<span class="icon-condos"[^>]*>(.*?)</span>',63 h):64 t = _text(raw)65 if not t:66 continue67 m = re.match(r"(\d+)\s*[Pp]ersonnes?", t)68 if m:69 d["capacity"] = float(m.group(1))70 continue71 m = re.match(r"(\d+)\s*[Cc]hambres?", t)72 if m:73 d["bedrooms"] = float(m.group(1))74 continue75 m = re.match(r"(\d+)\s*[Ss]alles?\s*de\s*bain", t)76 if m:77 d["bathrooms"] = float(m.group(1))78 continue79 if len(t) <= 60:80 extras.append(t)81 if extras:82 d["amenities"] = extras8384 # description : JSON-LD Yoast (WebPage.description)85 m = re.search(r'(?s)<script type="application/ld\+json"[^>]*>'86 r"(.*?)</script>", h)87 if m:88 try:89 graph = json.loads(m.group(1)).get("@graph") or []90 for node in graph:91 if node.get("@type") == "WebPage" and node.get("description"):92 d["description"] = _text(node["description"])[:4000]93 break94 except ValueError:95 pass9697 # id Guesty (lien « réserver » guestybookings.com)98 m = re.search(r"guestybookings\.com/(?:fr/)?properties/([a-f0-9]{24})",99 h)100 if m:101 d["guesty_id"] = m.group(1)102103 # photos : uploads WP (originaux, sans logos ni vignettes -NxN)104 imgs: list[str] = []105 for u in re.findall(r'(https://accestremblant\.ca/wp-content/uploads/'106 r'20\d\d/\d\d/[^" ]+\.(?:jpe?g|png|webp))', h):107 if re.search(r"-\d{2,4}x\d{2,4}\.", u):108 continue109 if re.search(r"logo|favicon|icon", u, re.I):110 continue111 if u not in imgs:112 imgs.append(u)113 d["images"] = imgs[:20]114115 # carte des prix « autres condos » : slug → à partir de N $116 prices: dict[str, float] = {}117 for slug, val in re.findall(118 r'href="https://accestremblant\.ca/condos/([^"/]+)/"[^>]*>'119 r"\s*À partir de\s*([\d ,]+)\s*\$", h):120 try:121 prices[slug] = float(val.replace(" ", "").replace(",", "."))122 except ValueError:123 continue124 d["prices_seen"] = prices125 return d126127 # -- contrat ----------------------------------------------------------128 def fetch(self) -> list[StListing]:129 rows = []130 price_map: dict[str, float] = {}131 for url, lastmod in self._sitemap():132 slug = url.rstrip("/").rsplit("/", 1)[-1]133 try:134 det = self.detail(slug, lastmod, lambda u=url: self._detail(u))135 except Exception:136 continue137 price_map.update(det.get("prices_seen") or {})138 rows.append((slug, url, det))139140 listings: list[StListing] = []141 for slug, url, det in rows:142 title = slug.replace("-", " ").title()143 desc = det.get("description") or ""144 m = re.match(r"([^|]{2,60})\|", desc)145 if m: # « Verbier C | Découvrez… »146 title = m.group(1).strip()147 desc = desc.split("|", 1)[1].strip()148 price = price_map.get(slug)149150 details = {k: v for k, v in {151 "guesty_id": det.get("guesty_id"),152 }.items() if v}153154 listings.append(StListing(155 source=self.source_id,156 external_id=slug,157 url=url,158 title=title,159 property_type="Condo",160 city="Mont-Tremblant",161 region="Laurentides",162 price_night=price,163 price_label=(f"à partir de {price:.0f} $ / nuit"164 if price else ""),165 capacity=det.get("capacity"),166 bedrooms=det.get("bedrooms"),167 bathrooms=det.get("bathrooms"),168 description=desc[:4000],169 amenities=det.get("amenities") or [],170 details=details,171 images=det.get("images") or [],172 ))173 return listings174