Files
BSN-Chatsystem/check_authors.py
T

231 lines
8.4 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()