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/shawinigan.py : connecteur Ville de Shawinigan — événements5# Source : https://www.shawinigan.ca/loisirs-et-culture/evenements/6# calendrier/ (WordPress + plugin maison « tri-evenements »).7# Extraction: archive rendue serveur (cartes « tri-evenement-single » :8# date FR, titre, extrait, image, url fiche datée9# /evenements/AAAA/MM/JJ/<slug>/), pagination /calendrier/10# page/N. Enrichissement par fiche (date de fin « Date de11# fin : ... », lieu « Lieu : ... », heure si publiée) — CACHE12# INCRÉMENTAL disque, plafond SHA_FICHES par sync.13# Accès : site municipal public, robots.txt permissif (wp-admin14# seulement). GET directs throttlés, identification honnête.15# Prix : non publié par la source → jamais inventé.16# -----------------------------------------------------------------------------17from __future__ import annotations1819import html as _html20import os21import re2223from ..normalize import clean_text, parse_date_fr, parse_time24from ..schema import Event25from .base import BaseConnector2627LIST_URL = "https://www.shawinigan.ca/loisirs-et-culture/evenements/calendrier/"28PAGES_MAX = int(os.environ.get("SHA_PAGES", "6"))29FICHES_MAX = int(os.environ.get("SHA_FICHES", "40"))3031# cartes : « -single » (prochain événement) et « -list » (liste « À venir »)32_CARD_RE = re.compile(33 r'<article[^>]*class="tri-evenement-evenement tri-evenement-(?:single|list)"[^>]*>(.*?)</article>',34 re.S)35_TITLE_RE = re.compile(36 r'tri-evenement-titre"><a href="([^"]+)"[^>]*>(.*?)</a>', re.S)37_DATE_RE = re.compile(r'tri-evenement-date">.*?</i>\s*(.*?)\s*</p>', re.S)38_DESC_RE = re.compile(r'tri-evenement-desc">(.*?)</p>', re.S)39_IMG_RE = re.compile(r'<img[^>]+src="([^"]+)"')40_URL_DATE_RE = re.compile(r"/evenements/(\d{4})/(\d{2})/(\d{2})/")41# fiche42_F_FIN_RE = re.compile(r"Date de fin\s*:\s*(.*?)\s*</p>", re.S)43_F_LIEU_RE = re.compile(r"Lieu\s*:\s*(.*?)\s*</p>", re.S)44_F_HEURE_RE = re.compile(r"Heure\s*:\s*(.*?)\s*</p>", re.S)454647class ShawiniganConnector(BaseConnector):48 source_id = "shawinigan"49 request_delay = 0.85051 def _parse_page(self, html: str) -> list[dict]:52 rows: list[dict] = []53 for block in _CARD_RE.findall(html):54 t = _TITLE_RE.search(block)55 if not t:56 continue57 url = t.group(1)58 start = None59 m = _URL_DATE_RE.search(url) # URL datée = source fiable60 if m:61 start = f"{m.group(1)}-{m.group(2)}-{m.group(3)}"62 else:63 d = _DATE_RE.search(block)64 start = parse_date_fr(clean_text(d.group(1))) if d else None65 if not start:66 continue67 desc = _DESC_RE.search(block)68 img = _IMG_RE.search(block)69 rows.append({70 "url": url,71 "title": clean_text(_html.unescape(72 re.sub(r"<[^>]+>", " ", t.group(2)))),73 "start": start,74 "description": clean_text(_html.unescape(re.sub(75 r"<[^>]+>", " ", desc.group(1)))) if desc else "",76 "image": img.group(1) if img else "",77 })78 return rows7980 def _fiche(self, url: str) -> dict:81 html = self.get(url).text82 out: dict = {}83 m = _F_FIN_RE.search(html)84 if m:85 end = parse_date_fr(clean_text(re.sub(r"<[^>]+>", " ", m.group(1))))86 if end:87 out["end"] = end88 m = _F_LIEU_RE.search(html)89 if m:90 out["venue"] = clean_text(_html.unescape(91 re.sub(r"<[^>]+>", " ", m.group(1))))92 m = _F_HEURE_RE.search(html)93 if m:94 t = parse_time(clean_text(re.sub(r"<[^>]+>", " ", m.group(1))))95 if t:96 out["start_time"] = t97 return out9899 def fetch(self) -> list[Event]:100 rows: list[dict] = []101 seen_urls: set[str] = set()102 for page in range(1, PAGES_MAX + 1):103 url = LIST_URL if page == 1 else f"{LIST_URL}page/{page}/"104 try:105 batch = self._parse_page(self.get(url).text)106 except Exception:107 if page > 1: # fin de pagination (404)108 break109 raise110 new = [r for r in batch if r["url"] not in seen_urls]111 if not new:112 break113 seen_urls.update(r["url"] for r in new)114 rows += new115 cache = self.load_cache()116 fetched = 0117 for r in rows:118 # « 2026/09/05/<slug> » : la date de l'URL fait partie de la clé119 key = "/".join(r["url"].rstrip("/").split("/")[-4:])120 r["key"] = key121 if key not in cache and fetched < FICHES_MAX:122 try:123 cache[key] = self._fiche(r["url"])124 fetched += 1125 except Exception as exc: # fiche cassée ≠ source cassée126 print(f"[sorti-ka] shawinigan : fiche {key} ignorée : {exc}")127 if fetched:128 self.save_cache(cache)129 events: list[Event] = []130 seen: set[str] = set()131 for r in rows:132 ext = r["key"].replace("/", "-")133 if ext in seen:134 continue135 seen.add(ext)136 extra = cache.get(r["key"]) or {}137 events.append(Event(138 source=self.source_id,139 external_id=ext,140 url=r["url"],141 title=r["title"],142 description=r["description"],143 raw_categories=[r["title"]],144 venue=extra.get("venue", ""),145 city="Shawinigan",146 region="Mauricie",147 start_date=r["start"],148 start_time=extra.get("start_time"),149 end_date=extra.get("end") or r["start"],150 organizer="Ville de Shawinigan",151 image=r["image"],152 ))153 return events154