🎯 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
190 lines
6.8 KiB
Python
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")
|