Cliquenabend: Release-Feld der Spieldetailseiten parsen (echtes Erscheinungsjahr statt Update-Datum)
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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>—</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():
|
||||||
|
|||||||
Reference in New Issue
Block a user