SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
5.5 KB · 136 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Lou-Ka — Location court terme3# connectors/panoraloges.py : Panora Loges Fluviales (panoraloges.com) —4#   10 loges (mini-chalets) en bord de mer à Sainte-Anne-des-Monts (Gaspésie),5#   déclinées en DEUX gammes identiques à l'intérieur d'une même gamme :6#   loges simples (nos 8-10, 2 pers.) et loges doubles (nos 1-7, 4 pers.).7#8# Méthode : site Squarespace ; l'API ?format=json de la page /fr/loges est9#   vide (items=0) → on parse les DEUX pages de gamme statiques10#   /fr/logessimples et /fr/logesdoubles. Chaque page expose en <h2-4> :11#   prix (« 175 $ » sous « À partir de »), « N invités », « N chambres »,12#   « N lits Queen », « 1 salle de bain », puis la liste des commodités13#   jusqu'à « Stationnements ». 2 fiches « gamme » (multi_unit) plutôt que14#   10 fiches par unité artificiellement identiques — le site lui-même15#   laisse le libre choix de la loge à l'intérieur d'une gamme.16# -----------------------------------------------------------------------------17from __future__ import annotations1819import html as _html20import re2122from ..schema import StListing23from .base import StConnector2425SITE = "https://www.panoraloges.com"2627_PAGES = [28    # (external_id, chemin, titre, unités)29    ("loges-doubles", "/fr/logesdoubles", "Loges doubles — Panora Loges "30     "Fluviales", "Loges 1 à 7"),31    ("loges-simples", "/fr/logessimples", "Loges simples — Panora Loges "32     "Fluviales", "Loges 8 à 10"),33]3435_TAG_RE = re.compile(r"<[^>]+>")3637# images de gabarit à exclure de la galerie38_IMG_EXCLUDE = ("favicon", "logo", "icon", "texture", "panora_ecran",39                "Fond+de+page", "plan")404142def _text(fragment: str) -> str:43    return _html.unescape(re.sub(r"\s+", " ", _TAG_RE.sub(" ", fragment))).strip()444546class PanoraLoges(StConnector):47    source_id = "panoraloges"48    request_delay = 1.04950    def _parse(self, path: str) -> dict:51        h = self.get(SITE + path).text52        d: dict = {}5354        heads = [t for t in (_text(x) for x in re.findall(55            r"(?s)<h[1-4][^>]*>(.*?)</h[1-4]>", h)) if t]5657        m = re.search(r"(\d+)\s*\$", " ".join(heads))58        if m:59            d["price_night"] = float(m.group(1))6061        # commodités : entêtes entre « N invités » et « Stationnements »62        start = next((i for i, t in enumerate(heads)63                      if re.fullmatch(r"\d+\s+invités?", t)), None)64        end = next((i for i, t in enumerate(heads)65                    if t.lower().startswith("stationnement")), None)66        amen: list[str] = []67        if start is not None and end is not None and start < end:68            amen = heads[start:end + 1]69        blob = " | ".join(amen)70        for pat, key in ((r"(\d+)\s+invités?", "capacity"),71                         (r"(\d+)\s+chambres?", "bedrooms"),72                         (r"(\d+)\s+lits?\s+Queen", "beds"),73                         (r"(\d+)\s+salles?\s+de\s+bain", "bathrooms")):74            m = re.search(pat, blob, re.I)75            if m:76                d[key] = float(m.group(1))77        d["amenities"] = [a for a in amen78                          if not re.fullmatch(r"\d+\s+(invités?|chambres?"79                                              r"|salles?\s+de\s+bain)", a)]8081        # description : sous-titre de gamme + infos pratiques (les <p> du82        # gabarit Squarespace contiennent beaucoup de CSS → filtrés)83        paras = [_text(p) for p in re.findall(r"(?s)<p[^>]*>(.*?)</p>", h)]84        keep = [p for p in paras85                if 60 < len(p) < 600 and "{" not in p86                and not p.startswith(("FR:", "0 "))87                and "Infolettre" not in p and "418-" not in p]88        if keep:89            d["description"] = "\n".join(keep[:6])[:3000]9091        imgs: list[str] = []92        for u in re.findall(r"https://images\.squarespace-cdn\.com/[^\"\s?]+",93                            h):94            if any(x.lower() in u.lower() for x in _IMG_EXCLUDE):95                continue96            u = u.rstrip("\\,&")97            if not re.search(r"\.(?:jpe?g|png|webp)$", u, re.I):98                continue99            if u not in imgs:100                imgs.append(u)101            if len(imgs) >= 15:102                break103        d["images"] = imgs104        return d105106    # -- contrat --------------------------------------------------------------107    def fetch(self) -> list[StListing]:108        listings: list[StListing] = []109        for ext_id, path, title, units in _PAGES:110            d = self._parse(path)111            if not d.get("price_night") and not d.get("capacity"):112                continue113            price = d.get("price_night")114            listings.append(StListing(115                source=self.source_id,116                external_id=ext_id,117                url=SITE + path,118                title=title,119                property_type="Chalet",120                address="610 boul. Sainte-Anne E",121                city="Sainte-Anne-des-Monts",122                region="Gaspésie",123                price_night=price,124                price_label=f"À partir de {price:g} $ / nuit" if price else "",125                capacity=d.get("capacity"),126                bedrooms=d.get("bedrooms"),127                beds=d.get("beds"),128                bathrooms=d.get("bathrooms"),129                description=d.get("description") or "",130                amenities=d.get("amenities") or [],131                details={"multi_unit": True, "units": units,132                         "domain": "Panora Loges Fluviales"},133                images=d.get("images") or [],134            ))135        return listings136