- Scraper: HTML-Extraktion von ECOnGOOD-Webseite - Analyzer: LLM-Bewertung (Qwen) nach GWÖ-Matrix 2.0 - Aggregator: Partei-Auswertung + Kandidat:innen-Ranking - CLI: Reproduzierbarer Workflow (scrape → analyze → aggregate) - Output: 7 Dokumente inkl. Pressemitteilung und Methodik - 27 Kandidat:innen, 162 Einzelbewertungen
691 lines
20 KiB
Markdown
691 lines
20 KiB
Markdown
# GWÖ-Wahlprüfsteine Auswertung — Nachbau-Anleitung
|
||
|
||
Diese Anleitung erklärt, wie du das Tool für eigene Wahlprüfstein-Auswertungen nachbauen oder anpassen kannst.
|
||
|
||
---
|
||
|
||
## Was macht dieses Tool?
|
||
|
||
1. **Scrapen:** Lädt Wahlprüfstein-Antworten von einer Webseite
|
||
2. **Analysieren:** Bewertet jede Antwort automatisch nach GWÖ-Kriterien (mit KI)
|
||
3. **Aggregieren:** Fasst Ergebnisse nach Parteien zusammen
|
||
4. **Berichten:** Erstellt Markdown-Reports mit Zitaten und Empfehlungen
|
||
|
||
**Ergebnis:** Objektive, reproduzierbare Auswertung von Wahlprüfsteinen aus GWÖ-Perspektive.
|
||
|
||
---
|
||
|
||
## Voraussetzungen
|
||
|
||
### Software
|
||
- **Python 3.11+** (macOS/Linux: vorinstalliert, Windows: python.org)
|
||
- **Git** (optional, für Versionierung)
|
||
|
||
### API-Zugang
|
||
Du brauchst einen LLM-API-Zugang. Optionen:
|
||
|
||
| Anbieter | Modell | Kosten | Anmerkung |
|
||
|----------|--------|--------|-----------|
|
||
| **Alibaba DashScope** | qwen-plus | ~$0.001/Call | Empfohlen, günstig |
|
||
| **OpenAI** | gpt-4o-mini | ~$0.003/Call | Teurer, aber bekannt |
|
||
| **Anthropic** | claude-3-haiku | ~$0.002/Call | Gute Alternative |
|
||
|
||
**Für DashScope (empfohlen):**
|
||
1. Account erstellen: https://dashscope.console.aliyun.com
|
||
2. API-Key generieren
|
||
3. Key sicher speichern (siehe unten)
|
||
|
||
---
|
||
|
||
## Installation
|
||
|
||
### 1. Projektordner erstellen
|
||
|
||
```bash
|
||
mkdir -p ~/projekte/wahlpruefsteine-auswertung
|
||
cd ~/projekte/wahlpruefsteine-auswertung
|
||
```
|
||
|
||
### 2. Python Virtual Environment
|
||
|
||
```bash
|
||
python3 -m venv .venv
|
||
source .venv/bin/activate # Linux/macOS
|
||
# oder: .venv\Scripts\activate # Windows
|
||
```
|
||
|
||
### 3. Abhängigkeiten installieren
|
||
|
||
```bash
|
||
pip install beautifulsoup4 requests openai
|
||
```
|
||
|
||
### 4. API-Key konfigurieren
|
||
|
||
**Option A: Umgebungsvariable (einfach)**
|
||
```bash
|
||
export DASHSCOPE_API_KEY="sk-xxx..."
|
||
```
|
||
|
||
**Option B: macOS Keychain (sicher)**
|
||
```bash
|
||
security add-generic-password -s qwen-api -a $USER -w "sk-xxx..."
|
||
```
|
||
|
||
**Option C: .env-Datei**
|
||
```bash
|
||
echo "DASHSCOPE_API_KEY=sk-xxx..." > .env
|
||
pip install python-dotenv
|
||
# Dann in Python: from dotenv import load_dotenv; load_dotenv()
|
||
```
|
||
|
||
---
|
||
|
||
## Dateien erstellen
|
||
|
||
Erstelle diese 4 Dateien im Projektordner:
|
||
|
||
### 1. `schema.sql` — Datenbankstruktur
|
||
|
||
```sql
|
||
-- Kandidat:innen
|
||
CREATE TABLE IF NOT EXISTS kandidaten (
|
||
id INTEGER PRIMARY KEY,
|
||
vorname TEXT NOT NULL,
|
||
nachname TEXT NOT NULL,
|
||
plz TEXT,
|
||
kommune TEXT NOT NULL,
|
||
landkreis TEXT,
|
||
partei_raw TEXT NOT NULL,
|
||
partei_normalisiert TEXT NOT NULL,
|
||
ist_waehlergemeinschaft BOOLEAN DEFAULT FALSE,
|
||
pdf_url TEXT,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
-- Die 6 Fragen (anpassen an deine Wahlprüfsteine!)
|
||
CREATE TABLE IF NOT EXISTS fragen (
|
||
id INTEGER PRIMARY KEY,
|
||
nummer INTEGER UNIQUE NOT NULL,
|
||
kurztext TEXT NOT NULL,
|
||
volltext TEXT NOT NULL
|
||
);
|
||
|
||
-- Rohantworten
|
||
CREATE TABLE IF NOT EXISTS antworten_raw (
|
||
id INTEGER PRIMARY KEY,
|
||
kandidat_id INTEGER REFERENCES kandidaten(id) ON DELETE CASCADE,
|
||
frage_id INTEGER REFERENCES fragen(id) ON DELETE CASCADE,
|
||
antwort_kurz TEXT,
|
||
antwort_erlaeuterung TEXT,
|
||
UNIQUE(kandidat_id, frage_id)
|
||
);
|
||
|
||
-- LLM-Bewertungen
|
||
CREATE TABLE IF NOT EXISTS bewertungen (
|
||
id INTEGER PRIMARY KEY,
|
||
antwort_id INTEGER REFERENCES antworten_raw(id) ON DELETE CASCADE UNIQUE,
|
||
substanz_score INTEGER CHECK(substanz_score >= 0 AND substanz_score <= 3),
|
||
umfang TEXT CHECK(umfang IN ('keine', 'kurz', 'mittel', 'ausführlich')),
|
||
wortanzahl INTEGER,
|
||
gwoe_score REAL CHECK(gwoe_score >= 0 AND gwoe_score <= 10),
|
||
gwoe_begruendung TEXT,
|
||
matrix_felder TEXT, -- JSON-Array
|
||
staerken TEXT, -- JSON-Array
|
||
schwaechen TEXT, -- JSON-Array
|
||
bewertet_am DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
model TEXT DEFAULT 'qwen-plus'
|
||
);
|
||
|
||
-- Stammdaten: Fragen (ANPASSEN!)
|
||
INSERT OR IGNORE INTO fragen (nummer, kurztext, volltext) VALUES
|
||
(1, 'Leitlinien', 'Werden Sie sich für Maßnahmen einsetzen, welche die Werte und Themen der GWÖ in Leitlinien und Strategien Ihrer Kommune integrieren?'),
|
||
(2, 'Anreize', 'Werden Sie sich für Anreize einsetzen, um Unternehmen zu unterstützen, gemeinwohl-orientierter zu wirtschaften?'),
|
||
(3, 'Vergabe', 'Werden Sie sich dafür einsetzen, dass öffentliche Aufträge bevorzugt an Unternehmen mit Gemeinwohl-Bilanz vergeben werden?'),
|
||
(4, 'Information', 'Möchten Sie Bürger:innen regelmäßig über die Gemeinwohl-Auswirkungen kommunaler Entwicklungen informieren?'),
|
||
(5, 'Mitentscheidung', 'Möchten Sie Bürger:innen stärker in kommunale Entscheidungsprozesse einbinden?'),
|
||
(6, 'Bekanntheit', 'Möchten Sie die GWÖ in Ihrer Kommune und auf höheren Ebenen bekannter machen?');
|
||
```
|
||
|
||
### 2. `scraper.py` — Daten laden
|
||
|
||
```python
|
||
#!/usr/bin/env python3
|
||
"""
|
||
Wahlprüfsteine Scraper — ANPASSEN AN DEINE DATENQUELLE!
|
||
"""
|
||
|
||
import re
|
||
import sqlite3
|
||
from pathlib import Path
|
||
from bs4 import BeautifulSoup
|
||
import requests
|
||
|
||
# Partei-Normalisierung (ANPASSEN für dein Bundesland!)
|
||
PARTEI_MAPPING = {
|
||
r'bündnis\s*90\s*/?\s*die\s*grünen?': 'Grüne',
|
||
r'grüne': 'Grüne',
|
||
r'freie\s*wähler': 'Freie Wähler',
|
||
r'^csu$': 'CSU',
|
||
r'^cdu$': 'CDU',
|
||
r'^spd$': 'SPD',
|
||
r'^fdp$': 'FDP',
|
||
r'^ödp': 'ÖDP',
|
||
r'die\s*linke': 'Linke',
|
||
r'^afd$': 'AfD',
|
||
}
|
||
|
||
def normalize_partei(raw: str) -> tuple[str, bool]:
|
||
"""Normalisiert Parteinamen. Returns: (partei, ist_wählergemeinschaft)"""
|
||
raw_lower = raw.lower().strip()
|
||
|
||
for pattern, normalized in PARTEI_MAPPING.items():
|
||
if re.search(pattern, raw_lower, re.IGNORECASE):
|
||
return normalized, False
|
||
|
||
return 'Wählergemeinschaft', True
|
||
|
||
def parse_html(html_content: str) -> list[dict]:
|
||
"""
|
||
Parst HTML und extrahiert Kandidaten + Antworten.
|
||
|
||
WICHTIG: Diese Funktion musst du an deine HTML-Struktur anpassen!
|
||
"""
|
||
soup = BeautifulSoup(html_content, 'html.parser')
|
||
kandidaten = []
|
||
|
||
# BEISPIEL: Tabelle mit Kandidaten parsen
|
||
# ANPASSEN an deine HTML-Struktur!
|
||
for table in soup.find_all('table'):
|
||
rows = table.find_all('tr')
|
||
|
||
for row in rows[1:]: # Header überspringen
|
||
cells = row.find_all('td')
|
||
if len(cells) < 7:
|
||
continue
|
||
|
||
try:
|
||
kandidaten.append({
|
||
'plz': cells[0].get_text(strip=True),
|
||
'kommune': cells[1].get_text(strip=True),
|
||
'landkreis': cells[2].get_text(strip=True),
|
||
'vorname': cells[3].get_text(strip=True),
|
||
'nachname': cells[4].get_text(strip=True),
|
||
'partei_raw': cells[5].get_text(strip=True),
|
||
'pdf_url': cells[6].find('a')['href'] if cells[6].find('a') else None,
|
||
'antworten': {} # Frage-Nr -> (ja_nein, erläuterung)
|
||
})
|
||
|
||
# Antworten extrahieren (6 Fragen × 2 Spalten)
|
||
for i in range(6):
|
||
ja_nein = cells[7 + i*2].get_text(strip=True) if 7 + i*2 < len(cells) else None
|
||
erlaeuterung = cells[8 + i*2].get_text(strip=True) if 8 + i*2 < len(cells) else None
|
||
kandidaten[-1]['antworten'][i+1] = (ja_nein, erlaeuterung)
|
||
|
||
except (IndexError, KeyError):
|
||
continue
|
||
|
||
return kandidaten
|
||
|
||
def init_db(db_path: Path) -> sqlite3.Connection:
|
||
"""Initialisiert die Datenbank."""
|
||
conn = sqlite3.connect(db_path)
|
||
conn.row_factory = sqlite3.Row
|
||
|
||
schema_path = Path(__file__).parent / 'schema.sql'
|
||
with open(schema_path) as f:
|
||
conn.executescript(f.read())
|
||
|
||
conn.commit()
|
||
return conn
|
||
|
||
def save_to_db(conn: sqlite3.Connection, kandidaten: list[dict]) -> int:
|
||
"""Speichert Kandidaten in der Datenbank."""
|
||
cursor = conn.cursor()
|
||
|
||
for k in kandidaten:
|
||
partei_norm, ist_wg = normalize_partei(k['partei_raw'])
|
||
|
||
cursor.execute("""
|
||
INSERT OR REPLACE INTO kandidaten
|
||
(vorname, nachname, plz, kommune, landkreis, partei_raw,
|
||
partei_normalisiert, ist_waehlergemeinschaft, pdf_url)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""", (k['vorname'], k['nachname'], k['plz'], k['kommune'],
|
||
k['landkreis'], k['partei_raw'], partei_norm, ist_wg, k['pdf_url']))
|
||
|
||
kandidat_id = cursor.lastrowid
|
||
|
||
for frage_nr, (ja_nein, erlaeuterung) in k['antworten'].items():
|
||
cursor.execute("""
|
||
INSERT OR REPLACE INTO antworten_raw
|
||
(kandidat_id, frage_id, antwort_kurz, antwort_erlaeuterung)
|
||
VALUES (?, ?, ?, ?)
|
||
""", (kandidat_id, frage_nr, ja_nein, erlaeuterung))
|
||
|
||
conn.commit()
|
||
return len(kandidaten)
|
||
|
||
def main():
|
||
import argparse
|
||
|
||
parser = argparse.ArgumentParser(description='Wahlprüfsteine Scraper')
|
||
parser.add_argument('--url', required=True, help='URL der Wahlprüfsteine-Seite')
|
||
parser.add_argument('--db', default='wahlpruefsteine.db', help='Datenbankpfad')
|
||
|
||
args = parser.parse_args()
|
||
|
||
print(f"Lade: {args.url}")
|
||
response = requests.get(args.url, timeout=30)
|
||
kandidaten = parse_html(response.text)
|
||
|
||
print(f"Gefunden: {len(kandidaten)} Kandidat:innen")
|
||
|
||
conn = init_db(Path(args.db))
|
||
count = save_to_db(conn, kandidaten)
|
||
conn.close()
|
||
|
||
print(f"Gespeichert: {count} in {args.db}")
|
||
|
||
if __name__ == '__main__':
|
||
main()
|
||
```
|
||
|
||
### 3. `analyzer.py` — KI-Bewertung
|
||
|
||
```python
|
||
#!/usr/bin/env python3
|
||
"""
|
||
GWÖ-Bewertung mit LLM
|
||
"""
|
||
|
||
import sqlite3
|
||
import json
|
||
import os
|
||
import time
|
||
from pathlib import Path
|
||
from openai import OpenAI
|
||
|
||
# GWÖ-Kontext für das LLM
|
||
GWOE_CONTEXT = """
|
||
## GWÖ-Matrix 2.0 für Gemeinden
|
||
|
||
### Die 5 Werte
|
||
1. **Menschenwürde** — Grundrechte, Rechtsstaatlichkeit
|
||
2. **Solidarität** — Gemeinnutz, Mehrwert für Gemeinschaft
|
||
3. **Ökologische Nachhaltigkeit** — Klimaschutz, Ressourcenschonung
|
||
4. **Soziale Gerechtigkeit** — Daseinsvorsorge, gerechte Verteilung
|
||
5. **Transparenz & Mitbestimmung** — Bürgerbeteiligung, Demokratie
|
||
|
||
### Berührungsgruppen
|
||
- A: Lieferant:innen (Beschaffung)
|
||
- B: Finanzpartner:innen (Haushalt)
|
||
- C: Verwaltung (Mitarbeitende)
|
||
- D: Bürger:innen (Daseinsvorsorge) — WICHTIGSTE für Kommunalpolitik!
|
||
- E: Gesellschaft (überregional)
|
||
|
||
### Bewertungsskala
|
||
- 7-10: Vorbildlich (innovative Maßnahmen)
|
||
- 4-6: Erfahren (erkennbare Verbesserungen)
|
||
- 2-3: Fortgeschritten (erste Maßnahmen)
|
||
- 0-1: Basislinie
|
||
"""
|
||
|
||
SYSTEM_PROMPT = f"""Du bist ein GWÖ-Experte und analysierst Wahlprüfstein-Antworten.
|
||
|
||
{GWOE_CONTEXT}
|
||
|
||
Bewerte die Antwort und gib NUR valides JSON zurück:
|
||
|
||
{{
|
||
"substanz_score": 0-3, // 0=keine Antwort, 1=ausweichend, 2=substanziell, 3=umfassend
|
||
"umfang": "keine|kurz|mittel|ausführlich",
|
||
"gwoe_score": 0.0-10.0,
|
||
"gwoe_begruendung": "2-3 Sätze",
|
||
"matrix_felder": ["D5", "C2"], // Berührte Felder
|
||
"staerken": ["Punkt 1"],
|
||
"schwaechen": ["Punkt 1"]
|
||
}}"""
|
||
|
||
def get_api_client():
|
||
"""Erstellt API-Client (ANPASSEN für deinen Anbieter!)"""
|
||
|
||
# Option 1: DashScope (Qwen)
|
||
api_key = os.environ.get('DASHSCOPE_API_KEY')
|
||
if api_key:
|
||
return OpenAI(
|
||
api_key=api_key,
|
||
base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
||
), "qwen-plus"
|
||
|
||
# Option 2: OpenAI
|
||
api_key = os.environ.get('OPENAI_API_KEY')
|
||
if api_key:
|
||
return OpenAI(api_key=api_key), "gpt-4o-mini"
|
||
|
||
raise ValueError("Kein API-Key gefunden! Setze DASHSCOPE_API_KEY oder OPENAI_API_KEY")
|
||
|
||
def analyze_answer(client, model: str, kandidat: str, frage: str, antwort: str) -> dict:
|
||
"""Analysiert eine einzelne Antwort."""
|
||
|
||
user_prompt = f"""## Frage
|
||
{frage}
|
||
|
||
## Antwort von {kandidat}
|
||
{antwort if antwort else "(keine Antwort)"}
|
||
|
||
Bewerte nach GWÖ-Kriterien."""
|
||
|
||
response = client.chat.completions.create(
|
||
model=model,
|
||
messages=[
|
||
{"role": "system", "content": SYSTEM_PROMPT},
|
||
{"role": "user", "content": user_prompt}
|
||
],
|
||
temperature=0.3,
|
||
response_format={"type": "json_object"}
|
||
)
|
||
|
||
return json.loads(response.choices[0].message.content)
|
||
|
||
def main():
|
||
import argparse
|
||
|
||
parser = argparse.ArgumentParser(description='GWÖ-Analyzer')
|
||
parser.add_argument('--db', default='wahlpruefsteine.db')
|
||
parser.add_argument('--limit', type=int, help='Max. Anzahl Bewertungen')
|
||
|
||
args = parser.parse_args()
|
||
|
||
client, model = get_api_client()
|
||
print(f"Verwende Modell: {model}")
|
||
|
||
conn = sqlite3.connect(args.db)
|
||
conn.row_factory = sqlite3.Row
|
||
|
||
# Unbewertete Antworten holen
|
||
query = """
|
||
SELECT ar.id, k.vorname || ' ' || k.nachname as kandidat,
|
||
f.volltext as frage, ar.antwort_erlaeuterung as antwort
|
||
FROM antworten_raw ar
|
||
JOIN kandidaten k ON ar.kandidat_id = k.id
|
||
JOIN fragen f ON ar.frage_id = f.id
|
||
LEFT JOIN bewertungen b ON ar.id = b.antwort_id
|
||
WHERE b.id IS NULL
|
||
"""
|
||
if args.limit:
|
||
query += f" LIMIT {args.limit}"
|
||
|
||
antworten = conn.execute(query).fetchall()
|
||
print(f"Zu bewerten: {len(antworten)}")
|
||
|
||
for i, row in enumerate(antworten, 1):
|
||
print(f"[{i}/{len(antworten)}] {row['kandidat']}...", end=" ", flush=True)
|
||
|
||
try:
|
||
result = analyze_answer(client, model, row['kandidat'], row['frage'], row['antwort'])
|
||
|
||
conn.execute("""
|
||
INSERT INTO bewertungen
|
||
(antwort_id, substanz_score, umfang, gwoe_score, gwoe_begruendung,
|
||
matrix_felder, staerken, schwaechen, model)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""", (
|
||
row['id'],
|
||
result.get('substanz_score'),
|
||
result.get('umfang'),
|
||
result.get('gwoe_score'),
|
||
result.get('gwoe_begruendung'),
|
||
json.dumps(result.get('matrix_felder', [])),
|
||
json.dumps(result.get('staerken', [])),
|
||
json.dumps(result.get('schwaechen', [])),
|
||
model
|
||
))
|
||
conn.commit()
|
||
|
||
print(f"GWÖ: {result.get('gwoe_score', '?')}")
|
||
|
||
except Exception as e:
|
||
print(f"FEHLER: {e}")
|
||
|
||
time.sleep(0.5) # Rate Limiting
|
||
|
||
conn.close()
|
||
print("Fertig!")
|
||
|
||
if __name__ == '__main__':
|
||
main()
|
||
```
|
||
|
||
### 4. `aggregator.py` — Reports erstellen
|
||
|
||
```python
|
||
#!/usr/bin/env python3
|
||
"""
|
||
Aggregation und Report-Generierung
|
||
"""
|
||
|
||
import sqlite3
|
||
import json
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
|
||
def get_partei_stats(conn) -> list[dict]:
|
||
"""Statistiken pro Partei."""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("""
|
||
SELECT
|
||
k.partei_normalisiert AS partei,
|
||
COUNT(DISTINCT k.id) AS kandidaten,
|
||
COUNT(b.id) AS antworten,
|
||
ROUND(AVG(b.substanz_score), 2) AS avg_substanz,
|
||
ROUND(AVG(b.gwoe_score), 2) AS avg_gwoe
|
||
FROM kandidaten k
|
||
LEFT JOIN antworten_raw ar ON k.id = ar.kandidat_id
|
||
LEFT JOIN bewertungen b ON ar.id = b.antwort_id
|
||
WHERE b.id IS NOT NULL
|
||
GROUP BY k.partei_normalisiert
|
||
ORDER BY avg_gwoe DESC
|
||
""")
|
||
|
||
return [dict(row) for row in cursor.fetchall()]
|
||
|
||
def get_top_quotes(conn, partei: str, limit: int = 3) -> list[dict]:
|
||
"""Beste Zitate einer Partei."""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("""
|
||
SELECT
|
||
k.vorname || ' ' || k.nachname AS name,
|
||
k.kommune,
|
||
f.kurztext AS frage,
|
||
ar.antwort_erlaeuterung AS zitat,
|
||
b.gwoe_score
|
||
FROM bewertungen b
|
||
JOIN antworten_raw ar ON b.antwort_id = ar.id
|
||
JOIN kandidaten k ON ar.kandidat_id = k.id
|
||
JOIN fragen f ON ar.frage_id = f.id
|
||
WHERE k.partei_normalisiert = ?
|
||
AND ar.antwort_erlaeuterung IS NOT NULL
|
||
AND LENGTH(ar.antwort_erlaeuterung) > 50
|
||
ORDER BY b.gwoe_score DESC
|
||
LIMIT ?
|
||
""", (partei, limit))
|
||
|
||
return [dict(row) for row in cursor.fetchall()]
|
||
|
||
def generate_report(stats: list[dict], conn) -> str:
|
||
"""Generiert Markdown-Report."""
|
||
|
||
lines = [
|
||
"# GWÖ-Wahlprüfsteine Auswertung",
|
||
"",
|
||
f"*Stand: {datetime.now().strftime('%d.%m.%Y %H:%M')}*",
|
||
"",
|
||
"## Zusammenfassung",
|
||
"",
|
||
"| Partei | Kandidat:innen | Ø GWÖ | Ø Substanz |",
|
||
"|--------|---------------|-------|------------|",
|
||
]
|
||
|
||
for s in stats:
|
||
lines.append(f"| **{s['partei']}** | {s['kandidaten']} | {s['avg_gwoe']:.1f} | {s['avg_substanz']:.1f}/3 |")
|
||
|
||
lines.extend(["", "---", "", "## Details", ""])
|
||
|
||
for s in stats:
|
||
lines.append(f"### {s['partei']} (Ø {s['avg_gwoe']:.1f})")
|
||
lines.append("")
|
||
|
||
quotes = get_top_quotes(conn, s['partei'])
|
||
if quotes:
|
||
lines.append("**Beste Antworten:**")
|
||
for q in quotes:
|
||
zitat = q['zitat'][:200] + "..." if len(q['zitat']) > 200 else q['zitat']
|
||
lines.append(f"> *\"{zitat}\"*")
|
||
lines.append(f"> — {q['name']} ({q['kommune']}), GWÖ: {q['gwoe_score']:.1f}")
|
||
lines.append("")
|
||
|
||
lines.append("---")
|
||
lines.append("")
|
||
|
||
return "\n".join(lines)
|
||
|
||
def main():
|
||
import argparse
|
||
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument('--db', default='wahlpruefsteine.db')
|
||
parser.add_argument('--output', default='report.md')
|
||
|
||
args = parser.parse_args()
|
||
|
||
conn = sqlite3.connect(args.db)
|
||
conn.row_factory = sqlite3.Row
|
||
|
||
stats = get_partei_stats(conn)
|
||
report = generate_report(stats, conn)
|
||
|
||
Path(args.output).write_text(report)
|
||
print(f"Report erstellt: {args.output}")
|
||
|
||
conn.close()
|
||
|
||
if __name__ == '__main__':
|
||
main()
|
||
```
|
||
|
||
---
|
||
|
||
## Ausführung
|
||
|
||
### Vollständiger Workflow
|
||
|
||
```bash
|
||
# 1. Daten laden
|
||
python3 scraper.py --url "https://example.org/wahlpruefsteine"
|
||
|
||
# 2. KI-Bewertung (dauert je nach Anzahl)
|
||
python3 analyzer.py
|
||
|
||
# 3. Report generieren
|
||
python3 aggregator.py --output auswertung.md
|
||
```
|
||
|
||
### Status prüfen
|
||
|
||
```bash
|
||
sqlite3 wahlpruefsteine.db "
|
||
SELECT
|
||
(SELECT COUNT(*) FROM kandidaten) as kandidaten,
|
||
(SELECT COUNT(*) FROM antworten_raw) as antworten,
|
||
(SELECT COUNT(*) FROM bewertungen) as bewertet;
|
||
"
|
||
```
|
||
|
||
---
|
||
|
||
## Anpassungen für andere Datenquellen
|
||
|
||
### Andere HTML-Struktur
|
||
|
||
Passe `parse_html()` in `scraper.py` an:
|
||
|
||
```python
|
||
def parse_html(html_content: str) -> list[dict]:
|
||
soup = BeautifulSoup(html_content, 'html.parser')
|
||
|
||
# DEINE LOGIK HIER
|
||
# z.B. für andere Tabellenstruktur, div-basierte Layouts, etc.
|
||
|
||
return kandidaten
|
||
```
|
||
|
||
### Andere Fragen
|
||
|
||
Passe die `INSERT`-Statements in `schema.sql` an:
|
||
|
||
```sql
|
||
INSERT OR IGNORE INTO fragen (nummer, kurztext, volltext) VALUES
|
||
(1, 'Deine Frage 1', 'Vollständiger Text...'),
|
||
(2, 'Deine Frage 2', 'Vollständiger Text...'),
|
||
-- usw.
|
||
```
|
||
|
||
### Andere Parteien/Region
|
||
|
||
Passe `PARTEI_MAPPING` in `scraper.py` an:
|
||
|
||
```python
|
||
PARTEI_MAPPING = {
|
||
r'^cdu$': 'CDU', # statt CSU für andere Bundesländer
|
||
# deine Parteien...
|
||
}
|
||
```
|
||
|
||
### Andere LLM-Anbieter
|
||
|
||
Passe `get_api_client()` in `analyzer.py` an:
|
||
|
||
```python
|
||
# Für Anthropic Claude:
|
||
from anthropic import Anthropic
|
||
client = Anthropic(api_key=os.environ['ANTHROPIC_API_KEY'])
|
||
|
||
# Für lokales Ollama:
|
||
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
|
||
```
|
||
|
||
---
|
||
|
||
## Kosten-Übersicht
|
||
|
||
| Anbieter | Modell | ~Kosten/100 Antworten |
|
||
|----------|--------|----------------------|
|
||
| DashScope | qwen-plus | $0.06 |
|
||
| DashScope | qwen-turbo | $0.02 |
|
||
| OpenAI | gpt-4o-mini | $0.30 |
|
||
| Anthropic | claude-3-haiku | $0.20 |
|
||
| Ollama | lokal | $0.00 |
|
||
|
||
---
|
||
|
||
## Tipps
|
||
|
||
1. **Erst testen:** Starte mit `--limit 5` beim Analyzer
|
||
2. **HTML speichern:** `curl URL > data.html` für Offline-Entwicklung
|
||
3. **Logs prüfen:** Bei Fehlern einzelne Antworten manuell checken
|
||
4. **Schwellen anpassen:** Die GWÖ-Schwellen (5 für "empfohlen") ggf. senken
|
||
|
||
---
|
||
|
||
## Lizenz
|
||
|
||
MIT — Frei verwendbar, auch kommerziell.
|
||
|
||
---
|
||
|
||
*Anleitung erstellt: 29.03.2026*
|
||
*Fragen? → GWÖ-Community oder ECOnGOOD*
|