This repository contains a comprehensive Eurojackpot lottery analysis and prediction system including: - Historical data analysis and processing - ML-based prediction models - Automated weekly tip generation - Position and range analysis tools - Notification system for results 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
557 lines
17 KiB
Python
557 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Eurojackpot API Data Updater
|
|
|
|
Lädt Eurojackpot-Ziehungen von verschiedenen APIs:
|
|
1. lottoAPI (https://lottoapi.herokuapp.com)
|
|
2. Lottoland API
|
|
3. Sazka.cz API (Fallback)
|
|
|
|
Features:
|
|
- Automatisches Laden von APIs
|
|
- Fallback auf andere APIs wenn primäre fehlschlägt
|
|
- Duplikate-Vermeidung
|
|
- Automatisches Backup
|
|
"""
|
|
|
|
import requests
|
|
import pandas as pd
|
|
from datetime import datetime
|
|
import shutil
|
|
import os
|
|
from typing import List, Dict, Optional
|
|
import time
|
|
|
|
|
|
class EurojackpotAPIUpdater:
|
|
"""Updater für Eurojackpot über APIs."""
|
|
|
|
def __init__(self, data_file: str):
|
|
self.data_file = data_file
|
|
self.backup_file = None
|
|
self.df_existing = None
|
|
|
|
# API-Endpoints
|
|
self.apis = {
|
|
'lottoapi': {
|
|
'url': 'https://lottoapi.herokuapp.com/eurojackpot-results/100',
|
|
'name': 'lottoAPI',
|
|
'parser': self._parse_lottoapi
|
|
},
|
|
'lottoland': {
|
|
'url': 'https://media.lottoland.com/api/drawings/euroJackpot',
|
|
'name': 'Lottoland API',
|
|
'parser': self._parse_lottoland
|
|
},
|
|
'sazka': {
|
|
'url': 'https://www.sazka.cz/api/draw-info/past-draws/eurojackpot',
|
|
'name': 'Sazka.cz API',
|
|
'parser': self._parse_sazka
|
|
}
|
|
}
|
|
|
|
print("🔄 EUROJACKPOT API UPDATER")
|
|
print("=" * 70)
|
|
print(f"📁 Datei: {os.path.basename(data_file)}")
|
|
print("=" * 70)
|
|
|
|
def load_existing_data(self) -> bool:
|
|
"""Lädt existierende Daten."""
|
|
print("\n📂 Lade existierende Daten...")
|
|
|
|
try:
|
|
if not os.path.exists(self.data_file):
|
|
print(f" ⚠️ Datei existiert nicht - erstelle neue")
|
|
self.df_existing = pd.DataFrame(
|
|
columns=['datum', 'Z1', 'Z2', 'Z3', 'Z4', 'Z5', 'SZ1', 'SZ2']
|
|
)
|
|
return True
|
|
|
|
self.df_existing = pd.read_csv(self.data_file, sep=';')
|
|
|
|
# Konvertiere Datum
|
|
if 'datum' in self.df_existing.columns:
|
|
self.df_existing['datum'] = pd.to_datetime(
|
|
self.df_existing['datum'],
|
|
format='%Y-%m-%d',
|
|
errors='coerce'
|
|
)
|
|
|
|
print(f" ✅ {len(self.df_existing)} Ziehungen geladen")
|
|
|
|
if len(self.df_existing) > 0:
|
|
latest = self.df_existing['datum'].max()
|
|
oldest = self.df_existing['datum'].min()
|
|
print(f" 📅 Zeitraum: {oldest.strftime('%Y-%m-%d')} bis {latest.strftime('%Y-%m-%d')}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f" ❌ Fehler: {e}")
|
|
return False
|
|
|
|
def create_backup(self) -> bool:
|
|
"""Erstellt Backup."""
|
|
if not os.path.exists(self.data_file):
|
|
return True
|
|
|
|
try:
|
|
print("\n💾 Erstelle Backup...")
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
backup_dir = os.path.join(os.path.dirname(self.data_file), "backups")
|
|
os.makedirs(backup_dir, exist_ok=True)
|
|
|
|
filename = os.path.basename(self.data_file)
|
|
self.backup_file = os.path.join(backup_dir, f"{filename}.backup_{timestamp}")
|
|
|
|
shutil.copy2(self.data_file, self.backup_file)
|
|
print(f" ✅ Backup: {os.path.basename(self.backup_file)}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f" ⚠️ Backup-Fehler: {e}")
|
|
return False
|
|
|
|
def fetch_from_api(self, api_name: str) -> List[Dict]:
|
|
"""
|
|
Holt Daten von spezifischer API.
|
|
|
|
Args:
|
|
api_name: Name der API ('lottoapi', 'lottoland', 'sazka')
|
|
|
|
Returns:
|
|
Liste von Ziehungen
|
|
"""
|
|
if api_name not in self.apis:
|
|
print(f" ❌ Unbekannte API: {api_name}")
|
|
return []
|
|
|
|
api = self.apis[api_name]
|
|
print(f"\n🌐 Versuche {api['name']}...")
|
|
print(f" URL: {api['url']}")
|
|
|
|
try:
|
|
headers = {
|
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
|
}
|
|
|
|
response = requests.get(api['url'], headers=headers, timeout=15)
|
|
response.raise_for_status()
|
|
|
|
print(f" ✅ Antwort erhalten ({len(response.content)} bytes)")
|
|
|
|
# Parse mit spezifischem Parser
|
|
draws = api['parser'](response.json())
|
|
|
|
if draws:
|
|
print(f" ✅ {len(draws)} Ziehungen extrahiert")
|
|
else:
|
|
print(f" ⚠️ Keine Ziehungen extrahiert")
|
|
|
|
return draws
|
|
|
|
except requests.RequestException as e:
|
|
print(f" ❌ Netzwerkfehler: {e}")
|
|
return []
|
|
except Exception as e:
|
|
print(f" ❌ Fehler: {e}")
|
|
return []
|
|
|
|
def _parse_lottoapi(self, data: List[Dict]) -> List[Dict]:
|
|
"""
|
|
Parst lottoAPI Response.
|
|
|
|
Expected format:
|
|
[
|
|
{
|
|
"date": "2025-01-17",
|
|
"numbers": [3, 9, 12, 24, 39],
|
|
"euroNumbers": [5, 10]
|
|
},
|
|
...
|
|
]
|
|
"""
|
|
draws = []
|
|
|
|
try:
|
|
for item in data:
|
|
# Datum
|
|
date_str = item.get('date')
|
|
date_obj = datetime.strptime(date_str, '%Y-%m-%d')
|
|
|
|
# Hauptzahlen
|
|
numbers = sorted(item.get('numbers', []))
|
|
if len(numbers) != 5:
|
|
continue
|
|
|
|
# Eurozahlen
|
|
euro_numbers = sorted(item.get('euroNumbers', []))
|
|
if len(euro_numbers) != 2:
|
|
continue
|
|
|
|
draw = {
|
|
'datum': date_obj,
|
|
'Z1': numbers[0],
|
|
'Z2': numbers[1],
|
|
'Z3': numbers[2],
|
|
'Z4': numbers[3],
|
|
'Z5': numbers[4],
|
|
'SZ1': euro_numbers[0],
|
|
'SZ2': euro_numbers[1]
|
|
}
|
|
|
|
if self._validate_draw(draw):
|
|
draws.append(draw)
|
|
|
|
except Exception as e:
|
|
print(f" ⚠️ Parse-Fehler: {e}")
|
|
|
|
return draws
|
|
|
|
def _parse_lottoland(self, data: Dict) -> List[Dict]:
|
|
"""
|
|
Parst Lottoland API Response.
|
|
|
|
Expected format: {'last': {...}, 'next': {...}}
|
|
"""
|
|
draws = []
|
|
|
|
try:
|
|
# Lottoland gibt nur die letzte Ziehung zurück
|
|
if 'last' in data:
|
|
item = data['last']
|
|
|
|
# Datum extrahieren aus verschachteltem 'date' Objekt
|
|
date_info = item.get('date', {})
|
|
day = date_info.get('day')
|
|
month = date_info.get('month')
|
|
year = date_info.get('year')
|
|
|
|
if day and month and year:
|
|
date_obj = datetime(year, month, day)
|
|
else:
|
|
return draws
|
|
|
|
# Hauptzahlen
|
|
numbers = sorted(item.get('numbers', []))
|
|
if len(numbers) != 5:
|
|
return draws
|
|
|
|
# Eurozahlen
|
|
euro_numbers = sorted(item.get('euroNumbers', []))
|
|
if len(euro_numbers) != 2:
|
|
return draws
|
|
|
|
draw = {
|
|
'datum': date_obj,
|
|
'Z1': numbers[0],
|
|
'Z2': numbers[1],
|
|
'Z3': numbers[2],
|
|
'Z4': numbers[3],
|
|
'Z5': numbers[4],
|
|
'SZ1': euro_numbers[0],
|
|
'SZ2': euro_numbers[1]
|
|
}
|
|
|
|
if self._validate_draw(draw):
|
|
draws.append(draw)
|
|
print(f" ✅ Ziehung vom {date_obj.strftime('%Y-%m-%d')} extrahiert")
|
|
|
|
except Exception as e:
|
|
print(f" ⚠️ Parse-Fehler: {e}")
|
|
|
|
return draws
|
|
|
|
def _parse_sazka(self, data: Dict) -> List[Dict]:
|
|
"""
|
|
Parst Sazka.cz API Response.
|
|
|
|
Sazka hat zwei Steps:
|
|
1. Liste von Draw IDs holen
|
|
2. Für jede Draw ID Details holen
|
|
"""
|
|
draws = []
|
|
|
|
try:
|
|
# Erste Response enthält Liste von Ziehungen
|
|
if isinstance(data, list):
|
|
past_draws = data
|
|
elif isinstance(data, dict) and 'draws' in data:
|
|
past_draws = data['draws']
|
|
else:
|
|
return draws
|
|
|
|
print(f" 📊 {len(past_draws)} Ziehungen gefunden, hole Details...")
|
|
|
|
# Begrenzen auf letzte 10 Ziehungen (um Requests zu sparen)
|
|
for i, draw_item in enumerate(past_draws[:10]):
|
|
draw_id = draw_item.get('id') # Nicht 'drawId' sondern 'id'!
|
|
if not draw_id:
|
|
continue
|
|
|
|
# Hole Details für diese Ziehung
|
|
detail_url = f"https://www.sazka.cz/api/draw-info/draws/universal/eurojackpot/{draw_id}"
|
|
|
|
try:
|
|
response = requests.get(detail_url, timeout=10)
|
|
detail_data = response.json()
|
|
|
|
# Parse Detail
|
|
date_str = detail_data.get('date') # Nicht 'drawDate' sondern 'date'
|
|
date_obj = datetime.strptime(date_str, '%Y-%m-%d')
|
|
|
|
# Zahlen sind direkt in 'numbers' und 'euroNumbers'
|
|
numbers = sorted(detail_data.get('numbers', []))
|
|
euro_numbers = sorted(detail_data.get('euroNumbers', []))
|
|
|
|
if len(numbers) == 5 and len(euro_numbers) == 2:
|
|
draw = {
|
|
'datum': date_obj,
|
|
'Z1': numbers[0],
|
|
'Z2': numbers[1],
|
|
'Z3': numbers[2],
|
|
'Z4': numbers[3],
|
|
'Z5': numbers[4],
|
|
'SZ1': euro_numbers[0],
|
|
'SZ2': euro_numbers[1]
|
|
}
|
|
|
|
if self._validate_draw(draw):
|
|
draws.append(draw)
|
|
|
|
if (i + 1) % 5 == 0:
|
|
print(f" ⏳ {i + 1}/10 Details geladen...")
|
|
|
|
time.sleep(0.3) # Rate limiting
|
|
|
|
except Exception as e:
|
|
print(f" ⚠️ Fehler bei Draw ID {draw_id}: {e}")
|
|
continue
|
|
|
|
print(f" ✅ {len(draws)} gültige Ziehungen extrahiert")
|
|
|
|
except Exception as e:
|
|
print(f" ⚠️ Parse-Fehler: {e}")
|
|
|
|
return draws
|
|
|
|
def _validate_draw(self, draw: Dict) -> bool:
|
|
"""Validiert eine Ziehung."""
|
|
try:
|
|
required_fields = ['datum', 'Z1', 'Z2', 'Z3', 'Z4', 'Z5', 'SZ1', 'SZ2']
|
|
if not all(field in draw for field in required_fields):
|
|
return False
|
|
|
|
main_numbers = [draw[f'Z{i}'] for i in range(1, 6)]
|
|
if not all(isinstance(n, int) and 1 <= n <= 50 for n in main_numbers):
|
|
return False
|
|
|
|
if len(set(main_numbers)) != 5:
|
|
return False
|
|
|
|
euro_numbers = [draw['SZ1'], draw['SZ2']]
|
|
if not all(isinstance(n, int) and 1 <= n <= 12 for n in euro_numbers):
|
|
return False
|
|
|
|
if len(set(euro_numbers)) != 2:
|
|
return False
|
|
|
|
if not isinstance(draw['datum'], datetime):
|
|
return False
|
|
|
|
return True
|
|
|
|
except Exception:
|
|
return False
|
|
|
|
def fetch_from_all_apis(self) -> List[Dict]:
|
|
"""
|
|
Versucht alle APIs nacheinander.
|
|
|
|
Returns:
|
|
Liste von Ziehungen
|
|
"""
|
|
print("\n🔍 SUCHE NACH DATEN VON APIs")
|
|
print("=" * 70)
|
|
|
|
all_draws = []
|
|
|
|
# Versuche alle APIs
|
|
for api_name in ['lottoapi', 'lottoland', 'sazka']:
|
|
draws = self.fetch_from_api(api_name)
|
|
|
|
if draws:
|
|
all_draws.extend(draws)
|
|
print(f" ✅ {len(draws)} Ziehungen von {self.apis[api_name]['name']}")
|
|
break # Erste erfolgreiche API nutzen
|
|
else:
|
|
print(f" ⏭️ Weiter zur nächsten API...")
|
|
|
|
time.sleep(1) # Pause zwischen APIs
|
|
|
|
if not all_draws:
|
|
print("\n ❌ Keine Daten von APIs erhalten")
|
|
|
|
return all_draws
|
|
|
|
def merge_with_existing(self, new_draws: List[Dict]) -> pd.DataFrame:
|
|
"""Merged neue Ziehungen mit existierenden Daten."""
|
|
print(f"\n🔀 Merge mit existierenden Daten...")
|
|
|
|
if not new_draws:
|
|
print(" ⚠️ Keine neuen Ziehungen zum Mergen")
|
|
return self.df_existing
|
|
|
|
df_new = pd.DataFrame(new_draws)
|
|
|
|
if self.df_existing is None or len(self.df_existing) == 0:
|
|
df_combined = df_new
|
|
print(f" ✅ Neue Datei erstellt mit {len(df_combined)} Ziehungen")
|
|
else:
|
|
df_combined = pd.concat([self.df_existing, df_new], ignore_index=True)
|
|
|
|
before_dedup = len(df_combined)
|
|
df_combined = df_combined.drop_duplicates(subset=['datum'], keep='first')
|
|
after_dedup = len(df_combined)
|
|
|
|
duplicates = before_dedup - after_dedup
|
|
if duplicates > 0:
|
|
print(f" 🗑️ {duplicates} Duplikat(e) entfernt")
|
|
|
|
df_combined = df_combined.sort_values('datum', ascending=True)
|
|
df_combined = df_combined.reset_index(drop=True)
|
|
|
|
new_entries = len(df_combined) - (len(self.df_existing) if self.df_existing is not None else 0)
|
|
|
|
print(f" ✅ Merge abgeschlossen:")
|
|
print(f" Vorher: {len(self.df_existing) if self.df_existing is not None else 0} Ziehungen")
|
|
print(f" Neue: {new_entries} Ziehungen")
|
|
print(f" Gesamt: {len(df_combined)} Ziehungen")
|
|
|
|
return df_combined
|
|
|
|
def save_data(self, df: pd.DataFrame) -> bool:
|
|
"""Speichert Daten."""
|
|
try:
|
|
print(f"\n💾 Speichere Daten...")
|
|
|
|
df_export = df.copy()
|
|
df_export['datum'] = df_export['datum'].dt.strftime('%Y-%m-%d')
|
|
|
|
column_order = ['datum', 'Z1', 'Z2', 'Z3', 'Z4', 'Z5', 'SZ1', 'SZ2']
|
|
df_export = df_export[column_order]
|
|
|
|
df_export.to_csv(self.data_file, sep=';', index=False)
|
|
|
|
print(f" ✅ Gespeichert: {self.data_file}")
|
|
print(f" 📊 {len(df)} Ziehungen total")
|
|
|
|
if len(df) > 0:
|
|
latest = df['datum'].max()
|
|
oldest = df['datum'].min()
|
|
print(f" 📅 Zeitraum: {oldest.strftime('%Y-%m-%d')} bis {latest.strftime('%Y-%m-%d')}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f" ❌ Speicherfehler: {e}")
|
|
|
|
if self.backup_file and os.path.exists(self.backup_file):
|
|
print(" 🔄 Stelle Backup wieder her...")
|
|
shutil.copy2(self.backup_file, self.data_file)
|
|
print(" ✅ Backup wiederhergestellt")
|
|
|
|
return False
|
|
|
|
def update(self, api_name: str = 'all', create_backup: bool = True) -> bool:
|
|
"""
|
|
Führt komplettes Update durch.
|
|
|
|
Args:
|
|
api_name: 'all', 'lottoapi', 'lottoland', 'sazka'
|
|
create_backup: Backup erstellen
|
|
|
|
Returns:
|
|
True bei Erfolg
|
|
"""
|
|
print("\n🚀 STARTE UPDATE")
|
|
print("=" * 70)
|
|
|
|
if not self.load_existing_data():
|
|
return False
|
|
|
|
if create_backup:
|
|
self.create_backup()
|
|
|
|
if api_name == 'all':
|
|
new_draws = self.fetch_from_all_apis()
|
|
else:
|
|
new_draws = self.fetch_from_api(api_name)
|
|
|
|
if not new_draws:
|
|
print("\n❌ Keine Daten von APIs geladen")
|
|
return False
|
|
|
|
df_updated = self.merge_with_existing(new_draws)
|
|
success = self.save_data(df_updated)
|
|
|
|
print("\n" + "=" * 70)
|
|
if success:
|
|
print("✅ UPDATE ERFOLGREICH ABGESCHLOSSEN")
|
|
else:
|
|
print("❌ UPDATE FEHLGESCHLAGEN")
|
|
print("=" * 70)
|
|
|
|
return success
|
|
|
|
|
|
def main():
|
|
"""Hauptfunktion."""
|
|
import sys
|
|
|
|
print()
|
|
print("=" * 70)
|
|
print(" EUROJACKPOT API UPDATER")
|
|
print(" Unterstützt: lottoAPI, Lottoland, Sazka.cz")
|
|
print("=" * 70)
|
|
print()
|
|
|
|
default_file = "/Users/sebastianfrohlich/Library/Mobile Documents/com~apple~CloudDocs/Jupyter Notebooks/Eurojackpot/data/AlleEurojackpotzahlen.csv"
|
|
|
|
if len(sys.argv) > 1:
|
|
data_file = sys.argv[1]
|
|
api_name = sys.argv[2] if len(sys.argv) > 2 else 'all'
|
|
else:
|
|
print(f"Standard-Datei: {default_file}")
|
|
use_default = input("\nStandard verwenden? (j/n): ").strip().lower()
|
|
|
|
if use_default in ['j', 'ja', 'y', 'yes', '']:
|
|
data_file = default_file
|
|
else:
|
|
data_file = input("Pfad zur Datei: ").strip()
|
|
|
|
print("\n🌐 API WÄHLEN")
|
|
print("=" * 60)
|
|
print("1. Alle APIs versuchen (empfohlen)")
|
|
print("2. lottoAPI")
|
|
print("3. Lottoland API")
|
|
print("4. Sazka.cz API")
|
|
print()
|
|
|
|
api_choice = input("Wahl (1-4): ").strip()
|
|
api_map = {'1': 'all', '2': 'lottoapi', '3': 'lottoland', '4': 'sazka'}
|
|
api_name = api_map.get(api_choice, 'all')
|
|
|
|
print()
|
|
|
|
updater = EurojackpotAPIUpdater(data_file)
|
|
success = updater.update(api_name=api_name)
|
|
|
|
sys.exit(0 if success else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|