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/victoriaville.py : connecteur Ville de Victoriaville — « Quoi5# faire à Victo » (calendrier municipal officiel).6# Source : https://victoriaville.ca/calendriers-cours-et-ateliers/7# quoi-faire-a-victo (Craft CMS, rendu serveur).8# Extraction: la page liste UNE SEMAINE de séances, jour par jour9# (sections id="day-YYYY-MM-DD", ~115 <article> par semaine :10# titre, lieu, heures « De 9 h à 17 h », image CloudFront,11# lien fiche ?date=YYYY-MM-DD). Navigation par12# ?dateDebut=YYYY-MM-DD → on couvre VIC_WEEKS semaines13# (défaut 4) = 1 GET/semaine. Vérifié 2026-08-25.14# Accès : page municipale publique, robots.txt permissif (Craft :15# /cpresources/ et /vendor/ seulement). GET directs throttlés,16# identification honnête.17# Prix : non publié dans la liste → jamais inventé.18# -----------------------------------------------------------------------------19from __future__ import annotations2021import os22import re23from datetime import date, timedelta2425from ..normalize import clean_text, parse_time26from ..schema import Event27from .base import BaseConnector2829LIST_URL = ("https://victoriaville.ca/calendriers-cours-et-ateliers/"30 "quoi-faire-a-victo")31WEEKS = int(os.environ.get("VIC_WEEKS", "4"))3233_DAY_RE = re.compile(r'id="day-(\d{4}-\d{2}-\d{2})"')34_ARTICLE_RE = re.compile(35 r'<article class="thumbnails-listing-item">(.*?)</article>', re.S)36_LINK_RE = re.compile(37 r'href="(https://victoriaville\.ca/[^"?]+)\?date=(\d{4}-\d{2}-\d{2})"')38_TITLE_RE = re.compile(39 r'thumbnails-listing-item-title[^>]*>\s*(.*?)\s*</h3>', re.S)40_LIEU_RE = re.compile(41 r'thumbnails-listing-item-lieu[^>]*>\s*(.*?)\s*</p>', re.S)42_HOURS_RE = re.compile(43 r'thumbnails-listing-item-hours[^>]*>(.*?)</p>', re.S)44_IMG_RE = re.compile(r'<img[^>]+src="([^"]+)"')45# « De 9 h à 17 h », « 19 h 30 » — début puis fin éventuelle46_RANGE_RE = re.compile(47 r"(\d{1,2}\s*h(?:\s*\d{2})?)\s*(?:à|-|–)\s*(\d{1,2}\s*h(?:\s*\d{2})?)", re.I)484950class VictoriavilleConnector(BaseConnector):51 source_id = "victoriaville"52 request_delay = 1.05354 def _parse_week(self, html: str) -> list[dict]:55 """Découpe la page en sections-jour puis en cartes-séance."""56 rows: list[dict] = []57 marks = [(m.start(), m.group(1)) for m in _DAY_RE.finditer(html)]58 for i, (pos, day) in enumerate(marks):59 end = marks[i + 1][0] if i + 1 < len(marks) else len(html)60 for block in _ARTICLE_RE.findall(html[pos:end]):61 link = _LINK_RE.search(block)62 title = _TITLE_RE.search(block)63 if not link or not title:64 continue65 hours_txt = ""66 m = _HOURS_RE.search(block)67 if m:68 hours_txt = clean_text(re.sub(r"<[^>]+>", " ", m.group(1)))69 start_time = end_time = None70 rng = _RANGE_RE.search(hours_txt)71 if rng:72 start_time = parse_time(rng.group(1))73 end_time = parse_time(rng.group(2))74 else:75 start_time = parse_time(hours_txt)76 # « 00h00 » = placeholder « toute la journée » (vérifié live77 # 2026-08-25 : 28 cartes/semaine) → heure jamais inventée78 if start_time == "00:00":79 start_time = None80 if end_time == "00:00":81 end_time = None82 lieu = _LIEU_RE.search(block)83 img = _IMG_RE.search(block)84 rows.append({85 "url": link.group(1),86 "date": day,87 "title": clean_text(re.sub(r"<[^>]+>", "", title.group(1))),88 "venue": clean_text(re.sub(89 r"<[^>]+>", "", lieu.group(1))) if lieu else "",90 "start_time": start_time,91 "end_time": end_time,92 "image": img.group(1) if img else "",93 })94 return rows9596 def fetch(self) -> list[Event]:97 rows: list[dict] = []98 today = date.today()99 for week in range(WEEKS):100 url = LIST_URL101 params = None102 if week:103 params = {"dateDebut": (today + timedelta(days=7 * week))104 .isoformat(), "recherche": "",105 "programmationSpeciale": ""}106 try:107 rows += self._parse_week(self.get(url, params=params).text)108 except Exception as exc: # une semaine cassée ≠ source cassée109 if week == 0:110 raise111 print(f"[sorti-ka] victoriaville : semaine +{week} ignorée : {exc}")112 events: list[Event] = []113 seen: set[str] = set()114 for r in rows:115 slug = r["url"].rstrip("/").rsplit("/", 1)[-1]116 ext = f"{slug}-{r['date']}"117 if ext in seen or not r["title"]:118 continue119 seen.add(ext)120 events.append(Event(121 source=self.source_id,122 external_id=ext,123 url=f"{r['url']}?date={r['date']}",124 title=r["title"],125 raw_categories=[r["title"]],126 venue=r["venue"],127 city="Victoriaville",128 region="Centre-du-Québec",129 start_date=r["date"],130 start_time=r["start_time"],131 end_date=r["date"],132 end_time=r["end_time"],133 organizer="Ville de Victoriaville",134 image=r["image"],135 ))136 return events137