"""Adapter für cliquenabend.de Messe-Eventseiten (z. B. SPIEL 2025 Essen). Seite: …/events/130000-SPIEL-2025-Essen.html (große Einzelseite, ~4 MB). Struktur (Stand Analyse August 2026): Eine einzige Tabelle mit der Kopfzeile Update | Verlag | Stand | Spiel | Autor; eine Zeile pro Spiel. Titel steht in `a[href^="/spiele/"] > b`, Verlage in fett formatierten `div`-Blöcken der Verlagsspalte, das Update-Datum in der Form TT.MM.JJ. Die Seite ist nicht paginiert. """ from __future__ import annotations import logging import re from urllib.parse import urljoin from bs4 import BeautifulSoup from bs4.element import Tag from .basis import QuellenAdapter, QuellenTreffer, bild_aus_element _logger = logging.getLogger(__name__) DATUM_MUSTER = re.compile(r"\d{2}\.\d{2}\.(\d{2})\b") BASIS_URL = "https://www.cliquenabend.de" class CliquenabendQuelle(QuellenAdapter): name = "cliquenabend" anzeigename = "Cliquenabend Messe-Übersicht" start_urls = ("https://www.cliquenabend.de/events/130000-SPIEL-2025-Essen.html",) def seite_verarbeiten(self, inhalt: bytes, url: str): suppe = BeautifulSoup(inhalt, "html.parser") treffer: list[QuellenTreffer] = [] tabelle = suppe.find("table") if tabelle is None: return [], [] # Strukturänderung: nichts tun, kein Crash for zeile in tabelle.find_all("tr"): zellen = zeile.find_all("td") if len(zellen) < 5: continue if zellen[0].get_text(strip=True).lower() == "update": continue # Kopfzeile treffer.append(self._zeile(zellen, url)) return [t for t in treffer if t is not None], [] def _release_jahr(self, spiel_url: str) -> str | None: """Liest das Release-Feld von der Spieldetailseite. Die Detailseite (…/spiele/….html) enthält ein Feld `Release:
JAHR
` — meist nur das Jahr (z. B. „2025“), gelegentlich ein genaues Datum. Liefert die rohe Angabe zurück; `jahr_aus_datumsangabe` leitet daraus später das Jahr ab. Fehler (404, Strukturänderung) führen zu None und blockieren nie. """ try: antwort = self.client.hole(spiel_url) except Exception: _logger.warning("Cliquenabend: Detailseite nicht erreichbar (%s)", spiel_url) return None if antwort.status_code != 200: return None suppe = BeautifulSoup(antwort.content, "html.parser") fett = suppe.find( "b", string=lambda s: isinstance(s, str) and s.strip() == "Release:" ) if not isinstance(fett, Tag): return None # Wert steht direkt hinter dem Label (
-getrennt); der nächste # String-Knoten nach dem Label-Element (nicht darin) ist der Wert. text = fett.next_sibling while isinstance(text, Tag): text = text.next_sibling while isinstance(text, str) and not text.strip(): text = text.next_sibling while isinstance(text, Tag): text = text.next_sibling if not isinstance(text, str): return None wert = text.strip() return wert or None def _zeile(self, zellen, url: str) -> QuellenTreffer | None: spiel_zelle = zellen[3] titel_link = spiel_zelle.find("a", href=re.compile(r"^/spiele/")) titel = None quellen_url = url if titel_link is not None: fett = titel_link.find("b") titel = (fett or titel_link).get_text(" ", strip=True) href = titel_link.get("href") if href: quellen_url = urljoin(BASIS_URL, href) if not titel: return None verlag = _verlage(zellen[1]) autor = zellen[4].get_text(" ", strip=True) or None datum = None jahr_text = zellen[0].get_text(strip=True) fund = DATUM_MUSTER.search(jahr_text) if fund: # Datum so übernehmen, wie es in der Quelle steht (z. B. „15.10.25“). datum = fund.group(0) # Release-Feld der Detailseite hat Vorrang: dort steht das echte # Erscheinungsjahr/-datum statt des Update-Datums der Mese-Tabelle. if titel_link is not None: href = titel_link.get("href") if href: release = self._release_jahr(urljoin(BASIS_URL, href)) if release: datum = release return QuellenTreffer( titel=titel, verlag=verlag, autor=autor, erscheinungsdatum_oder_quartal=datum, quellen_url=quellen_url, bild_url=bild_aus_element(spiel_zelle, quellen_url), ) def _verlage(zelle) -> str | None: """Verlagsspalte: fett formatierte `div`-Blöcke enthalten die Namen.""" namen = [ div.get_text(" ", strip=True) for div in zelle.find_all("div") if div.get_text(" ", strip=True) ] if not namen: roh = zelle.get_text(" ", strip=True) namen = [teil for teil in (roh.split(",") if roh else []) if teil.strip() and teil.strip().lower() != "profil"] bereinigt = [] for name in namen: name = name.strip().rstrip(",").strip() # „Piatnik Profil ,“ → Profil-Link-Text hängt am Namen; abschneiden. if name.lower().endswith(" profil"): name = name[: -len(" Profil")].strip() if name and name.lower() != "profil": bereinigt.append(name) return ", ".join(bereinigt) if bereinigt else None