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 · 154 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/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