Initial commit: Lotto number generator project
This project includes multiple AI/ML-based lottery number generators for German Lotto 6aus49, including pattern analysis, weighted predictions, and hybrid approaches. Features automated weekly tip generation, performance tracking, and Telegram bot integration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
f6106b8333
48 files changed
+25168
No files matched your search
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Model Evaluation & Reporting Module
|
||||
Für umfassende ML-Model-Evaluation
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
try:
|
||||
from sklearn.model_selection import cross_val_score
|
||||
from sklearn.metrics import r2_score, mean_absolute_error, mean_squared_error
|
||||
SKLEARN_AVAILABLE = True
|
||||
except ImportError:
|
||||
SKLEARN_AVAILABLE = False
|
||||
|
||||
|
||||
class ModelEvaluator:
|
||||
"""Evaluiert und dokumentiert ML-Modelle."""
|
||||
|
||||
def __init__(self, cache_path=None):
|
||||
self.cache_path = cache_path
|
||||
self.evaluation_results = {}
|
||||
self.report_file = os.path.join(cache_path, 'model_evaluation.json') if cache_path else None
|
||||
|
||||
def evaluate_model(self, model, X_train, X_test, y_train, y_test, model_name, number):
|
||||
"""
|
||||
Umfassende Model-Evaluation.
|
||||
|
||||
Returns:
|
||||
dict with metrics
|
||||
"""
|
||||
if not SKLEARN_AVAILABLE:
|
||||
return {'error': 'scikit-learn not available'}
|
||||
|
||||
results = {
|
||||
'model_name': model_name,
|
||||
'number': number,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'data_size': {
|
||||
'train': len(X_train),
|
||||
'test': len(X_test)
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
# 1. Training Score
|
||||
y_train_pred = model.predict(X_train)
|
||||
results['train_r2'] = r2_score(y_train, y_train_pred)
|
||||
results['train_mae'] = mean_absolute_error(y_train, y_train_pred)
|
||||
results['train_rmse'] = np.sqrt(mean_squared_error(y_train, y_train_pred))
|
||||
|
||||
# 2. Test Score
|
||||
y_test_pred = model.predict(X_test)
|
||||
results['test_r2'] = r2_score(y_test, y_test_pred)
|
||||
results['test_mae'] = mean_absolute_error(y_test, y_test_pred)
|
||||
results['test_rmse'] = np.sqrt(mean_squared_error(y_test, y_test_pred))
|
||||
|
||||
# 3. Overfit Detection
|
||||
results['overfitting'] = results['train_r2'] - results['test_r2']
|
||||
results['is_overfit'] = results['overfitting'] > 0.2
|
||||
|
||||
# 4. Cross-Validation (3-fold for speed)
|
||||
try:
|
||||
cv_scores = cross_val_score(model, X_train, y_train, cv=3, scoring='r2')
|
||||
results['cv_mean'] = float(np.mean(cv_scores))
|
||||
results['cv_std'] = float(np.std(cv_scores))
|
||||
results['cv_scores'] = [float(s) for s in cv_scores]
|
||||
except Exception as e:
|
||||
results['cv_error'] = str(e)
|
||||
|
||||
# 5. Feature Importance (if available)
|
||||
if hasattr(model, 'feature_importances_'):
|
||||
importances = model.feature_importances_
|
||||
results['top_features'] = {
|
||||
f'feature_{i}': float(imp)
|
||||
for i, imp in enumerate(importances[:10]) # Top 10
|
||||
}
|
||||
results['feature_importance_sum'] = float(np.sum(importances))
|
||||
|
||||
# 6. Prediction Distribution
|
||||
results['pred_distribution'] = {
|
||||
'min': float(np.min(y_test_pred)),
|
||||
'max': float(np.max(y_test_pred)),
|
||||
'mean': float(np.mean(y_test_pred)),
|
||||
'std': float(np.std(y_test_pred))
|
||||
}
|
||||
|
||||
# 7. Quality Rating
|
||||
test_r2 = results['test_r2']
|
||||
if test_r2 > 0.7:
|
||||
results['quality'] = 'Excellent'
|
||||
elif test_r2 > 0.5:
|
||||
results['quality'] = 'Good'
|
||||
elif test_r2 > 0.3:
|
||||
results['quality'] = 'Fair'
|
||||
elif test_r2 > 0.1:
|
||||
results['quality'] = 'Poor'
|
||||
else:
|
||||
results['quality'] = 'Very Poor'
|
||||
|
||||
except Exception as e:
|
||||
results['error'] = str(e)
|
||||
|
||||
return results
|
||||
|
||||
def add_evaluation(self, number, model_name, results):
|
||||
"""Fügt Evaluation-Result hinzu."""
|
||||
key = f"{number}_{model_name}"
|
||||
self.evaluation_results[key] = results
|
||||
|
||||
def generate_summary_report(self):
|
||||
"""Generiert Zusammenfassungs-Report."""
|
||||
if not self.evaluation_results:
|
||||
return "Keine Evaluation-Daten vorhanden"
|
||||
|
||||
report = []
|
||||
report.append("=" * 80)
|
||||
report.append("MODEL EVALUATION SUMMARY")
|
||||
report.append("=" * 80)
|
||||
report.append(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
report.append(f"Total Evaluations: {len(self.evaluation_results)}")
|
||||
report.append("")
|
||||
|
||||
# Aggregate stats
|
||||
all_test_r2 = []
|
||||
all_cv_mean = []
|
||||
overfit_count = 0
|
||||
quality_dist = defaultdict(int)
|
||||
|
||||
for key, results in self.evaluation_results.items():
|
||||
if 'test_r2' in results:
|
||||
all_test_r2.append(results['test_r2'])
|
||||
if 'cv_mean' in results:
|
||||
all_cv_mean.append(results['cv_mean'])
|
||||
if results.get('is_overfit'):
|
||||
overfit_count += 1
|
||||
if 'quality' in results:
|
||||
quality_dist[results['quality']] += 1
|
||||
|
||||
# Overall Stats
|
||||
report.append("OVERALL STATISTICS")
|
||||
report.append("-" * 80)
|
||||
if all_test_r2:
|
||||
report.append(f"Test R² Score:")
|
||||
report.append(f" Mean: {np.mean(all_test_r2):.4f}")
|
||||
report.append(f" Median: {np.median(all_test_r2):.4f}")
|
||||
report.append(f" Std: {np.std(all_test_r2):.4f}")
|
||||
report.append(f" Min: {np.min(all_test_r2):.4f}")
|
||||
report.append(f" Max: {np.max(all_test_r2):.4f}")
|
||||
report.append("")
|
||||
|
||||
if all_cv_mean:
|
||||
report.append(f"Cross-Validation R² Score:")
|
||||
report.append(f" Mean: {np.mean(all_cv_mean):.4f}")
|
||||
report.append(f" Std: {np.std(all_cv_mean):.4f}")
|
||||
report.append("")
|
||||
|
||||
report.append(f"Overfitting Detection:")
|
||||
report.append(f" Overfit Models: {overfit_count}/{len(self.evaluation_results)}")
|
||||
report.append("")
|
||||
|
||||
report.append(f"Quality Distribution:")
|
||||
for quality in ['Excellent', 'Good', 'Fair', 'Poor', 'Very Poor']:
|
||||
count = quality_dist.get(quality, 0)
|
||||
pct = (count / len(self.evaluation_results)) * 100 if self.evaluation_results else 0
|
||||
report.append(f" {quality:12}: {count:3} ({pct:5.1f}%)")
|
||||
report.append("")
|
||||
|
||||
# Top 10 Best Models
|
||||
sorted_results = sorted(
|
||||
[(k, v) for k, v in self.evaluation_results.items() if 'test_r2' in v],
|
||||
key=lambda x: x[1]['test_r2'],
|
||||
reverse=True
|
||||
)[:10]
|
||||
|
||||
report.append("TOP 10 MODELS (by Test R²)")
|
||||
report.append("-" * 80)
|
||||
report.append(f"{'Number':<10} {'Model':<20} {'Test R²':<12} {'CV Mean':<12} {'Quality':<15}")
|
||||
report.append("-" * 80)
|
||||
|
||||
for key, results in sorted_results:
|
||||
number = results.get('number', 'N/A')
|
||||
model = results.get('model_name', 'N/A')
|
||||
test_r2 = results.get('test_r2', 0)
|
||||
cv_mean = results.get('cv_mean', 0)
|
||||
quality = results.get('quality', 'N/A')
|
||||
|
||||
report.append(f"{number:<10} {model:<20} {test_r2:<12.4f} {cv_mean:<12.4f} {quality:<15}")
|
||||
|
||||
report.append("")
|
||||
report.append("=" * 80)
|
||||
|
||||
return "\n".join(report)
|
||||
|
||||
def save_evaluation(self):
|
||||
"""Speichert Evaluation persistent."""
|
||||
if not self.report_file:
|
||||
return
|
||||
|
||||
try:
|
||||
os.makedirs(os.path.dirname(self.report_file), exist_ok=True)
|
||||
|
||||
data = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'total_evaluations': len(self.evaluation_results),
|
||||
'results': self.evaluation_results
|
||||
}
|
||||
|
||||
with open(self.report_file, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
print(f" 💾 Evaluation saved: {self.report_file}")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Could not save evaluation: {e}")
|
||||
|
||||
def load_evaluation(self):
|
||||
"""Lädt gespeicherte Evaluation."""
|
||||
if not self.report_file or not os.path.exists(self.report_file):
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self.report_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
self.evaluation_results = data.get('results', {})
|
||||
timestamp = data.get('timestamp', 'Unknown')
|
||||
|
||||
print(f" 📂 Evaluation loaded: {len(self.evaluation_results)} results (from {timestamp[:10]})")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Could not load evaluation: {e}")
|
||||
|
||||
def print_summary(self):
|
||||
"""Druckt Summary auf Console."""
|
||||
summary = self.generate_summary_report()
|
||||
print("\n" + summary)
|
||||
@@ -0,0 +1,340 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Lotto 6aus49 Benachrichtigungs-System
|
||||
|
||||
Unterstützt:
|
||||
- Telegram Bot Notifications
|
||||
- Email Notifications (optional)
|
||||
|
||||
Konfiguration über config/notifications.json
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
class LottoNotifier:
|
||||
"""Benachrichtigungs-System für Lotto 6aus49."""
|
||||
|
||||
def __init__(self, config_path: str = None):
|
||||
"""
|
||||
Initialisiert Notifier.
|
||||
|
||||
Args:
|
||||
config_path: Pfad zur Konfigurationsdatei
|
||||
"""
|
||||
if config_path is None:
|
||||
# Default: config/notifications.json im Projekt-Root
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(os.path.dirname(script_dir))
|
||||
config_path = os.path.join(project_root, 'config', 'notifications.json')
|
||||
|
||||
self.config_path = config_path
|
||||
self.config = self._load_config()
|
||||
|
||||
def _load_config(self) -> dict:
|
||||
"""Lädt Konfiguration."""
|
||||
if not os.path.exists(self.config_path):
|
||||
print(f"⚠️ Konfigurationsdatei nicht gefunden: {self.config_path}")
|
||||
return {
|
||||
"telegram": {"enabled": False},
|
||||
"email": {"enabled": False}
|
||||
}
|
||||
|
||||
try:
|
||||
with open(self.config_path, 'r') as f:
|
||||
config = json.load(f)
|
||||
return config
|
||||
except Exception as e:
|
||||
print(f"⚠️ Fehler beim Laden der Konfiguration: {e}")
|
||||
return {
|
||||
"telegram": {"enabled": False},
|
||||
"email": {"enabled": False}
|
||||
}
|
||||
|
||||
def send_tips_generated(self, tips: List[Dict], timestamp: str, best_tip: Dict):
|
||||
"""
|
||||
Sendet Benachrichtigung über generierte Tipps.
|
||||
|
||||
Args:
|
||||
tips: Liste aller generierten Tipps
|
||||
timestamp: Timestamp der Generierung
|
||||
best_tip: Bester Tipp (höchste Confidence)
|
||||
"""
|
||||
if self.config['telegram']['enabled']:
|
||||
self._send_telegram_tips(tips, timestamp, best_tip)
|
||||
|
||||
if self.config['email']['enabled']:
|
||||
self._send_email_tips(tips, timestamp, best_tip)
|
||||
|
||||
def _send_telegram_tips(self, tips: List[Dict], timestamp: str, best_tip: Dict):
|
||||
"""Sendet Telegram-Nachricht."""
|
||||
try:
|
||||
bot_token = self.config['telegram']['bot_token']
|
||||
chat_id = self.config['telegram']['chat_id']
|
||||
|
||||
# Formatiere Nachricht
|
||||
message = self._format_telegram_message(tips, timestamp, best_tip)
|
||||
|
||||
# Sende über Telegram Bot API
|
||||
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
||||
data = {
|
||||
"chat_id": chat_id,
|
||||
"text": message,
|
||||
"parse_mode": "Markdown"
|
||||
}
|
||||
|
||||
response = requests.post(url, data=data, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
print("✅ Telegram-Benachrichtigung gesendet")
|
||||
else:
|
||||
print(f"⚠️ Telegram-Fehler: {response.status_code}")
|
||||
print(f" Response: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Telegram-Benachrichtigung fehlgeschlagen: {e}")
|
||||
|
||||
def _format_telegram_message(self, tips: List[Dict], timestamp: str, best_tip: Dict) -> str:
|
||||
"""Formatiert Telegram-Nachricht."""
|
||||
# Header
|
||||
message = "🎲 *LOTTO 6AUS49 - NEUE TIPPS GENERIERT*\n"
|
||||
message += "=" * 40 + "\n\n"
|
||||
|
||||
# Timestamp
|
||||
message += f"📅 *Generiert:* {timestamp}\n"
|
||||
message += f"📊 *Anzahl Tipps:* {len(tips)}\n\n"
|
||||
|
||||
# Bester Tipp
|
||||
message += "⭐ *BESTER TIPP:*\n"
|
||||
numbers_str = ' - '.join([f"{n:02d}" for n in best_tip['numbers']])
|
||||
message += f"🎯 Zahlen: `{numbers_str}`\n"
|
||||
message += f"🌟 Superzahl: `{best_tip['superzahl']}`\n"
|
||||
message += f"📈 Confidence: `{best_tip['confidence']:.4f}`\n"
|
||||
message += f"🎨 Strategie: `{best_tip['strategy']}`\n\n"
|
||||
|
||||
# Statistiken
|
||||
avg_conf = sum(t['confidence'] for t in tips) / len(tips)
|
||||
avg_qual = sum(t['quality'] for t in tips) / len(tips)
|
||||
|
||||
message += "📊 *STATISTIKEN:*\n"
|
||||
message += f"🎯 Ø Confidence: `{avg_conf:.4f}`\n"
|
||||
message += f"💎 Ø Quality: `{avg_qual:.4f}`\n\n"
|
||||
|
||||
# Top 3 Tipps
|
||||
message += "🏆 *TOP 3 TIPPS:*\n"
|
||||
sorted_tips = sorted(tips, key=lambda t: t['confidence'], reverse=True)[:3]
|
||||
|
||||
for i, tip in enumerate(sorted_tips, 1):
|
||||
nums = ' - '.join([f"{n:02d}" for n in tip['numbers']])
|
||||
message += f"{i}. `{nums}` + SZ `{tip['superzahl']}` "
|
||||
message += f"({tip['confidence']:.3f})\n"
|
||||
|
||||
message += "\n🍀 *Viel Glück!*"
|
||||
|
||||
return message
|
||||
|
||||
def _send_email_tips(self, tips: List[Dict], timestamp: str, best_tip: Dict):
|
||||
"""Sendet Email-Benachrichtigung."""
|
||||
# TODO: Email-Versand implementieren wenn gewünscht
|
||||
print("⚠️ Email-Benachrichtigung noch nicht implementiert")
|
||||
|
||||
def send_draw_results(self, draw: Dict, evaluation_summary: Dict, best_match: Dict):
|
||||
"""
|
||||
Sendet Benachrichtigung über Ziehungs-Ergebnisse und Tipp-Evaluation.
|
||||
|
||||
Args:
|
||||
draw: Dict mit Ziehungsdaten (date, Z1-Z6, SZ)
|
||||
evaluation_summary: Dict mit avg_main, sz_rate
|
||||
best_match: Dict mit main_matches, sz_match
|
||||
"""
|
||||
if self.config['telegram']['enabled']:
|
||||
self._send_telegram_draw_results(draw, evaluation_summary, best_match)
|
||||
|
||||
if self.config['email']['enabled']:
|
||||
self._send_email_draw_results(draw, evaluation_summary, best_match)
|
||||
|
||||
def _send_telegram_draw_results(self, draw: Dict, evaluation_summary: Dict, best_match: Dict):
|
||||
"""Sendet Telegram-Nachricht mit Ziehungsergebnissen."""
|
||||
try:
|
||||
bot_token = self.config['telegram']['bot_token']
|
||||
chat_id = self.config['telegram']['chat_id']
|
||||
|
||||
# Formatiere Nachricht
|
||||
message = self._format_draw_results_message(draw, evaluation_summary, best_match)
|
||||
|
||||
# Sende über Telegram Bot API
|
||||
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
||||
data = {
|
||||
"chat_id": chat_id,
|
||||
"text": message,
|
||||
"parse_mode": "Markdown"
|
||||
}
|
||||
|
||||
response = requests.post(url, data=data, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
print("✅ Telegram-Benachrichtigung gesendet")
|
||||
else:
|
||||
print(f"⚠️ Telegram-Fehler: {response.status_code}")
|
||||
print(f" Response: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Telegram-Benachrichtigung fehlgeschlagen: {e}")
|
||||
|
||||
def _format_draw_results_message(self, draw: Dict, evaluation_summary: Dict, best_match: Dict) -> str:
|
||||
"""Formatiert Telegram-Nachricht für Ziehungsergebnisse."""
|
||||
# Header
|
||||
message = "🎲 *LOTTO 6AUS49 - ZIEHUNGSERGEBNISSE*\n"
|
||||
message += "=" * 40 + "\n\n"
|
||||
|
||||
# Ziehungsdatum
|
||||
draw_date = draw['date']
|
||||
if hasattr(draw_date, 'strftime'):
|
||||
date_str = draw_date.strftime('%d.%m.%Y')
|
||||
else:
|
||||
date_str = str(draw_date)
|
||||
|
||||
message += f"📅 *Ziehung vom:* {date_str}\n\n"
|
||||
|
||||
# Gezogene Zahlen
|
||||
drawn_numbers = [draw.get(f'Z{i}') for i in range(1, 7)]
|
||||
numbers_str = ' - '.join([f"{n:02d}" for n in drawn_numbers if n is not None])
|
||||
message += f"🎯 *Gewinnzahlen:* `{numbers_str}`\n"
|
||||
|
||||
if draw.get('SZ') is not None:
|
||||
message += f"🌟 *Superzahl:* `{draw['SZ']}`\n\n"
|
||||
else:
|
||||
message += "\n"
|
||||
|
||||
# Evaluation Summary
|
||||
message += "📊 *TIPP-EVALUATION:*\n"
|
||||
if evaluation_summary:
|
||||
avg_main = evaluation_summary.get('avg_main', 0)
|
||||
sz_rate = evaluation_summary.get('sz_rate', 0)
|
||||
message += f"🎯 Ø Treffer Hauptzahlen: `{avg_main:.2f}`\n"
|
||||
message += f"🌟 Superzahl-Rate: `{sz_rate*100:.1f}%`\n\n"
|
||||
|
||||
# Bester Tipp
|
||||
message += "🏆 *BESTER TIPP:*\n"
|
||||
if best_match:
|
||||
main_matches = best_match.get('main_matches', 0)
|
||||
sz_match = best_match.get('sz_match', False)
|
||||
|
||||
# Rating emoji
|
||||
if main_matches == 6 and sz_match:
|
||||
rating = "🏆 JACKPOT!"
|
||||
elif main_matches == 6:
|
||||
rating = "💰 Klasse 2"
|
||||
elif main_matches == 5 and sz_match:
|
||||
rating = "💰 Klasse 3"
|
||||
elif main_matches == 5:
|
||||
rating = "💰 Klasse 4"
|
||||
elif main_matches == 4:
|
||||
rating = "💵 Klasse 6"
|
||||
elif main_matches == 3:
|
||||
rating = "✅ Klasse 8"
|
||||
elif main_matches >= 2:
|
||||
rating = "👍 OK"
|
||||
else:
|
||||
rating = "⚪ Niedrig"
|
||||
|
||||
sz_indicator = "✅" if sz_match else "⚪"
|
||||
message += f"🎯 Treffer Hauptzahlen: `{main_matches}/6`\n"
|
||||
message += f"🌟 Superzahl: {sz_indicator}\n"
|
||||
message += f"📈 Bewertung: {rating}\n"
|
||||
|
||||
message += "\n🔄 *System wurde aktualisiert und trainiert!*"
|
||||
|
||||
return message
|
||||
|
||||
def _send_email_draw_results(self, draw: Dict, evaluation_summary: Dict, best_match: Dict):
|
||||
"""Sendet Email-Benachrichtigung."""
|
||||
# TODO: Email-Versand implementieren wenn gewünscht
|
||||
print("⚠️ Email-Benachrichtigung noch nicht implementiert")
|
||||
|
||||
def send_test_notification(self):
|
||||
"""Sendet Test-Benachrichtigung."""
|
||||
if self.config['telegram']['enabled']:
|
||||
try:
|
||||
bot_token = self.config['telegram']['bot_token']
|
||||
chat_id = self.config['telegram']['chat_id']
|
||||
|
||||
message = "🧪 *LOTTO 6AUS49 TEST*\n\n"
|
||||
message += "✅ Benachrichtigungs-System funktioniert!\n"
|
||||
message += f"📅 {os.popen('date').read().strip()}"
|
||||
|
||||
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
||||
data = {
|
||||
"chat_id": chat_id,
|
||||
"text": message,
|
||||
"parse_mode": "Markdown"
|
||||
}
|
||||
|
||||
response = requests.post(url, data=data, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
print("✅ Test-Benachrichtigung erfolgreich gesendet")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Fehler: {response.status_code}")
|
||||
print(f" Response: {response.text}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Test fehlgeschlagen: {e}")
|
||||
return False
|
||||
else:
|
||||
print("⚠️ Telegram nicht aktiviert in config/notifications.json")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Test-Funktion."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Lotto 6aus49 Notifier Test")
|
||||
parser.add_argument(
|
||||
'--test',
|
||||
action='store_true',
|
||||
help='Sende Test-Benachrichtigung'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--config',
|
||||
type=str,
|
||||
help='Pfad zur Config-Datei'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
notifier = LottoNotifier(config_path=args.config)
|
||||
|
||||
if args.test:
|
||||
notifier.send_test_notification()
|
||||
else:
|
||||
# Beispiel-Tipps
|
||||
example_tips = [
|
||||
{
|
||||
'numbers': [7, 14, 21, 28, 35, 42],
|
||||
'superzahl': 3,
|
||||
'confidence': 0.7234,
|
||||
'quality': 0.6891,
|
||||
'strategy': 'PURE-AI'
|
||||
},
|
||||
{
|
||||
'numbers': [2, 11, 19, 27, 36, 45],
|
||||
'superzahl': 7,
|
||||
'confidence': 0.6978,
|
||||
'quality': 0.6543,
|
||||
'strategy': 'HYBRID-OPT'
|
||||
}
|
||||
]
|
||||
|
||||
best = example_tips[0]
|
||||
notifier.send_tips_generated(example_tips, "2024-11-27 16:00", best)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Einfacher Lotto 6aus49 Data Updater
|
||||
|
||||
Manuelle Eingabe oder CSV-Import von neuen Ziehungen.
|
||||
Perfekt als Fallback wenn Web-Scraping oder APIs nicht funktionieren.
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
import shutil
|
||||
import os
|
||||
|
||||
|
||||
def load_existing_data(data_file):
|
||||
"""Lädt existierende Daten."""
|
||||
print("📂 Lade existierende Daten...")
|
||||
|
||||
if not os.path.exists(data_file):
|
||||
print(" ⚠️ Datei existiert nicht - erstelle neue")
|
||||
return pd.DataFrame(columns=['datum', 'Z1', 'Z2', 'Z3', 'Z4', 'Z5', 'Z6', 'SZ'])
|
||||
|
||||
df = pd.read_csv(data_file, sep=';')
|
||||
df['datum'] = pd.to_datetime(df['datum'], format='%Y-%m-%d', errors='coerce')
|
||||
|
||||
print(f" ✅ {len(df)} Ziehungen geladen")
|
||||
if len(df) > 0:
|
||||
print(f" 📅 Neueste: {df['datum'].max().strftime('%Y-%m-%d')}")
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def create_backup(data_file):
|
||||
"""Erstellt Backup."""
|
||||
if not os.path.exists(data_file):
|
||||
return
|
||||
|
||||
print("\n💾 Erstelle Backup...")
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
# Backup im Lotto/data/backups Verzeichnis
|
||||
parent_dir = os.path.dirname(data_file)
|
||||
lotto_dir = os.path.join(parent_dir, "Lotto")
|
||||
backup_dir = os.path.join(lotto_dir, "data", "backups")
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
|
||||
backup_file = os.path.join(backup_dir, f"{os.path.basename(data_file)}.backup_{timestamp}")
|
||||
shutil.copy2(data_file, backup_file)
|
||||
print(f" ✅ Backup: {os.path.basename(backup_file)}")
|
||||
|
||||
|
||||
def manual_input_mode():
|
||||
"""Manuelle Eingabe von Ziehungen."""
|
||||
print("\n⌨️ MANUELLE EINGABE")
|
||||
print("=" * 70)
|
||||
print("Format: YYYY-MM-DD Z1 Z2 Z3 Z4 Z5 Z6 SZ")
|
||||
print("Beispiel: 2025-01-22 7 14 21 28 35 42 3")
|
||||
print("Leer lassen zum Beenden.\n")
|
||||
|
||||
draws = []
|
||||
while True:
|
||||
user_input = input(f"Ziehung {len(draws) + 1}: ").strip()
|
||||
|
||||
if not user_input:
|
||||
break
|
||||
|
||||
try:
|
||||
parts = user_input.split()
|
||||
if len(parts) != 8:
|
||||
print(" ❌ Ungültiges Format. Bitte 8 Werte eingeben.")
|
||||
continue
|
||||
|
||||
date_obj = datetime.strptime(parts[0], '%Y-%m-%d')
|
||||
numbers = [int(parts[i]) for i in range(1, 7)]
|
||||
superzahl = int(parts[7])
|
||||
|
||||
# Validierung
|
||||
if not all(1 <= n <= 49 for n in numbers):
|
||||
print(" ❌ Hauptzahlen müssen zwischen 1 und 49 liegen.")
|
||||
continue
|
||||
|
||||
if not 0 <= superzahl <= 9:
|
||||
print(" ❌ Superzahl muss zwischen 0 und 9 liegen.")
|
||||
continue
|
||||
|
||||
if len(set(numbers)) != 6:
|
||||
print(" ❌ Hauptzahlen müssen eindeutig sein.")
|
||||
continue
|
||||
|
||||
# Sortiere Zahlen
|
||||
numbers_sorted = sorted(numbers)
|
||||
|
||||
draws.append({
|
||||
'datum': date_obj,
|
||||
'Z1': numbers_sorted[0],
|
||||
'Z2': numbers_sorted[1],
|
||||
'Z3': numbers_sorted[2],
|
||||
'Z4': numbers_sorted[3],
|
||||
'Z5': numbers_sorted[4],
|
||||
'Z6': numbers_sorted[5],
|
||||
'SZ': superzahl
|
||||
})
|
||||
|
||||
print(f" ✅ Hinzugefügt: {date_obj.strftime('%Y-%m-%d')} | "
|
||||
f"{'-'.join(map(str, numbers_sorted))} | SZ: {superzahl}")
|
||||
|
||||
except ValueError as e:
|
||||
print(f" ❌ Fehler: {e}")
|
||||
continue
|
||||
|
||||
return draws
|
||||
|
||||
|
||||
def csv_import_mode():
|
||||
"""CSV-Import von Ziehungen."""
|
||||
print("\n📁 CSV-IMPORT")
|
||||
print("=" * 70)
|
||||
print("CSV-Format: datum;Z1;Z2;Z3;Z4;Z5;Z6;SZ")
|
||||
print("Beispiel: 2025-01-22;7;14;21;28;35;42;3\n")
|
||||
|
||||
csv_file = input("Pfad zur CSV-Datei: ").strip()
|
||||
|
||||
if not os.path.exists(csv_file):
|
||||
print(f" ❌ Datei nicht gefunden: {csv_file}")
|
||||
return []
|
||||
|
||||
try:
|
||||
df_import = pd.read_csv(csv_file, sep=';')
|
||||
df_import['datum'] = pd.to_datetime(df_import['datum'], format='%Y-%m-%d', errors='coerce')
|
||||
|
||||
draws = df_import.to_dict('records')
|
||||
print(f" ✅ {len(draws)} Ziehungen aus CSV geladen")
|
||||
|
||||
# Zeige erste 3
|
||||
for i, draw in enumerate(draws[:3]):
|
||||
nums = f"{draw['Z1']}-{draw['Z2']}-{draw['Z3']}-{draw['Z4']}-{draw['Z5']}-{draw['Z6']}"
|
||||
print(f" {i+1}. {draw['datum'].strftime('%Y-%m-%d')} | {nums} | SZ: {draw['SZ']}")
|
||||
|
||||
if len(draws) > 3:
|
||||
print(f" ... und {len(draws) - 3} weitere")
|
||||
|
||||
return draws
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Fehler beim Laden: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def merge_and_save(df_existing, new_draws, data_file):
|
||||
"""Merged und speichert Daten."""
|
||||
if not new_draws:
|
||||
print("\n⚠️ Keine neuen Ziehungen zum Hinzufügen")
|
||||
return False
|
||||
|
||||
print(f"\n🔀 Merge {len(new_draws)} neue Ziehung(en)...")
|
||||
|
||||
df_new = pd.DataFrame(new_draws)
|
||||
df_combined = pd.concat([df_existing, df_new], ignore_index=True)
|
||||
|
||||
# Duplikate entfernen
|
||||
before = len(df_combined)
|
||||
df_combined = df_combined.drop_duplicates(subset=['datum'], keep='first')
|
||||
after = len(df_combined)
|
||||
|
||||
if before - after > 0:
|
||||
print(f" 🗑️ {before - after} Duplikat(e) entfernt")
|
||||
|
||||
# Sortieren
|
||||
df_combined = df_combined.sort_values('datum', ascending=True).reset_index(drop=True)
|
||||
|
||||
# Speichern
|
||||
print(f"\n💾 Speichere {len(df_combined)} Ziehungen...")
|
||||
|
||||
df_export = df_combined.copy()
|
||||
df_export['datum'] = df_export['datum'].dt.strftime('%Y-%m-%d')
|
||||
|
||||
df_export.to_csv(data_file, sep=';', index=False)
|
||||
|
||||
print(f" ✅ Gespeichert: {data_file}")
|
||||
print(f" 📊 Vorher: {len(df_existing)} | Neu: {len(df_combined) - len(df_existing)} | Gesamt: {len(df_combined)}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
"""Hauptfunktion."""
|
||||
print()
|
||||
print("=" * 70)
|
||||
print(" LOTTO 6AUS49 SIMPLE UPDATER")
|
||||
print(" Manuelle Eingabe oder CSV-Import")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
# Datei-Pfad
|
||||
default_file = "/Users/sebastianfrohlich/Library/Mobile Documents/com~apple~CloudDocs/Jupyter Notebooks/Lotto/data/AlleLottozahlen.csv"
|
||||
|
||||
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()
|
||||
|
||||
# Lade existierende Daten
|
||||
df_existing = load_existing_data(data_file)
|
||||
|
||||
# Backup
|
||||
create_backup(data_file)
|
||||
|
||||
# Eingabe-Modus wählen
|
||||
print("\n📝 EINGABE-MODUS")
|
||||
print("=" * 70)
|
||||
print("1. Manuelle Eingabe (einzelne Ziehungen)")
|
||||
print("2. CSV-Import (mehrere Ziehungen)")
|
||||
print()
|
||||
|
||||
mode = input("Wahl (1/2): ").strip()
|
||||
|
||||
if mode == '2':
|
||||
new_draws = csv_import_mode()
|
||||
else:
|
||||
new_draws = manual_input_mode()
|
||||
|
||||
# Merge und speichern
|
||||
success = merge_and_save(df_existing, new_draws, data_file)
|
||||
|
||||
print()
|
||||
print("=" * 70)
|
||||
if success:
|
||||
print("✅ UPDATE ERFOLGREICH")
|
||||
else:
|
||||
print("⚠️ KEINE ÄNDERUNGEN")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,496 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Lotto 6aus49 API Data Updater
|
||||
|
||||
Lädt Lotto-Ziehungen von verschiedenen APIs:
|
||||
1. lottoAPI (https://lottoapi.herokuapp.com)
|
||||
2. Lotto.de API
|
||||
3. Fallback Quellen
|
||||
|
||||
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 LottoAPIUpdater:
|
||||
"""Updater für Lotto 6aus49 ü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 = {
|
||||
'github': {
|
||||
'url': 'https://johannesfriedrich.github.io/LottoNumberArchive/Lottonumbers_tidy_complete.json',
|
||||
'name': 'GitHub Lotto Archive',
|
||||
'parser': self._parse_github_archive
|
||||
},
|
||||
'lottoapi': {
|
||||
'url': 'https://lottoapi.herokuapp.com/lotto/6aus49/100',
|
||||
'name': 'lottoAPI (Backup)',
|
||||
'parser': self._parse_lottoapi
|
||||
}
|
||||
}
|
||||
|
||||
print("🔄 LOTTO 6AUS49 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', 'Z6', 'SZ']
|
||||
)
|
||||
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), "data", "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')
|
||||
|
||||
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_github_archive(self, data: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
Parst GitHub Lotto Archive Response.
|
||||
|
||||
Expected format:
|
||||
[
|
||||
{
|
||||
"id": 4963,
|
||||
"date": "26.11.2025",
|
||||
"variable": "Lottozahl",
|
||||
"value": 2
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
draws = []
|
||||
|
||||
try:
|
||||
# Gruppiere nach ID (jede ID = eine Ziehung)
|
||||
draws_by_id = {}
|
||||
|
||||
for entry in data:
|
||||
draw_id = entry.get('id')
|
||||
if draw_id not in draws_by_id:
|
||||
draws_by_id[draw_id] = {
|
||||
'date': entry.get('date'),
|
||||
'numbers': [],
|
||||
'superzahl': None
|
||||
}
|
||||
|
||||
variable = entry.get('variable')
|
||||
value = entry.get('value')
|
||||
|
||||
if variable == 'Lottozahl':
|
||||
draws_by_id[draw_id]['numbers'].append(value)
|
||||
elif variable == 'Superzahl':
|
||||
draws_by_id[draw_id]['superzahl'] = value
|
||||
|
||||
# Konvertiere zu unserem Format
|
||||
for draw_id, draw_data in draws_by_id.items():
|
||||
try:
|
||||
# Parse Datum (Format: DD.MM.YYYY)
|
||||
date_str = draw_data['date']
|
||||
day, month, year = date_str.split('.')
|
||||
date_obj = datetime(int(year), int(month), int(day))
|
||||
|
||||
# Sortiere Zahlen
|
||||
numbers = sorted(draw_data['numbers'])
|
||||
|
||||
# Validierung
|
||||
if len(numbers) != 6:
|
||||
continue
|
||||
|
||||
superzahl = draw_data['superzahl']
|
||||
if superzahl is None:
|
||||
continue
|
||||
|
||||
draw = {
|
||||
'datum': date_obj,
|
||||
'Z1': numbers[0],
|
||||
'Z2': numbers[1],
|
||||
'Z3': numbers[2],
|
||||
'Z4': numbers[3],
|
||||
'Z5': numbers[4],
|
||||
'Z6': numbers[5],
|
||||
'SZ': superzahl
|
||||
}
|
||||
|
||||
if self._validate_draw(draw):
|
||||
draws.append(draw)
|
||||
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Parse-Fehler: {e}")
|
||||
|
||||
return draws
|
||||
|
||||
def _parse_lottoapi(self, data: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
Parst lottoAPI Response.
|
||||
|
||||
Expected format:
|
||||
[
|
||||
{
|
||||
"date": "2025-01-22",
|
||||
"numbers": [3, 9, 12, 24, 39, 45],
|
||||
"superzahl": 7
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
draws = []
|
||||
|
||||
try:
|
||||
for item in data:
|
||||
# Datum
|
||||
date_str = item.get('date')
|
||||
if not date_str:
|
||||
continue
|
||||
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d')
|
||||
|
||||
# Hauptzahlen (6 Zahlen)
|
||||
numbers = sorted(item.get('numbers', []))
|
||||
if len(numbers) != 6:
|
||||
continue
|
||||
|
||||
# Superzahl
|
||||
superzahl = item.get('superzahl')
|
||||
if superzahl is None:
|
||||
continue
|
||||
|
||||
draw = {
|
||||
'datum': date_obj,
|
||||
'Z1': numbers[0],
|
||||
'Z2': numbers[1],
|
||||
'Z3': numbers[2],
|
||||
'Z4': numbers[3],
|
||||
'Z5': numbers[4],
|
||||
'Z6': numbers[5],
|
||||
'SZ': superzahl
|
||||
}
|
||||
|
||||
if self._validate_draw(draw):
|
||||
draws.append(draw)
|
||||
|
||||
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', 'Z6', 'SZ']
|
||||
if not all(field in draw for field in required_fields):
|
||||
return False
|
||||
|
||||
# Hauptzahlen (1-49)
|
||||
main_numbers = [draw[f'Z{i}'] for i in range(1, 7)]
|
||||
if not all(isinstance(n, int) and 1 <= n <= 49 for n in main_numbers):
|
||||
return False
|
||||
|
||||
if len(set(main_numbers)) != 6:
|
||||
return False
|
||||
|
||||
# Superzahl (0-9)
|
||||
if not isinstance(draw['SZ'], int) or not 0 <= draw['SZ'] <= 9:
|
||||
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 (GitHub zuerst, da zuverlässig)
|
||||
for api_name in ['github', 'lottoapi']:
|
||||
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")
|
||||
print(" 💡 Tipp: Verwende simple_update.py für manuelle Eingabe")
|
||||
|
||||
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', 'Z6', 'SZ']
|
||||
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' oder 'lottoapi'
|
||||
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")
|
||||
print("💡 Tipp: Verwende simple_update.py für manuelle Eingabe")
|
||||
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(" LOTTO 6AUS49 API UPDATER")
|
||||
print(" Unterstützt: GitHub Lotto Archive, lottoAPI")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
default_file = "/Users/sebastianfrohlich/Library/Mobile Documents/com~apple~CloudDocs/Jupyter Notebooks/Lotto/data/AlleLottozahlen.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()
|
||||
|
||||
api_name = 'all'
|
||||
|
||||
print()
|
||||
|
||||
updater = LottoAPIUpdater(data_file)
|
||||
success = updater.update(api_name=api_name)
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,499 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Lotto 6aus49 Data Updater für lotto.de
|
||||
|
||||
Web-Scraper für https://www.lotto.de/lotto-6aus49/lottozahlen
|
||||
|
||||
Features:
|
||||
- Automatisches Scraping aller historischen Ziehungen
|
||||
- Duplikate-Vermeidung
|
||||
- Automatisches Backup vor Update
|
||||
- Validierung der Daten
|
||||
- Fortschrittsanzeige
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import datetime
|
||||
import shutil
|
||||
import os
|
||||
from typing import List, Dict, Optional
|
||||
import time
|
||||
|
||||
|
||||
class LottoWebUpdater:
|
||||
"""Updater für lotto.de Web-Scraping"""
|
||||
|
||||
def __init__(self, data_file: str):
|
||||
self.data_file = data_file
|
||||
self.url = "https://www.lotto.de/lotto-6aus49/lottozahlen"
|
||||
self.backup_file = None
|
||||
self.df_existing = None
|
||||
|
||||
print("🔄 LOTTO 6AUS49 WEB UPDATER")
|
||||
print("=" * 70)
|
||||
print(f"📁 Datei: {os.path.basename(data_file)}")
|
||||
print(f"🌐 Quelle: {self.url}")
|
||||
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', 'Z6', 'SZ']
|
||||
)
|
||||
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")
|
||||
parent_dir = os.path.dirname(self.data_file)
|
||||
lotto_dir = os.path.join(parent_dir, "Lotto")
|
||||
backup_dir = os.path.join(lotto_dir, "data", "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_draws_from_web(self, max_pages: int = 5) -> List[Dict]:
|
||||
"""
|
||||
Scrapt Ziehungen von lotto.de
|
||||
|
||||
Args:
|
||||
max_pages: Maximale Anzahl Seiten (jede Seite ~20 Ziehungen)
|
||||
|
||||
Returns:
|
||||
Liste von Ziehungen
|
||||
"""
|
||||
print(f"\n🌐 Lade Daten von {self.url}...")
|
||||
print(f" 📄 Lade bis zu {max_pages} Seiten...")
|
||||
|
||||
all_draws = []
|
||||
|
||||
try:
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
||||
}
|
||||
|
||||
for page in range(1, max_pages + 1):
|
||||
url = f"{self.url}?page={page}" if page > 1 else self.url
|
||||
|
||||
print(f"\n 📄 Seite {page}/{max_pages}...")
|
||||
|
||||
response = requests.get(url, headers=headers, timeout=15)
|
||||
response.raise_for_status()
|
||||
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
# Finde Ziehungen auf dieser Seite
|
||||
page_draws = self._extract_draws_from_page(soup, page)
|
||||
|
||||
if not page_draws:
|
||||
print(f" ⚠️ Keine Ziehungen gefunden - Abbruch")
|
||||
break
|
||||
|
||||
all_draws.extend(page_draws)
|
||||
print(f" ✅ {len(page_draws)} Ziehungen extrahiert")
|
||||
|
||||
# Rate limiting
|
||||
if page < max_pages:
|
||||
time.sleep(1)
|
||||
|
||||
print(f"\n ✅ Gesamt: {len(all_draws)} Ziehungen von {page} Seite(n)")
|
||||
return all_draws
|
||||
|
||||
except requests.RequestException as e:
|
||||
print(f" ❌ Netzwerkfehler: {e}")
|
||||
return all_draws # Return was bisher geladen wurde
|
||||
except Exception as e:
|
||||
print(f" ❌ Fehler: {e}")
|
||||
return all_draws
|
||||
|
||||
def _extract_draws_from_page(self, soup: BeautifulSoup, page_num: int) -> List[Dict]:
|
||||
"""
|
||||
Extrahiert Ziehungen von einer Seite.
|
||||
|
||||
Args:
|
||||
soup: BeautifulSoup Objekt
|
||||
page_num: Seitennummer
|
||||
|
||||
Returns:
|
||||
Liste von Ziehungen
|
||||
"""
|
||||
draws = []
|
||||
|
||||
try:
|
||||
# Verschiedene Selektoren versuchen
|
||||
# 1. Versuche mit data-attribute
|
||||
result_divs = soup.find_all('div', class_=lambda x: x and 'result' in x.lower())
|
||||
|
||||
if not result_divs:
|
||||
# 2. Versuche table
|
||||
result_divs = soup.find_all('tr', class_=lambda x: x and ('draw' in x.lower() or 'result' in x.lower()))
|
||||
|
||||
if not result_divs:
|
||||
# 3. Versuche generische Container
|
||||
result_divs = soup.find_all('div', class_=lambda x: x and 'drawing' in x.lower())
|
||||
|
||||
print(f" 🔍 {len(result_divs)} potentielle Ergebnis-Container gefunden")
|
||||
|
||||
for i, elem in enumerate(result_divs):
|
||||
try:
|
||||
# Extrahiere Datum
|
||||
date_elem = elem.find('time') or elem.find(class_=lambda x: x and 'date' in x.lower())
|
||||
if not date_elem:
|
||||
# Versuche direkten Text
|
||||
date_text = self._extract_date_text(elem.get_text())
|
||||
else:
|
||||
date_text = date_elem.get_text().strip()
|
||||
|
||||
date_obj = self._parse_date(date_text)
|
||||
|
||||
# Extrahiere Zahlen - verschiedene Ansätze
|
||||
numbers = self._extract_numbers(elem)
|
||||
|
||||
if len(numbers) < 7: # Mindestens 6 Hauptzahlen + 1 Superzahl
|
||||
continue
|
||||
|
||||
main_numbers = sorted(numbers[:6])
|
||||
superzahl = numbers[6]
|
||||
|
||||
draw = {
|
||||
'datum': date_obj,
|
||||
'Z1': main_numbers[0],
|
||||
'Z2': main_numbers[1],
|
||||
'Z3': main_numbers[2],
|
||||
'Z4': main_numbers[3],
|
||||
'Z5': main_numbers[4],
|
||||
'Z6': main_numbers[5],
|
||||
'SZ': superzahl
|
||||
}
|
||||
|
||||
if self._validate_draw(draw):
|
||||
draws.append(draw)
|
||||
else:
|
||||
print(f" ⚠️ Element {i+1}: Validierung fehlgeschlagen")
|
||||
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Fehler beim Extrahieren: {e}")
|
||||
|
||||
return draws
|
||||
|
||||
def _extract_date_text(self, text: str) -> str:
|
||||
"""Extrahiert Datum aus Text."""
|
||||
import re
|
||||
|
||||
# Suche nach Datum im Format DD.MM.YYYY oder YYYY-MM-DD
|
||||
patterns = [
|
||||
r'(\d{2}\.\d{2}\.\d{4})',
|
||||
r'(\d{4}-\d{2}-\d{2})',
|
||||
r'(\d{1,2}\.\s*\w+\s*\d{4})' # z.B. "22. Januar 2025"
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, text)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
return text.strip()
|
||||
|
||||
def _extract_numbers(self, elem) -> List[int]:
|
||||
"""Extrahiert Zahlen aus Element."""
|
||||
numbers = []
|
||||
|
||||
# 1. Versuche mit span/div mit Klassen wie 'ball', 'number', etc.
|
||||
number_elems = elem.find_all(class_=lambda x: x and any(c in x.lower() for c in ['ball', 'number', 'zahl']))
|
||||
|
||||
if number_elems:
|
||||
for num_elem in number_elems:
|
||||
try:
|
||||
text = num_elem.get_text().strip()
|
||||
# Extrahiere nur Zahlen
|
||||
import re
|
||||
nums = re.findall(r'\d+', text)
|
||||
if nums:
|
||||
numbers.append(int(nums[0]))
|
||||
except:
|
||||
continue
|
||||
|
||||
# 2. Fallback: Alle Zahlen aus Text extrahieren
|
||||
if len(numbers) < 7:
|
||||
import re
|
||||
text = elem.get_text()
|
||||
# Finde alle Zahlen
|
||||
all_nums = re.findall(r'\b(\d+)\b', text)
|
||||
|
||||
# Filtere plausible Lotto-Zahlen (1-49 für Hauptzahlen, 0-9 für Superzahl)
|
||||
plausible = []
|
||||
for n in all_nums:
|
||||
num = int(n)
|
||||
if 1 <= num <= 49:
|
||||
plausible.append(num)
|
||||
elif 0 <= num <= 9 and len(plausible) >= 6:
|
||||
# Könnte Superzahl sein
|
||||
plausible.append(num)
|
||||
|
||||
if len(plausible) >= 7:
|
||||
numbers = plausible[:7]
|
||||
|
||||
return numbers
|
||||
|
||||
def _parse_date(self, date_str: str) -> datetime:
|
||||
"""Parst Datum."""
|
||||
import re
|
||||
|
||||
# Clean up
|
||||
date_str = date_str.strip()
|
||||
|
||||
# Format 1: DD.MM.YYYY
|
||||
try:
|
||||
return datetime.strptime(date_str, '%d.%m.%Y')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Format 2: YYYY-MM-DD
|
||||
try:
|
||||
return datetime.strptime(date_str, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Format 3: DD. MONAT YYYY (z.B. "22. Januar 2025")
|
||||
months_de = {
|
||||
'januar': 1, 'februar': 2, 'märz': 3, 'april': 4,
|
||||
'mai': 5, 'juni': 6, 'juli': 7, 'august': 8,
|
||||
'september': 9, 'oktober': 10, 'november': 11, 'dezember': 12
|
||||
}
|
||||
|
||||
match = re.search(r'(\d{1,2})\.\s*(\w+)\s*(\d{4})', date_str.lower())
|
||||
if match:
|
||||
day = int(match.group(1))
|
||||
month_str = match.group(2)
|
||||
year = int(match.group(3))
|
||||
|
||||
if month_str in months_de:
|
||||
return datetime(year, months_de[month_str], day)
|
||||
|
||||
# Fallback: Aktuelles Datum
|
||||
print(f" ⚠️ Konnte Datum nicht parsen: {date_str}")
|
||||
return datetime.now()
|
||||
|
||||
def _validate_draw(self, draw: Dict) -> bool:
|
||||
"""Validiert eine Ziehung."""
|
||||
try:
|
||||
required_fields = ['datum', 'Z1', 'Z2', 'Z3', 'Z4', 'Z5', 'Z6', 'SZ']
|
||||
if not all(field in draw for field in required_fields):
|
||||
return False
|
||||
|
||||
# Hauptzahlen (1-49)
|
||||
main_numbers = [draw[f'Z{i}'] for i in range(1, 7)]
|
||||
if not all(isinstance(n, int) and 1 <= n <= 49 for n in main_numbers):
|
||||
return False
|
||||
|
||||
if len(set(main_numbers)) != 6:
|
||||
return False
|
||||
|
||||
# Superzahl (0-9)
|
||||
if not isinstance(draw['SZ'], int) or not 0 <= draw['SZ'] <= 9:
|
||||
return False
|
||||
|
||||
if not isinstance(draw['datum'], datetime):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
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', 'Z6', 'SZ']
|
||||
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, max_pages: int = 5, create_backup: bool = True) -> bool:
|
||||
"""
|
||||
Führt komplettes Update durch.
|
||||
|
||||
Args:
|
||||
max_pages: Maximale Anzahl Seiten zum Scrapen
|
||||
create_backup: Backup vor Update 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()
|
||||
|
||||
new_draws = self.fetch_draws_from_web(max_pages=max_pages)
|
||||
|
||||
if not new_draws:
|
||||
print("\n❌ Keine Daten von Website geladen")
|
||||
print("💡 Tipp: Verwende simple_update.py für manuelle Eingabe")
|
||||
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(" LOTTO 6AUS49 WEB UPDATER")
|
||||
print(" Quelle: lotto.de")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
default_file = "/Users/sebastianfrohlich/Library/Mobile Documents/com~apple~CloudDocs/Jupyter Notebooks/Lotto/data/AlleLottozahlen.csv"
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
data_file = sys.argv[1]
|
||||
max_pages = int(sys.argv[2]) if len(sys.argv) > 2 else 5
|
||||
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()
|
||||
|
||||
pages_input = input("\nAnzahl Seiten laden (1-10, default=5): ").strip()
|
||||
max_pages = int(pages_input) if pages_input else 5
|
||||
|
||||
print()
|
||||
|
||||
updater = LottoWebUpdater(data_file)
|
||||
success = updater.update(max_pages=max_pages)
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+460
@@ -0,0 +1,460 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CSV-Validator für Lotto 6aus49 und Eurojackpot
|
||||
|
||||
Validiert CSV-Dateien auf:
|
||||
- Korrekte Spaltenstruktur
|
||||
- Datumformat und -konsistenz
|
||||
- Zahlenbereich und Eindeutigkeit
|
||||
- Wochentag (nur Mi/Sa für Lotto, nur Di/Fr für Eurojackpot)
|
||||
- Chronologische Sortierung
|
||||
- Duplikate
|
||||
- Fehlende Werte
|
||||
- Zukunftsdaten
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
import sys
|
||||
import os
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
|
||||
class LottoCSVValidator:
|
||||
"""Validator für Lotto 6aus49 CSV-Dateien."""
|
||||
|
||||
def __init__(self, csv_file: str):
|
||||
self.csv_file = csv_file
|
||||
self.df = None
|
||||
self.errors = []
|
||||
self.warnings = []
|
||||
self.lottery_type = self._detect_lottery_type()
|
||||
|
||||
def _detect_lottery_type(self) -> str:
|
||||
"""Erkennt ob Lotto oder Eurojackpot basierend auf Dateiname."""
|
||||
basename = os.path.basename(self.csv_file).lower()
|
||||
if 'eurojackpot' in basename:
|
||||
return 'eurojackpot'
|
||||
elif 'lotto' in basename:
|
||||
return 'lotto'
|
||||
else:
|
||||
# Versuche anhand der Spalten zu erkennen
|
||||
return 'unknown'
|
||||
|
||||
def load_csv(self) -> bool:
|
||||
"""Lädt CSV-Datei."""
|
||||
try:
|
||||
if not os.path.exists(self.csv_file):
|
||||
self.errors.append(f"❌ Datei existiert nicht: {self.csv_file}")
|
||||
return False
|
||||
|
||||
self.df = pd.read_csv(self.csv_file, sep=';')
|
||||
|
||||
# Auto-detect wenn noch unknown
|
||||
if self.lottery_type == 'unknown':
|
||||
if 'SZ2' in self.df.columns:
|
||||
self.lottery_type = 'eurojackpot'
|
||||
elif 'SZ' in self.df.columns:
|
||||
self.lottery_type = 'lotto'
|
||||
else:
|
||||
self.errors.append("❌ Kann Lottery-Typ nicht erkennen")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.errors.append(f"❌ Fehler beim Laden: {e}")
|
||||
return False
|
||||
|
||||
def validate_structure(self) -> bool:
|
||||
"""Validiert Spaltenstruktur."""
|
||||
if self.lottery_type == 'lotto':
|
||||
expected_cols = ['datum', 'Z1', 'Z2', 'Z3', 'Z4', 'Z5', 'Z6', 'SZ']
|
||||
elif self.lottery_type == 'eurojackpot':
|
||||
expected_cols = ['datum', 'Z1', 'Z2', 'Z3', 'Z4', 'Z5', 'SZ1', 'SZ2']
|
||||
else:
|
||||
self.errors.append("❌ Unbekannter Lottery-Typ")
|
||||
return False
|
||||
|
||||
actual_cols = list(self.df.columns)
|
||||
|
||||
if actual_cols != expected_cols:
|
||||
self.errors.append(f"❌ Spaltenstruktur falsch")
|
||||
self.errors.append(f" Erwartet: {expected_cols}")
|
||||
self.errors.append(f" Gefunden: {actual_cols}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def validate_dates(self) -> bool:
|
||||
"""Validiert Datumsspalte."""
|
||||
valid = True
|
||||
|
||||
# Prüfe auf leere Daten
|
||||
empty_dates = self.df[self.df['datum'].isna() | (self.df['datum'] == '')]
|
||||
if len(empty_dates) > 0:
|
||||
self.errors.append(f"❌ {len(empty_dates)} Zeile(n) mit fehlendem Datum:")
|
||||
for idx in empty_dates.index[:5]: # Zeige max 5
|
||||
self.errors.append(f" Zeile {idx + 2}")
|
||||
if len(empty_dates) > 5:
|
||||
self.errors.append(f" ... und {len(empty_dates) - 5} weitere")
|
||||
valid = False
|
||||
|
||||
# Konvertiere Datum
|
||||
try:
|
||||
self.df['datum'] = pd.to_datetime(self.df['datum'], format='%Y-%m-%d', errors='coerce')
|
||||
except Exception as e:
|
||||
self.errors.append(f"❌ Fehler beim Parsen der Datumsangaben: {e}")
|
||||
return False
|
||||
|
||||
# Prüfe auf ungültige Datumsangaben
|
||||
invalid_dates = self.df[self.df['datum'].isna()]
|
||||
if len(invalid_dates) > 0:
|
||||
self.errors.append(f"❌ {len(invalid_dates)} ungültige Datumsangaben")
|
||||
valid = False
|
||||
|
||||
# Prüfe auf Zukunftsdaten
|
||||
today = datetime.now()
|
||||
future_dates = self.df[self.df['datum'] > today]
|
||||
if len(future_dates) > 0:
|
||||
self.errors.append(f"❌ {len(future_dates)} Datum/Daten in der Zukunft:")
|
||||
for idx, row in future_dates.iterrows():
|
||||
self.errors.append(f" Zeile {idx + 2}: {row['datum'].strftime('%Y-%m-%d')}")
|
||||
valid = False
|
||||
|
||||
# Prüfe Wochentage
|
||||
if self.lottery_type == 'lotto':
|
||||
# Historisch:
|
||||
# 1955-1965: Sonntags (und manchmal Montags bei Feiertagen)
|
||||
# 1965-2000: Samstags
|
||||
# Ab 2000: Mittwoch + Samstag
|
||||
weekday_names = {0: 'Mo', 1: 'Di', 2: 'Mi', 3: 'Do', 4: 'Fr', 5: 'Sa', 6: 'So'}
|
||||
else: # eurojackpot
|
||||
# Eurojackpot: Dienstag + Freitag
|
||||
weekday_names = {0: 'Mo', 1: 'Di', 2: 'Mi', 3: 'Do', 4: 'Fr', 5: 'Sa', 6: 'So'}
|
||||
|
||||
wrong_weekdays = []
|
||||
for idx, row in self.df.iterrows():
|
||||
if pd.notna(row['datum']):
|
||||
weekday = row['datum'].weekday()
|
||||
date = row['datum']
|
||||
|
||||
if self.lottery_type == 'lotto':
|
||||
# Lotto: Historische Regeln
|
||||
if date < datetime(1965, 9, 1):
|
||||
# Bis Aug 1965: Sonntag (und Montag bei Feiertagen)
|
||||
if weekday not in [0, 6]: # Mo, So
|
||||
wrong_weekdays.append((idx, date))
|
||||
elif date < datetime(2000, 12, 1):
|
||||
# Sep 1965 - Nov 2000: Nur Samstag
|
||||
if weekday != 5: # Sa
|
||||
wrong_weekdays.append((idx, date))
|
||||
else:
|
||||
# Ab Dez 2000: Mittwoch + Samstag
|
||||
if weekday not in [2, 5]: # Mi, Sa
|
||||
wrong_weekdays.append((idx, date))
|
||||
else:
|
||||
# Eurojackpot: Dienstag + Freitag
|
||||
if weekday not in [1, 4]: # Di, Fr
|
||||
wrong_weekdays.append((idx, date))
|
||||
|
||||
if len(wrong_weekdays) > 0:
|
||||
# Filtere mögliche Feiertags-Sonderziehungen (< 5 pro Zeitraum = OK)
|
||||
if len(wrong_weekdays) <= 5:
|
||||
self.warnings.append(f"⚠️ {len(wrong_weekdays)} Ziehung(en) am ungewöhnlichen Wochentag (möglicherweise Feiertage):")
|
||||
for idx, date in wrong_weekdays:
|
||||
weekday_name = weekday_names[date.weekday()]
|
||||
self.warnings.append(f" Zeile {idx + 2}: {date.strftime('%Y-%m-%d')} ({weekday_name})")
|
||||
else:
|
||||
self.errors.append(f"❌ {len(wrong_weekdays)} Ziehung(en) am falschen Wochentag:")
|
||||
for idx, date in wrong_weekdays[:5]:
|
||||
weekday_name = weekday_names[date.weekday()]
|
||||
self.errors.append(f" Zeile {idx + 2}: {date.strftime('%Y-%m-%d')} ({weekday_name})")
|
||||
if len(wrong_weekdays) > 5:
|
||||
self.errors.append(f" ... und {len(wrong_weekdays) - 5} weitere")
|
||||
valid = False
|
||||
|
||||
return valid
|
||||
|
||||
def validate_numbers(self) -> bool:
|
||||
"""Validiert Zahlenwerte."""
|
||||
valid = True
|
||||
|
||||
if self.lottery_type == 'lotto':
|
||||
main_cols = ['Z1', 'Z2', 'Z3', 'Z4', 'Z5', 'Z6']
|
||||
main_range = (1, 49)
|
||||
sz_cols = ['SZ']
|
||||
sz_range = (0, 9)
|
||||
else: # eurojackpot
|
||||
main_cols = ['Z1', 'Z2', 'Z3', 'Z4', 'Z5']
|
||||
main_range = (1, 50)
|
||||
sz_cols = ['SZ1', 'SZ2']
|
||||
sz_range = (1, 12)
|
||||
|
||||
# Prüfe Hauptzahlen
|
||||
for col in main_cols:
|
||||
# Fehlende Werte
|
||||
missing = self.df[self.df[col].isna()]
|
||||
if len(missing) > 0:
|
||||
self.errors.append(f"❌ {len(missing)} fehlende Werte in Spalte {col}")
|
||||
valid = False
|
||||
|
||||
# Zahlenbereich
|
||||
out_of_range = self.df[
|
||||
(self.df[col] < main_range[0]) |
|
||||
(self.df[col] > main_range[1])
|
||||
]
|
||||
if len(out_of_range) > 0:
|
||||
self.errors.append(f"❌ {len(out_of_range)} Werte außerhalb {main_range} in {col}")
|
||||
for idx, row in out_of_range.head(3).iterrows():
|
||||
self.errors.append(f" Zeile {idx + 2}: {row[col]}")
|
||||
valid = False
|
||||
|
||||
# Prüfe Superzahl(en)
|
||||
for col in sz_cols:
|
||||
missing = self.df[self.df[col].isna()]
|
||||
if len(missing) > 0:
|
||||
self.warnings.append(f"⚠️ {len(missing)} fehlende Werte in Spalte {col}")
|
||||
|
||||
out_of_range = self.df[
|
||||
(self.df[col] < sz_range[0]) |
|
||||
(self.df[col] > sz_range[1])
|
||||
]
|
||||
if len(out_of_range) > 0:
|
||||
self.errors.append(f"❌ {len(out_of_range)} Werte außerhalb {sz_range} in {col}")
|
||||
valid = False
|
||||
|
||||
# Prüfe Eindeutigkeit der Hauptzahlen pro Zeile
|
||||
duplicate_numbers = []
|
||||
for idx, row in self.df.iterrows():
|
||||
main_numbers = [row[col] for col in main_cols if pd.notna(row[col])]
|
||||
if len(main_numbers) != len(set(main_numbers)):
|
||||
duplicate_numbers.append((idx, main_numbers))
|
||||
|
||||
if len(duplicate_numbers) > 0:
|
||||
self.errors.append(f"❌ {len(duplicate_numbers)} Zeile(n) mit doppelten Hauptzahlen:")
|
||||
for idx, numbers in duplicate_numbers[:5]:
|
||||
self.errors.append(f" Zeile {idx + 2}: {numbers}")
|
||||
if len(duplicate_numbers) > 5:
|
||||
self.errors.append(f" ... und {len(duplicate_numbers) - 5} weitere")
|
||||
valid = False
|
||||
|
||||
# Prüfe Sortierung der Hauptzahlen pro Zeile
|
||||
unsorted_rows = []
|
||||
for idx, row in self.df.iterrows():
|
||||
main_numbers = [row[col] for col in main_cols if pd.notna(row[col])]
|
||||
if main_numbers != sorted(main_numbers):
|
||||
unsorted_rows.append((idx, main_numbers))
|
||||
|
||||
if len(unsorted_rows) > 0:
|
||||
self.warnings.append(f"⚠️ {len(unsorted_rows)} Zeile(n) mit unsortierten Zahlen:")
|
||||
for idx, numbers in unsorted_rows[:3]:
|
||||
self.warnings.append(f" Zeile {idx + 2}: {numbers}")
|
||||
|
||||
# Prüfe Sortierung der Eurozahlen (nur Eurojackpot)
|
||||
if self.lottery_type == 'eurojackpot':
|
||||
unsorted_euro = []
|
||||
for idx, row in self.df.iterrows():
|
||||
if pd.notna(row['SZ1']) and pd.notna(row['SZ2']):
|
||||
if row['SZ1'] > row['SZ2']:
|
||||
unsorted_euro.append((idx, row['SZ1'], row['SZ2']))
|
||||
|
||||
if len(unsorted_euro) > 0:
|
||||
self.warnings.append(f"⚠️ {len(unsorted_euro)} Zeile(n) mit unsortierten Eurozahlen")
|
||||
|
||||
return valid
|
||||
|
||||
def validate_duplicates(self) -> bool:
|
||||
"""Prüft auf doppelte Datumssätze."""
|
||||
duplicates = self.df[self.df.duplicated(subset=['datum'], keep=False)]
|
||||
|
||||
if len(duplicates) > 0:
|
||||
self.errors.append(f"❌ {len(duplicates)} doppelte Datumseinträge gefunden:")
|
||||
for idx, row in duplicates.head(5).iterrows():
|
||||
self.errors.append(f" Zeile {idx + 2}: {row['datum'].strftime('%Y-%m-%d')}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def validate_chronology(self) -> bool:
|
||||
"""Prüft chronologische Sortierung."""
|
||||
if len(self.df) < 2:
|
||||
return True
|
||||
|
||||
unsorted = []
|
||||
for i in range(len(self.df) - 1):
|
||||
if self.df.iloc[i]['datum'] > self.df.iloc[i + 1]['datum']:
|
||||
unsorted.append((i, self.df.iloc[i]['datum'], self.df.iloc[i + 1]['datum']))
|
||||
|
||||
if len(unsorted) > 0:
|
||||
self.warnings.append(f"⚠️ {len(unsorted)} Stelle(n) mit falscher chronologischer Reihenfolge:")
|
||||
for idx, date1, date2 in unsorted[:3]:
|
||||
self.warnings.append(f" Zeile {idx + 2}: {date1.strftime('%Y-%m-%d')} > {date2.strftime('%Y-%m-%d')}")
|
||||
|
||||
return len(unsorted) == 0
|
||||
|
||||
def validate_completeness(self) -> bool:
|
||||
"""Prüft auf Lücken in den Ziehungen."""
|
||||
if len(self.df) < 2:
|
||||
return True
|
||||
|
||||
if self.lottery_type == 'lotto':
|
||||
# Lotto: 2x pro Woche (Mi, Sa)
|
||||
expected_days = 3.5 # Durchschnitt
|
||||
else:
|
||||
# Eurojackpot: 2x pro Woche (Di, Fr)
|
||||
expected_days = 3.5
|
||||
|
||||
gaps = []
|
||||
for i in range(len(self.df) - 1):
|
||||
date1 = self.df.iloc[i]['datum']
|
||||
date2 = self.df.iloc[i + 1]['datum']
|
||||
diff = (date2 - date1).days
|
||||
|
||||
# Wenn Lücke größer als 7 Tage, könnte Ziehung fehlen
|
||||
if diff > 7:
|
||||
gaps.append((i, date1, date2, diff))
|
||||
|
||||
if len(gaps) > 0:
|
||||
self.warnings.append(f"⚠️ {len(gaps)} mögliche Lücke(n) in den Ziehungen (>7 Tage):")
|
||||
for idx, date1, date2, diff in gaps[:5]:
|
||||
self.warnings.append(f" Zeile {idx + 2}: {date1.strftime('%Y-%m-%d')} → {date2.strftime('%Y-%m-%d')} ({diff} Tage)")
|
||||
if len(gaps) > 5:
|
||||
self.warnings.append(f" ... und {len(gaps) - 5} weitere")
|
||||
|
||||
return True
|
||||
|
||||
def get_statistics(self) -> Dict:
|
||||
"""Erstellt Statistiken."""
|
||||
if self.df is None or len(self.df) == 0:
|
||||
return {}
|
||||
|
||||
stats = {
|
||||
'total_draws': len(self.df),
|
||||
'date_range': (
|
||||
self.df['datum'].min().strftime('%Y-%m-%d'),
|
||||
self.df['datum'].max().strftime('%Y-%m-%d')
|
||||
),
|
||||
'years_covered': (self.df['datum'].max().year - self.df['datum'].min().year) + 1,
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
def validate_all(self) -> bool:
|
||||
"""Führt alle Validierungen durch."""
|
||||
print(f"\n{'='*70}")
|
||||
print(f" CSV VALIDATOR")
|
||||
print(f" Typ: {self.lottery_type.upper()}")
|
||||
print(f"{'='*70}")
|
||||
print(f"\n📁 Datei: {os.path.basename(self.csv_file)}")
|
||||
|
||||
if not self.load_csv():
|
||||
return False
|
||||
|
||||
print(f"✅ Datei geladen: {len(self.df)} Zeilen")
|
||||
|
||||
# Alle Validierungen
|
||||
checks = [
|
||||
("Spaltenstruktur", self.validate_structure),
|
||||
("Datumsangaben", self.validate_dates),
|
||||
("Zahlenwerte", self.validate_numbers),
|
||||
("Duplikate", self.validate_duplicates),
|
||||
("Chronologie", self.validate_chronology),
|
||||
("Vollständigkeit", self.validate_completeness),
|
||||
]
|
||||
|
||||
print(f"\n🔍 VALIDIERUNG")
|
||||
print("="*70)
|
||||
|
||||
all_valid = True
|
||||
for check_name, check_func in checks:
|
||||
try:
|
||||
result = check_func()
|
||||
status = "✅" if result else "❌"
|
||||
print(f"{status} {check_name}")
|
||||
if not result:
|
||||
all_valid = False
|
||||
except Exception as e:
|
||||
print(f"❌ {check_name} - Fehler: {e}")
|
||||
all_valid = False
|
||||
|
||||
# Statistiken
|
||||
stats = self.get_statistics()
|
||||
if stats:
|
||||
print(f"\n📊 STATISTIKEN")
|
||||
print("="*70)
|
||||
print(f"Ziehungen gesamt: {stats['total_draws']}")
|
||||
print(f"Zeitraum: {stats['date_range'][0]} bis {stats['date_range'][1]}")
|
||||
print(f"Jahre: {stats['years_covered']}")
|
||||
|
||||
# Fehler ausgeben
|
||||
if self.errors:
|
||||
print(f"\n❌ FEHLER ({len(self.errors)})")
|
||||
print("="*70)
|
||||
for error in self.errors:
|
||||
print(error)
|
||||
|
||||
# Warnungen ausgeben
|
||||
if self.warnings:
|
||||
print(f"\n⚠️ WARNUNGEN ({len(self.warnings)})")
|
||||
print("="*70)
|
||||
for warning in self.warnings:
|
||||
print(warning)
|
||||
|
||||
# Zusammenfassung
|
||||
print(f"\n{'='*70}")
|
||||
if all_valid and not self.errors:
|
||||
print("✅ VALIDIERUNG ERFOLGREICH - Keine Fehler gefunden!")
|
||||
elif not self.errors and self.warnings:
|
||||
print("✅ VALIDIERUNG OK - Nur Warnungen (keine kritischen Fehler)")
|
||||
else:
|
||||
print("❌ VALIDIERUNG FEHLGESCHLAGEN")
|
||||
print("="*70)
|
||||
|
||||
return all_valid and len(self.errors) == 0
|
||||
|
||||
|
||||
def main():
|
||||
"""Hauptfunktion."""
|
||||
import sys
|
||||
|
||||
# Standard-Dateien
|
||||
default_files = {
|
||||
'lotto': '/Users/sebastianfrohlich/Library/Mobile Documents/com~apple~CloudDocs/Jupyter Notebooks/Lotto/data/AlleLottozahlen.csv',
|
||||
'eurojackpot': '/Users/sebastianfrohlich/Library/Mobile Documents/com~apple~CloudDocs/Jupyter Notebooks/AlleEurojackpotzahlen.csv'
|
||||
}
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
# Benutzerdefinierte Datei
|
||||
csv_file = sys.argv[1]
|
||||
validator = LottoCSVValidator(csv_file)
|
||||
success = validator.validate_all()
|
||||
sys.exit(0 if success else 1)
|
||||
else:
|
||||
# Validiere beide Standard-Dateien
|
||||
print("\n🎲 Validiere beide Lottery-Dateien...\n")
|
||||
|
||||
results = {}
|
||||
for lottery_type, csv_file in default_files.items():
|
||||
if os.path.exists(csv_file):
|
||||
validator = LottoCSVValidator(csv_file)
|
||||
results[lottery_type] = validator.validate_all()
|
||||
else:
|
||||
print(f"\n⚠️ {lottery_type.upper()}: Datei nicht gefunden: {csv_file}")
|
||||
results[lottery_type] = False
|
||||
|
||||
# Gesamtergebnis
|
||||
print("\n" + "="*70)
|
||||
print(" GESAMTERGEBNIS")
|
||||
print("="*70)
|
||||
for lottery_type, success in results.items():
|
||||
status = "✅" if success else "❌"
|
||||
print(f"{status} {lottery_type.upper()}")
|
||||
print("="*70)
|
||||
|
||||
all_success = all(results.values())
|
||||
sys.exit(0 if all_success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+401
@@ -0,0 +1,401 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Ziehungs-Verifizierer für Lotto 6aus49 und Eurojackpot
|
||||
|
||||
Verifiziert Ziehungen gegen offizielle Datenquellen:
|
||||
1. GitHub Lotto Archive (für Lotto 6aus49)
|
||||
2. Eurojackpot-zahlen.eu (für Eurojackpot)
|
||||
|
||||
Prüft:
|
||||
- Vollständigkeit (fehlende Ziehungen)
|
||||
- Korrektheit (falsche Zahlen)
|
||||
- Duplikate
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import requests
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
class DrawVerifier:
|
||||
"""Verifiziert Ziehungen gegen offizielle Quellen."""
|
||||
|
||||
def __init__(self, csv_file: str, lottery_type: str):
|
||||
self.csv_file = csv_file
|
||||
self.lottery_type = lottery_type
|
||||
self.df_local = None
|
||||
self.df_official = None
|
||||
self.errors = []
|
||||
self.warnings = []
|
||||
self.info = []
|
||||
|
||||
def load_local_data(self) -> bool:
|
||||
"""Lädt lokale CSV-Datei."""
|
||||
try:
|
||||
if not os.path.exists(self.csv_file):
|
||||
self.errors.append(f"❌ Datei existiert nicht: {self.csv_file}")
|
||||
return False
|
||||
|
||||
self.df_local = pd.read_csv(self.csv_file, sep=';')
|
||||
self.df_local['datum'] = pd.to_datetime(
|
||||
self.df_local['datum'],
|
||||
format='%Y-%m-%d',
|
||||
errors='coerce'
|
||||
)
|
||||
|
||||
self.info.append(f"✅ Lokale Daten: {len(self.df_local)} Ziehungen")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.errors.append(f"❌ Fehler beim Laden: {e}")
|
||||
return False
|
||||
|
||||
def fetch_official_lotto_data(self) -> bool:
|
||||
"""Holt offizielle Lotto 6aus49 Daten von GitHub Archive."""
|
||||
try:
|
||||
url = 'https://johannesfriedrich.github.io/LottoNumberArchive/Lottonumbers_tidy_complete.json'
|
||||
|
||||
print("🌐 Lade offizielle Lotto-Daten von GitHub Archive...")
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
|
||||
# Parse gruppierte Daten
|
||||
draws_by_id = {}
|
||||
for entry in data:
|
||||
draw_id = entry.get('id')
|
||||
if draw_id not in draws_by_id:
|
||||
draws_by_id[draw_id] = {
|
||||
'date': entry.get('date'),
|
||||
'numbers': [],
|
||||
'superzahl': None
|
||||
}
|
||||
|
||||
variable = entry.get('variable')
|
||||
value = entry.get('value')
|
||||
|
||||
if variable == 'Lottozahl':
|
||||
draws_by_id[draw_id]['numbers'].append(value)
|
||||
elif variable == 'Superzahl':
|
||||
draws_by_id[draw_id]['superzahl'] = value
|
||||
|
||||
# Konvertiere zu DataFrame
|
||||
official_draws = []
|
||||
for draw_id, draw_data in draws_by_id.items():
|
||||
try:
|
||||
date_str = draw_data['date']
|
||||
day, month, year = date_str.split('.')
|
||||
date_obj = datetime(int(year), int(month), int(day))
|
||||
|
||||
numbers = sorted(draw_data['numbers'])
|
||||
|
||||
if len(numbers) == 6:
|
||||
official_draws.append({
|
||||
'datum': date_obj,
|
||||
'Z1': numbers[0],
|
||||
'Z2': numbers[1],
|
||||
'Z3': numbers[2],
|
||||
'Z4': numbers[3],
|
||||
'Z5': numbers[4],
|
||||
'Z6': numbers[5],
|
||||
'SZ': draw_data['superzahl']
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
self.df_official = pd.DataFrame(official_draws)
|
||||
self.info.append(f"✅ Offizielle Daten: {len(self.df_official)} Ziehungen")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.errors.append(f"❌ Fehler beim Laden offizieller Daten: {e}")
|
||||
return False
|
||||
|
||||
def fetch_official_eurojackpot_data(self) -> bool:
|
||||
"""
|
||||
Holt offizielle Eurojackpot Daten.
|
||||
|
||||
Note: Da es keine vollständige öffentliche API gibt,
|
||||
beschränken wir uns auf Plausibilitätsprüfungen.
|
||||
"""
|
||||
self.warnings.append("⚠️ Keine vollständige offizielle Eurojackpot-API verfügbar")
|
||||
self.warnings.append(" Nur Plausibilitätsprüfungen möglich")
|
||||
|
||||
# Erstelle minimale "offizielle" Daten basierend auf erwarteten Ziehungsterminen
|
||||
# Dies ist nur für Vollständigkeitsprüfung
|
||||
|
||||
if len(self.df_local) > 0:
|
||||
start_date = self.df_local['datum'].min()
|
||||
end_date = datetime.now()
|
||||
|
||||
expected_dates = []
|
||||
current = start_date
|
||||
|
||||
while current <= end_date:
|
||||
# Eurojackpot: Dienstag (1) und Freitag (4)
|
||||
if current.weekday() in [1, 4]:
|
||||
expected_dates.append(current)
|
||||
current += timedelta(days=1)
|
||||
|
||||
self.df_official = pd.DataFrame({'datum': expected_dates})
|
||||
self.info.append(f"ℹ️ Erwartete Ziehungstermine: {len(expected_dates)}")
|
||||
|
||||
return True
|
||||
|
||||
def compare_completeness(self) -> List[datetime]:
|
||||
"""Prüft auf fehlende Ziehungen."""
|
||||
if self.df_official is None or self.df_local is None:
|
||||
return []
|
||||
|
||||
official_dates = set(self.df_official['datum'].dt.date)
|
||||
local_dates = set(self.df_local['datum'].dt.date)
|
||||
|
||||
missing = official_dates - local_dates
|
||||
extra = local_dates - official_dates
|
||||
|
||||
if missing:
|
||||
self.errors.append(f"❌ {len(missing)} fehlende Ziehung(en):")
|
||||
for date in sorted(missing)[:10]:
|
||||
self.errors.append(f" {date.strftime('%Y-%m-%d')}")
|
||||
if len(missing) > 10:
|
||||
self.errors.append(f" ... und {len(missing) - 10} weitere")
|
||||
|
||||
if extra:
|
||||
self.warnings.append(f"⚠️ {len(extra)} zusätzliche Ziehung(en) (nicht in offiziellen Daten):")
|
||||
for date in sorted(extra)[:5]:
|
||||
self.warnings.append(f" {date.strftime('%Y-%m-%d')}")
|
||||
if len(extra) > 5:
|
||||
self.warnings.append(f" ... und {len(extra) - 5} weitere")
|
||||
|
||||
return list(missing)
|
||||
|
||||
def compare_accuracy(self) -> int:
|
||||
"""Vergleicht Zahlenwerte mit offiziellen Daten."""
|
||||
if self.df_official is None or self.df_local is None:
|
||||
return 0
|
||||
|
||||
if self.lottery_type == 'eurojackpot':
|
||||
# Keine detaillierten offiziellen Daten verfügbar
|
||||
return 0
|
||||
|
||||
mismatches = 0
|
||||
|
||||
# Merge auf Datum
|
||||
merged = pd.merge(
|
||||
self.df_local,
|
||||
self.df_official,
|
||||
on='datum',
|
||||
suffixes=('_local', '_official'),
|
||||
how='inner'
|
||||
)
|
||||
|
||||
if self.lottery_type == 'lotto':
|
||||
cols_to_check = ['Z1', 'Z2', 'Z3', 'Z4', 'Z5', 'Z6', 'SZ']
|
||||
else: # eurojackpot
|
||||
cols_to_check = ['Z1', 'Z2', 'Z3', 'Z4', 'Z5', 'SZ1', 'SZ2']
|
||||
|
||||
for idx, row in merged.iterrows():
|
||||
mismatch_cols = []
|
||||
|
||||
for col in cols_to_check:
|
||||
local_col = f"{col}_local"
|
||||
official_col = f"{col}_official"
|
||||
|
||||
if local_col in row and official_col in row:
|
||||
# Vergleiche nur wenn beide Werte vorhanden
|
||||
if pd.notna(row[local_col]) and pd.notna(row[official_col]):
|
||||
if row[local_col] != row[official_col]:
|
||||
mismatch_cols.append(
|
||||
f"{col}: {int(row[local_col])} ≠ {int(row[official_col])}"
|
||||
)
|
||||
|
||||
if mismatch_cols:
|
||||
mismatches += 1
|
||||
if mismatches == 1:
|
||||
self.errors.append("❌ Zahlen-Abweichungen gefunden:")
|
||||
|
||||
if mismatches <= 10:
|
||||
date_str = row['datum'].strftime('%Y-%m-%d')
|
||||
self.errors.append(f" {date_str}: {', '.join(mismatch_cols)}")
|
||||
|
||||
if mismatches > 10:
|
||||
self.errors.append(f" ... und {mismatches - 10} weitere Abweichungen")
|
||||
|
||||
return mismatches
|
||||
|
||||
def check_recent_draws(self, days: int = 30) -> None:
|
||||
"""Prüft besonders die letzten N Tage."""
|
||||
if self.df_local is None:
|
||||
return
|
||||
|
||||
cutoff = datetime.now() - timedelta(days=days)
|
||||
recent = self.df_local[self.df_local['datum'] >= cutoff]
|
||||
|
||||
self.info.append(f"ℹ️ Letzte {days} Tage: {len(recent)} Ziehungen")
|
||||
|
||||
if len(recent) == 0:
|
||||
self.warnings.append(f"⚠️ Keine Ziehungen in den letzten {days} Tagen!")
|
||||
|
||||
# Erwartete Anzahl berechnen
|
||||
if self.lottery_type == 'lotto':
|
||||
# 2x pro Woche
|
||||
expected = (days / 7) * 2
|
||||
else: # eurojackpot
|
||||
# 2x pro Woche
|
||||
expected = (days / 7) * 2
|
||||
|
||||
if len(recent) < expected * 0.8: # Toleranz 20%
|
||||
self.warnings.append(
|
||||
f"⚠️ Weniger Ziehungen als erwartet: {len(recent)} vs. ~{int(expected)}"
|
||||
)
|
||||
|
||||
def verify_all(self) -> bool:
|
||||
"""Führt komplette Verifikation durch."""
|
||||
print(f"\n{'='*70}")
|
||||
print(f" ZIEHUNGS-VERIFIZIERER")
|
||||
print(f" Typ: {self.lottery_type.upper()}")
|
||||
print(f"{'='*70}")
|
||||
print(f"\n📁 Datei: {os.path.basename(self.csv_file)}")
|
||||
|
||||
# Lade lokale Daten
|
||||
if not self.load_local_data():
|
||||
return False
|
||||
|
||||
# Lade offizielle Daten
|
||||
print()
|
||||
if self.lottery_type == 'lotto':
|
||||
if not self.fetch_official_lotto_data():
|
||||
return False
|
||||
else: # eurojackpot
|
||||
if not self.fetch_official_eurojackpot_data():
|
||||
return False
|
||||
|
||||
print(f"\n🔍 VERIFIKATION")
|
||||
print("="*70)
|
||||
|
||||
# Prüfungen
|
||||
print("Prüfe Vollständigkeit...")
|
||||
missing = self.compare_completeness()
|
||||
|
||||
if self.lottery_type == 'lotto':
|
||||
print("Prüfe Zahlenwerte...")
|
||||
mismatches = self.compare_accuracy()
|
||||
|
||||
print("Prüfe aktuelle Ziehungen...")
|
||||
self.check_recent_draws(30)
|
||||
|
||||
# Statistiken
|
||||
print(f"\n📊 STATISTIKEN")
|
||||
print("="*70)
|
||||
for info in self.info:
|
||||
print(info)
|
||||
|
||||
# Zusammenfassung
|
||||
if self.df_local is not None and self.df_official is not None:
|
||||
if self.lottery_type == 'lotto':
|
||||
overlap = len(pd.merge(
|
||||
self.df_local,
|
||||
self.df_official,
|
||||
on='datum',
|
||||
how='inner'
|
||||
))
|
||||
|
||||
if overlap > 0:
|
||||
print(f"\n✅ {overlap} Ziehungen in beiden Quellen")
|
||||
|
||||
# Genauigkeit
|
||||
if self.lottery_type == 'lotto':
|
||||
accuracy = ((overlap - (mismatches if 'mismatches' in locals() else 0)) / overlap * 100)
|
||||
print(f"✅ Genauigkeit: {accuracy:.1f}%")
|
||||
|
||||
# Fehler
|
||||
if self.errors:
|
||||
print(f"\n❌ FEHLER ({len(self.errors)})")
|
||||
print("="*70)
|
||||
for error in self.errors:
|
||||
print(error)
|
||||
|
||||
# Warnungen
|
||||
if self.warnings:
|
||||
print(f"\n⚠️ WARNUNGEN ({len(self.warnings)})")
|
||||
print("="*70)
|
||||
for warning in self.warnings:
|
||||
print(warning)
|
||||
|
||||
# Ergebnis
|
||||
print(f"\n{'='*70}")
|
||||
if not self.errors:
|
||||
if self.warnings:
|
||||
print("✅ VERIFIKATION OK - Nur Warnungen")
|
||||
else:
|
||||
print("✅ VERIFIKATION ERFOLGREICH - Alle Ziehungen korrekt!")
|
||||
else:
|
||||
print("❌ VERIFIKATION FEHLGESCHLAGEN - Fehler gefunden")
|
||||
print("="*70)
|
||||
|
||||
return len(self.errors) == 0
|
||||
|
||||
|
||||
def main():
|
||||
"""Hauptfunktion."""
|
||||
|
||||
# Standard-Dateien
|
||||
files = {
|
||||
'lotto': {
|
||||
'path': '/Users/sebastianfrohlich/Library/Mobile Documents/com~apple~CloudDocs/Jupyter Notebooks/Lotto/data/AlleLottozahlen.csv',
|
||||
'type': 'lotto'
|
||||
},
|
||||
'eurojackpot': {
|
||||
'path': '/Users/sebastianfrohlich/Library/Mobile Documents/com~apple~CloudDocs/Jupyter Notebooks/AlleEurojackpotzahlen.csv',
|
||||
'type': 'eurojackpot'
|
||||
}
|
||||
}
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
# Einzelne Datei
|
||||
csv_file = sys.argv[1]
|
||||
lottery_type = sys.argv[2] if len(sys.argv) > 2 else 'lotto'
|
||||
|
||||
verifier = DrawVerifier(csv_file, lottery_type)
|
||||
success = verifier.verify_all()
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
else:
|
||||
# Beide Dateien
|
||||
print("\n🎲 Verifiziere beide Lottery-Dateien...\n")
|
||||
|
||||
results = {}
|
||||
|
||||
for name, config in files.items():
|
||||
if os.path.exists(config['path']):
|
||||
verifier = DrawVerifier(config['path'], config['type'])
|
||||
results[name] = verifier.verify_all()
|
||||
else:
|
||||
print(f"\n⚠️ {name.upper()}: Datei nicht gefunden")
|
||||
results[name] = False
|
||||
|
||||
# Gesamtergebnis
|
||||
print("\n" + "="*70)
|
||||
print(" GESAMTERGEBNIS")
|
||||
print("="*70)
|
||||
for name, success in results.items():
|
||||
status = "✅" if success else "❌"
|
||||
print(f"{status} {name.upper()}")
|
||||
print("="*70)
|
||||
|
||||
all_success = all(results.values())
|
||||
sys.exit(0 if all_success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user