- Créer une arborescence propre (src/, scripts/, config/, data/, docs/, tests/) - Déplacer les modules Python dans src/ - Déplacer les scripts autonomes dans scripts/ - Nettoyer les fichiers temporaires et __pycache__ - Mettre à jour le README.md avec documentation complète - Mettre à jour les imports dans les scripts pour la nouvelle structure - Configurer le .gitignore pour ignorer les données et logs - Organiser les données dans data/ (courses, resultats, clubs, exports) Structure du projet: - src/: Modules principaux (ffa_scraper, ffa_analyzer) - scripts/: Scripts CLI et utilitaires - config/: Configuration (config.env) - data/: Données générées - docs/: Documentation - tests/: Tests unitaires 💘 Generated with Crush Assisted-by: GLM-4.7 via Crush <crush@charm.land>
216 lines
7.6 KiB
Python
Executable File
216 lines
7.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Script pour rechercher un athlète dans les résultats FFA
|
|
Peut utiliser les fichiers CSV ou chercher directement depuis l'URL FFA
|
|
"""
|
|
|
|
import pandas as pd
|
|
import os
|
|
import sys
|
|
import argparse
|
|
import logging
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../src'))
|
|
from ffa_scraper import FFAScraper
|
|
from ffa_analyzer import FFADataAnalyzer
|
|
from datetime import datetime
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
)
|
|
|
|
def search_athlete_csv(nom, prenom=None, data_dir="data"):
|
|
"""Rechercher un athlète dans les fichiers CSV"""
|
|
results_path = os.path.join(data_dir, 'resultats', 'results.csv')
|
|
|
|
if not os.path.exists(results_path):
|
|
logging.error(f"Fichier de résultats introuvable: {results_path}")
|
|
return []
|
|
|
|
try:
|
|
df = pd.read_csv(results_path, encoding='utf-8-sig')
|
|
|
|
# Filtre par nom (obligatoire)
|
|
mask = df['nom'].str.contains(nom, case=False, na=False)
|
|
|
|
# Filtre par prénom (optionnel)
|
|
if prenom:
|
|
mask &= df['prenom'].str.contains(prenom, case=False, na=False)
|
|
|
|
results = df[mask].to_dict('records')
|
|
logging.info(f"Trouvé {len(results)} résultats pour {nom} {prenom or ''}")
|
|
return results
|
|
except Exception as e:
|
|
logging.error(f"Erreur lors de la recherche dans les CSV: {e}")
|
|
return []
|
|
|
|
def search_athlete_live(nom, prenom=None, max_pages=5):
|
|
"""Rechercher un athlète en direct depuis le site FFA"""
|
|
scraper = FFAScraper()
|
|
|
|
logging.info(f"Recherche en direct pour {nom} {prenom or ''}...")
|
|
logging.warning("Note: Cela peut prendre du temps car il faut scraper les résultats")
|
|
|
|
# Récupérer les courses et leurs résultats
|
|
total_pages, total_courses, _ = scraper._detect_pagination_info()
|
|
|
|
if not total_pages:
|
|
logging.error("Impossible de détecter les données")
|
|
return []
|
|
|
|
max_pages = min(max_pages, total_pages)
|
|
logging.info(f"Analyse de {max_pages} pages...")
|
|
|
|
all_results = []
|
|
courses = scraper.get_courses_list(max_pages=max_pages, use_multithreading=True)
|
|
|
|
for course in courses:
|
|
if course.get('resultats_url'):
|
|
results = scraper.get_course_results(course['resultats_url'])
|
|
|
|
# Filtrer les résultats pour cet athlète
|
|
for result in results:
|
|
match_nom = nom.lower() in result.get('nom', '').lower()
|
|
match_prenom = not prenom or prenom.lower() in result.get('prenom', '').lower()
|
|
|
|
if match_nom and match_prenom:
|
|
# Ajouter des infos de la course
|
|
result['course_nom'] = course.get('nom', '')
|
|
result['course_date'] = course.get('date', '')
|
|
result['course_lieu'] = course.get('lieu', '')
|
|
all_results.append(result)
|
|
|
|
logging.info(f"Trouvé {len(all_results)} résultats en direct")
|
|
return all_results
|
|
|
|
def display_athlete_results(results, show_details=False, limit=None):
|
|
"""Afficher les résultats d'un athlète"""
|
|
if not results:
|
|
print("\n❌ Aucun résultat trouvé pour cet athlète")
|
|
return
|
|
|
|
# Identifier l'athlète
|
|
athlete_nom = results[0].get('nom', 'Inconnu')
|
|
athlete_prenom = results[0].get('prenom', '')
|
|
|
|
print(f"\n{'='*80}")
|
|
print(f"🏃 RÉSULTATS POUR {athlete_prenom} {athlete_nom}")
|
|
print(f"{'='*80}\n")
|
|
|
|
if limit:
|
|
results = results[:limit]
|
|
|
|
# Afficher les informations générales
|
|
print(f"Club: {results[0].get('club', 'Inconnu')}")
|
|
print(f"Total des courses: {len(results)}")
|
|
|
|
if show_details:
|
|
# Calculer des statistiques
|
|
podiums = 0
|
|
victoires = 0
|
|
places = []
|
|
|
|
for result in results:
|
|
try:
|
|
place = int(result.get('place', 0))
|
|
if place == 1:
|
|
victoires += 1
|
|
podiums += 1
|
|
elif place <= 3:
|
|
podiums += 1
|
|
places.append(place)
|
|
except:
|
|
pass
|
|
|
|
print(f"Victoires: {victoires}")
|
|
print(f"Podiums: {podiums}")
|
|
|
|
if places:
|
|
avg_place = sum(places) / len(places)
|
|
print(f"Place moyenne: {avg_place:.2f}")
|
|
|
|
print(f"\n{'='*80}\n")
|
|
|
|
# Afficher les résultats individuels
|
|
print(f"{'📋 LISTE DES COURSES':<}")
|
|
print(f"{'='*80}\n")
|
|
|
|
for i, result in enumerate(results, 1):
|
|
print(f"{i}. {result.get('course_nom', result.get('course_url', 'Inconnu'))}")
|
|
|
|
if result.get('course_date'):
|
|
print(f" 📅 Date: {result['course_date']}")
|
|
|
|
if result.get('course_lieu'):
|
|
print(f" 📍 Lieu: {result['course_lieu']}")
|
|
|
|
print(f" 🏆 Place: {result.get('place', 'N/A')}")
|
|
print(f" ⏱️ Temps: {result.get('temps', result.get('resultat', 'N/A'))}")
|
|
print(f" 🏷️ Catégorie: {result.get('categorie', 'N/A')}")
|
|
|
|
if show_details:
|
|
if result.get('points'):
|
|
print(f" 🎯 Points: {result['points']}")
|
|
if result.get('niveau'):
|
|
print(f" 📊 Niveau: {result['niveau']}")
|
|
|
|
print()
|
|
|
|
print(f"{'='*80}")
|
|
|
|
def export_results_csv(results, nom, prenom=None, output_dir="data"):
|
|
"""Exporter les résultats d'un athlète en CSV"""
|
|
os.makedirs(os.path.join(output_dir, 'exports'), exist_ok=True)
|
|
|
|
if prenom:
|
|
filename = f"athlete_{nom}_{prenom}_results.csv"
|
|
else:
|
|
filename = f"athlete_{nom}_results.csv"
|
|
|
|
filepath = os.path.join(output_dir, 'exports', filename.replace(" ", "_"))
|
|
|
|
df = pd.DataFrame(results)
|
|
df.to_csv(filepath, index=False, encoding='utf-8-sig')
|
|
logging.info(f"Exporté {len(results)} résultats dans {filepath}")
|
|
return filepath
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Rechercher un athlète dans les résultats FFA')
|
|
parser.add_argument('nom', help='Nom de l\'athlète à rechercher')
|
|
parser.add_argument('--prenom', help='Prénom de l\'athlète (optionnel)')
|
|
parser.add_argument('--csv', action='store_true', default=True,
|
|
help='Utiliser les fichiers CSV existants (défaut)')
|
|
parser.add_argument('--live', action='store_true',
|
|
help='Récupérer les données en direct depuis le site FFA')
|
|
parser.add_argument('--data-dir', default='data',
|
|
help='Répertoire des données CSV')
|
|
parser.add_argument('--max-pages', type=int, default=5,
|
|
help='Nombre maximum de pages à scraper en mode live (défaut: 5)')
|
|
parser.add_argument('--details', action='store_true',
|
|
help='Afficher les détails complets')
|
|
parser.add_argument('--limit', type=int,
|
|
help='Limiter le nombre de résultats affichés')
|
|
parser.add_argument('--export', action='store_true',
|
|
help='Exporter les résultats en CSV')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Recherche
|
|
if args.live:
|
|
print(f"\n🔍 Mode live: recherche en direct sur le site FFA...")
|
|
results = search_athlete_live(args.nom, args.prenom, args.max_pages)
|
|
else:
|
|
print(f"\n📂 Mode CSV: recherche dans {args.data_dir}/")
|
|
results = search_athlete_csv(args.nom, args.prenom, args.data_dir)
|
|
|
|
# Affichage
|
|
display_athlete_results(results, show_details=args.details, limit=args.limit)
|
|
|
|
# Export
|
|
if args.export and results:
|
|
filepath = export_results_csv(results, args.nom, args.prenom, args.data_dir)
|
|
print(f"\n💾 Exporté dans: {filepath}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|