Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.
HTML 82.9%
Python 15.2%
TypeScript 0.9%
JavaScript 0.7%
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