Cliquenabend: Release-Feld der Spieldetailseiten parsen (echtes Erscheinungsjahr statt Update-Datum)

This commit is contained in:
Flo Hartmann
2026-08-25 00:44:31 +00:00
parent aea52d1563
commit 199f8b080d
2 changed files with 85 additions and 5 deletions

View File

@@ -10,12 +10,17 @@ Die Seite ist nicht paginiert.
""" """
from __future__ import annotations from __future__ import annotations
import logging
import re import re
from urllib.parse import urljoin
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from bs4.element import Tag
from .basis import QuellenAdapter, QuellenTreffer, bild_aus_element from .basis import QuellenAdapter, QuellenTreffer, bild_aus_element
_logger = logging.getLogger(__name__)
DATUM_MUSTER = re.compile(r"\d{2}\.\d{2}\.(\d{2})\b") DATUM_MUSTER = re.compile(r"\d{2}\.\d{2}\.(\d{2})\b")
BASIS_URL = "https://www.cliquenabend.de" BASIS_URL = "https://www.cliquenabend.de"
@@ -40,6 +45,42 @@ class CliquenabendQuelle(QuellenAdapter):
treffer.append(self._zeile(zellen, url)) treffer.append(self._zeile(zellen, url))
return [t for t in treffer if t is not None], [] 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
`<b>Release:</b><br> JAHR <br>` — 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 (<br>-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: def _zeile(self, zellen, url: str) -> QuellenTreffer | None:
spiel_zelle = zellen[3] spiel_zelle = zellen[3]
titel_link = spiel_zelle.find("a", href=re.compile(r"^/spiele/")) titel_link = spiel_zelle.find("a", href=re.compile(r"^/spiele/"))
@@ -50,8 +91,6 @@ class CliquenabendQuelle(QuellenAdapter):
titel = (fett or titel_link).get_text(" ", strip=True) titel = (fett or titel_link).get_text(" ", strip=True)
href = titel_link.get("href") href = titel_link.get("href")
if href: if href:
from urllib.parse import urljoin
quellen_url = urljoin(BASIS_URL, href) quellen_url = urljoin(BASIS_URL, href)
if not titel: if not titel:
return None return None
@@ -66,6 +105,15 @@ class CliquenabendQuelle(QuellenAdapter):
# Datum so übernehmen, wie es in der Quelle steht (z. B. „15.10.25“). # Datum so übernehmen, wie es in der Quelle steht (z. B. „15.10.25“).
datum = fund.group(0) 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( return QuellenTreffer(
titel=titel, titel=titel,
verlag=verlag, verlag=verlag,

View File

@@ -124,10 +124,21 @@ CLIQUENABEND_SEITE = """
<tr><td>26.09.25</td><td>Piatnik Profil ,</td><td>6-G200</td> <tr><td>26.09.25</td><td>Piatnik Profil ,</td><td>6-G200</td>
<td><div><a href="/spiele/790700-Elixirus.html"><b>Elixirus</b></a></div></td> <td><div><a href="/spiele/790700-Elixirus.html"><b>Elixirus</b></a></div></td>
<td>Dickie Chapin</td></tr> <td>Dickie Chapin</td></tr>
<tr><td>01.10.25</td><td>ohne Link</td><td></td><td>Nur Texttitel</td><td>Unbekannt</td></tr> <tr><td>01.10.25</td><td>ohne Link</td><td>&mdash;</td><td>Nur Texttitel</td><td>Unbekannt</td></tr>
</table> </table>
""" """
CLIQUENABEND_DETAIL_1 = """
<html><body>
<b>Release:</b><br>
2025 <br>
<b>Anzahl der Spieler:</b><br>
1 bis 4 Spieler<br>
</body></html>
"""
CLIQUENABEND_DETAIL_FEHLT = "<html><body><p>Seite ohne Release-Feld</p></body></html>"
class FakeUhr: class FakeUhr:
"""Steuerbare Monoton-Uhr.""" """Steuerbare Monoton-Uhr."""
@@ -306,7 +317,11 @@ def test_parser_spielessen_nutzt_json_api_endpunkt():
def test_parser_cliquenabend_liest_tabellenspalten(): def test_parser_cliquenabend_liest_tabellenspalten():
adapter, _ = _adapter( adapter, _ = _adapter(
CliquenabendQuelle, CliquenabendQuelle,
{"https://www.cliquenabend.de/events/130000-SPIEL-2025-Essen.html": CLIQUENABEND_SEITE}, {
"https://www.cliquenabend.de/events/130000-SPIEL-2025-Essen.html": CLIQUENABEND_SEITE,
"https://www.cliquenabend.de/spiele/790610-Galactic-Cruise.html": CLIQUENABEND_DETAIL_1,
"https://www.cliquenabend.de/spiele/790700-Elixirus.html": CLIQUENABEND_DETAIL_FEHLT,
},
) )
ergebnis = adapter.sammle() ergebnis = adapter.sammle()
@@ -314,9 +329,26 @@ def test_parser_cliquenabend_liest_tabellenspalten():
erster = ergebnis.treffer[0] erster = ergebnis.treffer[0]
assert erster.verlag == "Dranda Games, PD Verlag" assert erster.verlag == "Dranda Games, PD Verlag"
assert erster.autor == "T.K. King und Koltin Thompson" assert erster.autor == "T.K. King und Koltin Thompson"
assert erster.erscheinungsdatum_oder_quartal == "15.10.25" # Release-Feld der Detailseite schlägt das Update-Datum der Tabelle:
assert erster.erscheinungsdatum_oder_quartal == "2025"
assert erster.quellen_url == "https://www.cliquenabend.de/spiele/790610-Galactic-Cruise.html" assert erster.quellen_url == "https://www.cliquenabend.de/spiele/790610-Galactic-Cruise.html"
assert ergebnis.treffer[1].verlag == "Piatnik" assert ergebnis.treffer[1].verlag == "Piatnik"
# Ohne Release-Feld auf der Detailseite fällt der Parser auf das
# Update-Datum zurück:
assert ergebnis.treffer[1].erscheinungsdatum_oder_quartal == "26.09.25"
def test_parser_cliquenabend_detailseite_fehler_blockiert_nicht():
adapter, _ = _adapter(
CliquenabendQuelle,
{
"https://www.cliquenabend.de/events/130000-SPIEL-2025-Essen.html": CLIQUENABEND_SEITE,
# Detailseiten nicht erreichbar → Update-Datum bleibt erhalten.
},
)
ergebnis = adapter.sammle()
assert len(ergebnis.treffer) == 2
assert ergebnis.treffer[0].erscheinungsdatum_oder_quartal == "15.10.25"
def test_parser_cliquenabend_toleriert_fehlende_tabelle(): def test_parser_cliquenabend_toleriert_fehlende_tabelle():