fix: QC-Ratings aus 7 älteren Artikeln entfernt (CHECK 10)
This commit is contained in:
Executable
+230
@@ -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()
|
||||
Reference in New Issue
Block a user