Files
ffa-calendar/scripts/extract_results_complete.py
Muyue f6c8e889d5 Feature: Complete FFA scraping system with results extraction
🎯 Major achievements:
- Scraped 133,358 courses from 2010-2026 (17 years)
- Extracted 1,753,172 athlete results
- Fixed season calculation bug for December months
- Implemented ultra-fast scraping without Selenium (100x faster)

📊 Data coverage:
- Temporal: 2010-2026 (complete)
- Monthly: All 12 months covered
- Geographic: 20,444 unique locations
- Results: 190.9 results per course average

🚀 Technical improvements:
- Season calculation corrected for FFA calendar system
- Sequential scraping for stability (no driver conflicts)
- Complete results extraction with all athlete data
- Club search functionality (found Haute Saintonge Athlétisme)

📁 New scripts:
- scrape_fast.py: Ultra-fast period scraping (requests + bs4)
- extract_results_complete.py: Complete results extraction
- combine_all_periods.py: Data consolidation tool

⏱️ Performance:
- Scraping: 16.1 minutes for 1,241 periods
- Extraction: 3 hours for 9,184 courses with results
- Total: 1,886,530 records extracted
2026-01-02 01:16:06 +01:00

190 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""
Script pour extraire les résultats des courses - VERSION COMPLETE
Sans limitation de nombre de résultats
"""
import os
import sys
import requests
import logging
from bs4 import BeautifulSoup
from tqdm import tqdm
import pandas as pd
import time
def extract_results_from_page(result_url):
"""Extraire TOUS les résultats d'une page de résultats"""
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
response = requests.get(result_url, headers=headers, timeout=30)
if response.status_code != 200:
return []
soup = BeautifulSoup(response.content, 'html.parser')
# Chercher la table des résultats
tables = soup.find_all('table')
results = []
for table in tables:
rows = table.find_all('tr')
# Chercher l'en-tête de la table
headers_found = False
for row in rows:
th = row.find_all('th')
if th:
headers_text = [h.get_text(strip=True).lower() for h in th]
# Vérifier si c'est une table de résultats
if 'rang' in headers_text or 'place' in headers_text or 'position' in headers_text:
headers_found = True
break
if headers_found:
# Extraire TOUTES les lignes de données
for row in rows:
cols = row.find_all('td')
if len(cols) >= 3:
try:
# Extraire les informations
rank = cols[0].get_text(strip=True)
name = cols[1].get_text(strip=True)
# Vérifier que c'est bien une ligne de données
if rank and name:
result = {
'rang': rank,
'nom': name,
'club': cols[2].get_text(strip=True) if len(cols) > 2 else '',
'temps': cols[3].get_text(strip=True) if len(cols) > 3 else '',
'annee_naissance': cols[4].get_text(strip=True) if len(cols) > 4 else '',
'categorie': cols[5].get_text(strip=True) if len(cols) > 5 else '',
'url_resultats': result_url
}
results.append(result)
except Exception as e:
continue
# 🔧 CORRECTION ICI : Retourner TOUS les résultats (pas de limite)
return results
except Exception as e:
logging.warning(f"Erreur pour {result_url}: {e}")
return []
def extract_all_results():
"""Extraire tous les résultats des courses"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('results_extraction.log'),
logging.StreamHandler()
]
)
logging.info("=== EXTRACTION COMPLÈTE DES RÉSULTATS ===")
logging.info("⚠️ AUCUNE LIMITE DE NOMBRE DE RÉSULTATS")
# Charger les données combinées
combined_file = "data/courses/courses_list_combined.csv"
if not os.path.exists(combined_file):
logging.error("Fichier combiné non trouvé !")
return None
df = pd.read_csv(combined_file, encoding='utf-8-sig')
# Filtrer les courses avec des URLs de résultats
df_with_results = df[df['resultats_url'].notna() & (df['resultats_url'] != '')]
logging.info(f"Courses à traiter: {len(df_with_results)}")
logging.info(f"Courses sans résultats: {len(df) - len(df_with_results)}")
# Extraire les résultats
all_results = []
with tqdm(total=len(df_with_results), desc="Extraction des résultats") as pbar:
for idx, row in df_with_results.iterrows():
result_url = row['resultats_url']
course_name = row.get('nom', 'N/A')
course_date = row.get('date', 'N/A')
try:
results = extract_results_from_page(result_url)
if results:
# Ajouter les informations de la course à chaque résultat
for result in results:
result['course_nom'] = course_name
result['course_date'] = course_date
result['course_lieu'] = row.get('lieu', 'N/A')
all_results.extend(results)
logging.info(f"[{idx+1}/{len(df_with_results)}] {len(results)} résultats pour {course_name} ({course_date})")
pbar.update(1)
# Attendre un peu entre les requêtes
time.sleep(0.3)
except Exception as e:
logging.error(f"Erreur pour {course_name}: {e}")
pbar.update(1)
# Sauvegarder les résultats
if all_results:
logging.info(f"Résultats extraits: {len(all_results)}")
output_dir = "data/results"
os.makedirs(output_dir, exist_ok=True)
output_file = os.path.join(output_dir, "results_extracted_complete.csv")
results_df = pd.DataFrame(all_results)
results_df.to_csv(output_file, index=False, encoding='utf-8-sig')
logging.info(f"Fichier sauvegardé: {output_file}")
# Statistiques
logging.info("\n=== STATISTIQUES ===")
logging.info(f"Résultats totaux: {len(all_results)}")
# Calculer le nombre moyen de résultats par course
avg_results = len(all_results) / len(df_with_results)
logging.info(f"Moyenne de résultats par course: {avg_results:.1f}")
# Trouver la course avec le plus de résultats
results_per_course = {}
for result in all_results:
course = result['course_nom']
if course not in results_per_course:
results_per_course[course] = 0
results_per_course[course] += 1
if results_per_course:
max_course = max(results_per_course, key=results_per_course.get)
min_course = min(results_per_course, key=results_per_course.get)
logging.info(f"Maximum: {results_per_course[max_course]} résultats pour {max_course}")
logging.info(f"Minimum: {results_per_course[min_course]} résultats pour {min_course}")
return results_df
else:
logging.warning("Aucun résultat extrait !")
return None
if __name__ == "__main__":
df = extract_all_results()
if df is not None:
logging.info("\n✅ Extraction complète terminée avec succès!")
else:
logging.info("\n❌ Extraction échouée")