SPB Git forge

spb/sorti-ka

Public

Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.

58commits 1branches 0releases
13.7 MBsize
maindefault branch
17 days agolast push
HTML 82.9% Python 15.2% TypeScript 0.9% JavaScript 0.7%
6.2 KB · 138 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Sorti-Ka — Agrégateur de sorties & événements (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/centreduquebec.py : Tourisme Centre-du-Québec — événements5#   (ATR, Phase 4). Première source de la région Centre-du-Québec (0 fiche6#   avant connexion).7#   Source    : https://tourismecentreduquebec.com/evenements/ — WordPress,8#               liste rendue serveur (~9 fiches à venir, revérifié 2026-08-25,9#               aucune pagination — la liste est complète) : chaque bloc10#               events_listing_results_event = plage de dates FR11#               (« 1 septembre 2026 au 6 septembre 2026 ») + titre + lien.12#   Extraction: GET liste (1 requête) → fiches détail incrémentales (cache13#               data/centreduquebec_cache.json, CDQ_FICHE_MAX/sync, re-visite14#               7 j, version v=2) : champs étiquetés <small> (Adresse, VILLE,15#               Entreprise → organisateur), description du bloc16#               event_details_row_description, « Programme » (liste d'activités17#               → catégories brutes), lien « Visiter le site web » (href vide18#               sur toutes les fiches en 2026-08 — bogue du gabarit à la19#               source, champ émis dès qu'il sera rempli), og:image.20#               Ni heure ni tarif ni GPS publiés → jamais inventés.21#   Accès     : site public, robots.txt sans interdiction sur /evenements/ —22#               GET throttlés, UA honnête.23# -----------------------------------------------------------------------------24from __future__ import annotations2526import os27import re28import time2930from ..normalize import clean_text, parse_date_range_fr31from ..schema import Event32from .base import BaseConnector3334BASE = "https://tourismecentreduquebec.com"35LIST_URL = BASE + "/evenements/"36FICHE_MAX = int(os.environ.get("CDQ_FICHE_MAX", "10"))37REFRESH_AFTER = 7 * 8640038CACHE_V = 2                 # v2 : + ville, organisateur, site web, programme,39#                             description du corps de fiche (2026-08-25)4041_BLOCK_RE = re.compile(42    r'events_listing_results_event_date">\s*<h4>\s*(?P<date>[^<]+?)\s*</h4>.*?'43    r'events_listing_results_event_name">\s*<h4>(?P<title>[^<]+)</h4>.*?'44    r'href="(?P<url>https://tourismecentreduquebec\.com/evenements/[^"]+)"',45    re.S)46# champs étiquetés de la fiche : <small>Adresse</small> 1910, chemin Dublin47_FIELD_RE = re.compile(48    r"<small>\s*(Adresse|Ville|Entreprise)\s*</small>\s*([^<]+)")49_DESC_RE = re.compile(50    r'event_details_row_description">\s*(.*?)\s*</div>', re.S)51_PROG_RE = re.compile(r"<h5>Programme</h5>\s*<p>(.*?)</p>", re.S)52_WEBSITE_RE = re.compile(53    r'href="(https?://[^"]+)"[^>]*>\s*Visiter le site web')54_OG_IMG_RE = re.compile(r'property="og:image"\s+content="([^"]*)"')55_OG_DESC_RE = re.compile(r'property="og:description"\s+content="([^"]*)"')565758class CentreDuQuebecConnector(BaseConnector):59    source_id = "centreduquebec"60    request_delay = 0.86162    def _parse_fiche(self, html: str) -> dict:63        fields = {label: clean_text(val)64                  for label, val in _FIELD_RE.findall(html)}65        desc_m = _DESC_RE.search(html)66        og_desc = _OG_DESC_RE.search(html)67        desc = clean_text(desc_m.group(1))[:2000] if desc_m else (68            clean_text(og_desc.group(1)) if og_desc else "")69        prog = _PROG_RE.search(html)70        programme = [clean_text(x) for x in71                     re.split(r"<br\s*/?>", prog.group(1))] if prog else []72        site = _WEBSITE_RE.search(html)73        img = _OG_IMG_RE.search(html)74        return {75            "address": fields.get("Adresse", ""),76            "city": fields.get("Ville", ""),77            "organizer": fields.get("Entreprise", ""),78            "website": site.group(1) if site else "",79            "raw_categories": [p for p in programme if p],80            "image": clean_text(img.group(1)) if img else "",81            "description": desc,82        }8384    def fetch(self) -> list[Event]:85        html = self.get(LIST_URL).text86        rows: list[dict] = []87        for m in _BLOCK_RE.finditer(html):88            url = m.group("url").rstrip("/") + "/"89            start, end = parse_date_range_fr(m.group("date"))90            rows.append({91                "external_id": url.rstrip("/").split("/")[-1],92                "url": url,93                "title": clean_text(m.group("title")),94                "start_date": start, "end_date": end,95            })9697        cache = self.load_cache()98        now = time.time()99        current = {r["external_id"] for r in rows}100        cache = {k: v for k, v in cache.items() if k in current}101        to_fetch = sorted(102            (r for r in rows103             if r["external_id"] not in cache104             or cache[r["external_id"]].get("v", 1) < CACHE_V105             or now - cache[r["external_id"]].get("ts", 0) > REFRESH_AFTER),106            key=lambda r: (cache.get(r["external_id"], {}).get("v", 1)107                           if r["external_id"] in cache else 0,108                           cache.get(r["external_id"], {}).get("ts", 0)))[:FICHE_MAX]109        for r in to_fetch:110            try:111                detail = self._parse_fiche(self.get(r["url"]).text)112            except Exception:113                detail = {}114            cache[r["external_id"]] = {"ts": now, "detail": detail,115                                       "v": CACHE_V}116        self.save_cache(cache)117118        events: list[Event] = []119        for r in rows:120            detail = cache.get(r["external_id"], {}).get("detail") or {}121            events.append(Event(122                source=self.source_id,123                external_id=r["external_id"],124                url=r["url"],125                title=r["title"],126                description=detail.get("description", ""),127                raw_categories=(detail.get("raw_categories") or []) + [r["title"]],128                address=detail.get("address", ""),129                city=detail.get("city", ""),130                tourist_region="Centre-du-Québec",131                start_date=r["start_date"],132                end_date=r["end_date"],133                organizer=detail.get("organizer", ""),134                website=detail.get("website", ""),135                image=detail.get("image", ""),136            ))137        return events138