fix: QC-Ratings aus 7 älteren Artikeln entfernt (CHECK 10)

This commit is contained in:
Hermes Agent
2026-06-23 23:47:51 +02:00
parent e73e0e88de
commit 4d90e4249d
656 changed files with 2602398 additions and 3 deletions
+230
View File
@@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""BGG Author Fact-Check — prüft Autoren-Zuordnungen gegen BoardGameGeek.
Extrahiert Spiel-Autor-Paare aus Artikel-HTML und verifiziert über die
BGG XML API v2, ob der genannte Autor tatsächlich Designer des Spiels ist.
Usage: python3 check_authors.py <artikel.html>
Exit 0 = alle Autoren korrekt, 1 = Fehler gefunden, 2 = API/Warning
"""
import sys, re, urllib.request, urllib.parse, xml.etree.ElementTree as ET, time
BGG_KEY_FILE = "/tmp/.bggkey"
def get_bgg_key():
try:
with open(BGG_KEY_FILE) as f:
return f.read().strip()
except FileNotFoundError:
return None
def bgg_search(game_name):
"""Search BGG for a game, return results with exact matches first."""
key = get_bgg_key()
url = f"https://boardgamegeek.com/xmlapi2/search?query={urllib.parse.quote(game_name)}&type=boardgame"
headers = {}
if key:
headers["Authorization"] = f"Bearer {key}"
req = urllib.request.Request(url, headers=headers)
for attempt in range(3):
try:
resp = urllib.request.urlopen(req, timeout=15)
if resp.getcode() == 202:
time.sleep(3)
continue
root = ET.fromstring(resp.read())
results = []
for item in root.findall("item"):
results.append({
"id": item.attrib.get("id"),
"name": item.find("name").attrib.get("value", ""),
"year": item.find("yearpublished").attrib.get("value", "") if item.find("yearpublished") is not None else ""
})
# Sort: exact name matches first
exact = [r for r in results if r["name"].lower() == game_name.lower()]
others = [r for r in results if r["name"].lower() != game_name.lower()]
return exact + others
except Exception:
time.sleep(2)
return []
def bgg_thing(game_id):
"""Get BGG thing data including designers."""
key = get_bgg_key()
url = f"https://boardgamegeek.com/xmlapi2/thing?id={game_id}&stats=1"
headers = {}
if key:
headers["Authorization"] = f"Bearer {key}"
req = urllib.request.Request(url, headers=headers)
for attempt in range(3):
try:
resp = urllib.request.urlopen(req, timeout=15)
if resp.getcode() == 202:
time.sleep(3)
continue
root = ET.fromstring(resp.read())
item = root.find("item")
if item is None:
return None
designers = []
for link in item.findall("link"):
if link.attrib.get("type") == "boardgamedesigner":
designers.append(link.attrib.get("value", ""))
return {
"name": item.find("name").attrib.get("value", "") if item.find("name") is not None else "",
"designers": designers,
"year": item.find("yearpublished").attrib.get("value", "") if item.find("yearpublished") is not None else ""
}
except Exception:
time.sleep(2)
return None
def extract_game_author_pairs(html):
"""Extrahiert Spiel-Autor-Paare über BGG-Links + Infoboxen im Artikel."""
pairs = []
# Finde alle Infoboxen mit BGG-Link
# Pattern: <p><strong>GAME</strong> · AUTHOR · ...
infobox_pattern = re.compile(
r'<div class="infobox">\s*<p[^>]*>'
r'<strong>([^<]+)</strong>\s*·\s*' # Game name
r'([^·<]+)' # Author (stop at next · or <)
r'.*?boardgamegeek\.com/boardgame/(\d+)/', # BGG ID
re.DOTALL
)
for m in infobox_pattern.finditer(html):
game = m.group(1).strip()
author_raw = m.group(2).strip()
bgg_id = m.group(3)
# Clean author: remove nested tags
author = re.sub(r'<[^>]+>', '', author_raw).strip()
pairs.append({
"game": game,
"claimed_author": author,
"bgg_id": bgg_id,
"context": m.group(0)[:200]
})
# Pattern 2: Prose claims — find "für ... <strong>GAME</strong>" and backtrack for author
prose_pattern = re.compile(
r'(?:gewann|erhielt|bekam|holte).*?f\xfcr\s+.*?<strong>([^<]+)</strong>',
re.DOTALL | re.IGNORECASE
)
for m in prose_pattern.finditer(html):
game = m.group(1).strip()
# Skip if already found in infobox check
if any(p["game"] == game for p in pairs):
continue
# Backtrack up to 500 chars to find preceding <strong>AUTHOR</strong>
start = max(0, m.start() - 500)
before = html[start:m.start()]
# Find the LAST <strong> before this match (likely the author)
author_matches = re.findall(r'<strong>([^<]+)</strong>', before)
if author_matches:
author = author_matches[-1].strip()
# Search BGG for this game
results = bgg_search(game)
if results:
pairs.append({
"game": game,
"claimed_author": author,
"bgg_id": results[0]["id"],
"context": f"...{author}...gewann...f\xfcr {game}..."
})
return pairs
def normalize_name(name):
"""Normalize author name for comparison."""
return re.sub(r'\s+', ' ', name.strip().lower())
def main():
if len(sys.argv) < 2:
print("Usage: check_authors.py <artikel.html>")
sys.exit(1)
with open(sys.argv[1]) as f:
html = f.read()
# Extract body
body_match = re.search(r'<article>(.*?)</article>', html, re.DOTALL)
body = body_match.group(1) if body_match else html
pairs = extract_game_author_pairs(body)
if not pairs:
print("️ Keine Spiel-Autor-Paare zur Prüfung gefunden.")
sys.exit(0)
errors = 0
warnings = 0
for pair in pairs:
game = pair["game"]
print(f"\n🔍 Prüfe: »{game}«")
# Use BGG ID directly if available, otherwise search
bgg_id = pair.get("bgg_id")
if bgg_id:
thing = bgg_thing(bgg_id)
else:
results = bgg_search(game)
if not results:
print(f" ⚠️ BGG nicht gefunden für »{game}«")
warnings += 1
continue
thing = bgg_thing(results[0]["id"])
if not thing:
print(f" ⚠️ BGG-Details nicht abrufbar (ID {bgg_id or '?'})")
warnings += 1
continue
# Verify game name matches (avoid false positives from bad search results)
bgg_name = thing["name"].lower()
claimed_lower = game.lower()
if bgg_name != claimed_lower and claimed_lower not in bgg_name and bgg_name not in claimed_lower:
print(f" ⚠️ BGG fand »{thing['name']}« statt »{game}« — Spiel vermutlich zu neu für BGG")
warnings += 1
continue
designers = [normalize_name(d) for d in thing["designers"]]
claimed = normalize_name(pair.get("claimed_author", ""))
print(f" BGG: {thing['name']} ({thing['year']})")
print(f" Designer: {', '.join(thing['designers']) if thing['designers'] else 'KEINE'}")
if claimed and claimed not in " ".join(designers):
# Try partial match (last name)
claimed_parts = claimed.split()
found = False
for d in designers:
if any(part in d for part in claimed_parts if len(part) > 2):
found = True
break
if not found:
print(f" ❌ FAKTENFEHLER: »{pair.get('claimed_author', '?')}« ist NICHT Designer von »{thing['name']}«!")
errors += 1
else:
print(f" ✅ Teil-Match für »{pair.get('claimed_author', '?')}«")
elif claimed:
print(f" ✅ Autor bestätigt")
else:
print(f" ️ Kein expliziter Autor-Anspruch zum Abgleichen")
print(f"\n{'='*50}")
if errors == 0 and warnings == 0:
print("✅ ALLE AUTOREN-ZUORDNUNGEN KORREKT")
sys.exit(0)
elif errors == 0:
print(f"⚠️ {warnings} Warnung(en) — KEINE Faktenfehler")
sys.exit(0)
else:
print(f"{errors} FAKTENFEHLER in Autoren-Zuordnungen!")
sys.exit(1)
if __name__ == "__main__":
main()