# ----------------------------------------------------------------------------- # Lou-Ka — Location court terme # connectors/chaletsbsl.py : Chalets BSL (chaletsbsl.com) — 4 chalets avec spa # sur un domaine privé de Saint-Simon-de-Rimouski (Bas-Saint-Laurent). # # Méthode : sitemap.xml (index) → sitemap_sections_*.xml → URLs /chalets/ # + lastmod (clé du cache détail ; les /en/ sont ignorées). Pages statiques # (CMS maison Bootstrap) : # -

= titre (préfixe « Chalets BSL - » retiré) ; # -

« À partir de 610$ pour 2 nuits » → price_night ; # -

« 2 pers. 1 chbre. 1 sdb. » ; # - description =

entre la capacité et le bloc country-info ; # - commodités =

des blocs country-name ; # - galerie = var chaletImgs = {"ete": […], …} (JSON par saison) ; # - lat/lng = const myLatLng = { lat: …, lng: … } ; CITQ en pied de fiche. # ----------------------------------------------------------------------------- from __future__ import annotations import html as _html import json import re from ..schema import StListing from .base import StConnector SITE = "https://chaletsbsl.com" SITEMAP = SITE + "/sitemap.xml" _TAG_RE = re.compile(r"<[^>]+>") def _text(fragment: str) -> str: return _html.unescape(re.sub(r"\s+", " ", _TAG_RE.sub(" ", fragment))).strip() class ChaletsBsl(StConnector): source_id = "chaletsbsl" request_delay = 1.0 # -- page détail ---------------------------------------------------------- def _detail(self, url: str) -> dict: h = self.get(url).text d: dict = {} m = re.search(r'(?s)

]*>(.*?)

', h) if m: d["title"] = re.sub(r"^Chalets BSL\s*-\s*", "", _text(m.group(1))) # « À partir de 610$ pour 2 nuits » → 305 $/nuit m = re.search(r"À partir de\s*([\d\s]+)\$\s*pour\s*(\d+)\s*nuits", h) if m: total = float(m.group(1).replace(" ", "")) nights = int(m.group(2)) if nights and 20 <= total / nights <= 20000: d["price_night"] = round(total / nights) d["price_ref"] = f"{total:g} $ pour {nights} nuits" # « 2 pers. 1 chbre. 1 sdb. » m = re.search(r'(?s)

]*>(.*?)

', h) if m: frag = _text(m.group(1)) for pat, key in ((r"(\d+)\s*pers", "capacity"), (r"(\d+)\s*chbre", "bedrooms"), (r"(\d+)\s*sdb", "bathrooms")): mm = re.search(pat, frag) if mm: d[key] = float(mm.group(1)) # description : les

entre la capacité et le bloc country-info i = h.find('class="capacite') j = h.find("country-info") if 0 < i < j: paras = [_text(p) for p in re.findall(r"(?s)]*>(.*?)

", h[i:j])] texte = "\n".join(p for p in paras if len(p) > 40) if texte: d["description"] = texte[:5000] # commodités : les
(tous portés par les blocs country-name) amen: list[str] = [] for h6 in re.findall(r"(?s)]*>(.*?)
", h): t = _text(h6) if t and t not in amen: amen.append(t) if amen: d["amenities"] = amen m = re.search(r"const myLatLng = \{\s*lat:\s*(-?\d+\.\d+)," r"\s*lng:\s*(-?\d+\.\d+)", h) if m: d["lat"], d["lng"] = float(m.group(1)), float(m.group(2)) m = re.search(r"CITQ\D{0,25}(\d{6})", h) if m: d["citq"] = m.group(1) # galerie : var chaletImgs = {"ete": […], "hiver": […]} m = re.search(r"var chaletImgs = (\{.*?\});", h, re.S) if m: try: seasons = json.loads(m.group(1)) imgs: list[str] = [] for key in ("ete", *sorted(k for k in seasons if k != "ete")): for u in seasons.get(key) or []: if isinstance(u, str) and u.startswith("https://") \ and u not in imgs and len(imgs) < 20: imgs.append(u) if imgs: d["images"] = imgs except ValueError: pass return d # -- contrat -------------------------------------------------------------- def fetch(self) -> list[StListing]: index = self.get(SITEMAP).text entries: list[tuple[str, str]] = [] for sub in re.findall(r"([^<]+)", index): if "sitemap_sections" not in sub: continue xml = self.get(sub).text entries += re.findall(r"(?s)\s*([^<]+)" r"(?:\s*([^<]*))?", xml) listings: list[StListing] = [] vus: set[str] = set() for url, lastmod in entries: m = re.match(r"https://chaletsbsl\.com/chalets/([^/]+)/?$", url) if not m: continue slug = m.group(1) if slug in vus: continue vus.add(slug) det = self.detail(slug, lastmod or "v1", lambda u=url: self._detail(u)) title = det.get("title") or "" if not title: continue price = det.get("price_night") details = {k: v for k, v in { "price_ref": det.get("price_ref") or "", "domain": "Chalets BSL", }.items() if v} listings.append(StListing( source=self.source_id, external_id=slug, url=url, title=title, property_type="Chalet", city="Saint-Simon-de-Rimouski", region="Bas-Saint-Laurent", price_night=float(price) if price else None, price_label=f"à partir de {price:g} $ / nuit" if price else "", capacity=det.get("capacity"), bedrooms=det.get("bedrooms"), bathrooms=det.get("bathrooms"), citq=det.get("citq") or "", description=det.get("description") or "", amenities=det.get("amenities") or [], details=details, images=det.get("images") or [], lat=det.get("lat"), lng=det.get("lng"), )) return listings