# ----------------------------------------------------------------------------- # Lou-Ka — Location court terme # connectors/gitespassant.py : Gîtes et Auberges du Passant certifiés # (réseau officiel de la Fédération des Agricotours du Québec — marques # déposées « Gîte du Passant », « Auberge du Passant », « Maison de # Campagne »). L'ancien giteetaubergedupassant.com ne répond plus : le # répertoire vit maintenant sur membres.terroiretsaveurs.com (plateforme # Yapla/MemboGo de Terroir et Saveurs du Québec). # # Méthode (annuaire Yapla, aucun anti-bot) : # 1. LISTE : POST /fr/repertoire/pagination/pageNumber/1 avec # categorie_detablissement[]=36680 (« Hébergement ») → le script # memboGo.directory.scroll.list["organization"] contient TOUS les ids de # membres (une vingtaine). Le même appel filtré par regionadministrative # (19 régions touristiques) donne la région de chaque membre. # 2. FICHE (cache self.detail, rafraîchie chaque mois) : # /fr/repertoire/detailorganization/id/ — HTML serveur : nom (h1), # description, « Types d'hébergement » (Gîte/Auberge du Passant, Maison # de Campagne…), numéro CITQ (#attestation), classification (soleils), # adresse postale (spans), téléphone, site web, photos du carrousel, # coordonnées lat/lng extraites de l'iframe Google Maps (paramètres # !2d!3d de l'embed — présent sur toutes les fiches), # chambres/capacité glanées dans la description quand mentionnées. # Pas de prix affiché (les tarifs sont chez chaque établissement). # ----------------------------------------------------------------------------- from __future__ import annotations import html as _html import json import os import re import time from ..schema import StListing from .base import StConnector BASE = "https://membres.terroiretsaveurs.com" PAGINATION = BASE + "/fr/repertoire/pagination/pageNumber/1" FICHE = BASE + "/fr/repertoire/detailorganization/id/{id}" CAT_HEBERGEMENT = "36680" # option « Hébergement » du formulaire # valeur de l'option regionadministrative → région touristique canonique _REGIONS = { "316581": "Abitibi-Témiscamingue", "316582": "Bas-Saint-Laurent", "316584": "Cantons-de-l'Est", "316585": "Centre-du-Québec", "316586": "Charlevoix", "316583": "Chaudière-Appalaches", "316587": "Côte-Nord", "591978": "Eeyou Istchee Baie-James", "316589": "Gaspésie", "316590": "Îles-de-la-Madeleine", "316588": "Lanaudière", "316591": "Laurentides", "316592": "Laval", "316593": "Mauricie", "316594": "Montérégie", "316595": "Outaouais", "316596": "Montréal", "316597": "Québec", "316598": "Saguenay–Lac-Saint-Jean", } # « Types d'hébergement » de la fiche → type canonique Lou-Ka _TYPES = [ ("gite du passant", "Gîte"), ("gite", "Gîte"), ("auberge du passant", "Auberge"), ("auberge", "Auberge"), ("maison de campagne", "Maison"), ("maison de ville", "Maison"), ("chalet", "Chalet"), ] _TAG_RE = re.compile(r"<[^>]+>") # iframe Google Maps : …/maps/embed?pb=…!2d!3d!… _GMAP_RE = re.compile(r"google\.com/maps/embed\?pb=[^\"']*" r"!2d(-?\d+\.\d+)!3d(-?\d+\.\d+)") _CAP_RE = re.compile(r"(\d{1,2})\s*personnes") _CHAMBRES_RE = re.compile(r"(\d{1,2})\s*chambres") def _text(fragment: str) -> str: return _html.unescape(re.sub(r"\s+", " ", _TAG_RE.sub(" ", fragment))).strip() class GitesPassant(StConnector): source_id = "gites_passant" # -- liste (ids de membres) -------------------------------------------- def _ids(self, region_value: str = "") -> list[str]: data = {"activetab": "organization", "search_type": "advanced", "categorie_detablissement[]": CAT_HEBERGEMENT} if region_value: data["regionadministrative"] = region_value resp = self.post(PAGINATION, data=data, headers={"X-Requested-With": "XMLHttpRequest"}) m = re.search(r'scroll\.list\["organization"\]\s*=\s*(\[.*?\]);', resp.text, re.S) if not m: return [] try: groups = json.loads(m.group(1)) except ValueError: return [] out: list[str] = [] for g in groups: out.extend(str(i) for i in (g or {}).get("items") or []) return out # -- fiche --------------------------------------------------------------- def _fiche(self, member_id: str) -> dict: brut = self.get(FICHE.format(id=member_id)).text # les fiches truffent le HTML de ", " ", brut) d: dict = {} h1s = re.findall(r"]*>(.*?)", h, re.S) titres = [_text(x) for x in h1s if _text(x) and _text(x) != "Répertoire"] if titres: d["title"] = titres[-1] m = re.search(r"(?s)

\s*Description\s*

(.*?)" r'<(?:h2|div class="carousel|/section)', h) if m: d["description"] = _text(m.group(1))[:5000] m = re.search(r'(?s)id="hebergement"[^>]*>(.*?)', h) if m: d["types"] = _text(m.group(1)).replace("Types d'hébergement :", "").strip() m = re.search(r'(?s)id="attestation"[^>]*>.*?

\s*(\d{4,8})\s*

', h) if m: d["citq"] = m.group(1) m = re.search(r'(?s)id="classification"[^>]*>.*?]*>(.*?)

', h) if m: classement = _text(m.group(1)) if classement not in ("", "0"): d["classification"] = classement # adresse :

rue
ville (Québec)… m = re.search(r"(?s)Coordonnées(.*?)

", h) if m: spans = [_text(s) for s in re.findall(r"]*>(.*?)", m.group(1))] spans = [s for s in spans if s and not s.startswith("Site Web")] if spans: d["street"] = spans[0] if len(spans) > 1: d["city"] = spans[1] for s in spans[2:]: if re.match(r"^[A-Z]\d[A-Z]\s?\d[A-Z]\d$", s): d["postal"] = s mm = re.search(r"Téléphone\s*:\s*([\d ().+-]{7,20})", m.group(1)) if mm: d["phone"] = mm.group(1).strip() m = re.search(r'id="site"[^>]*>.*?href="([^"]+)"', h, re.S) if m: d["website"] = m.group(1) # géolocalisation : centre de l'iframe Google Maps (brut : l'iframe # est parfois injectée par un