#!/usr/bin/env python3 """ Scraper FFA - Nouvelle approche jour par jour Ce script scrape les données de la FFA du 01/01/2024 au 01/08/2026, jour par jour, en récupérant les courses et leurs résultats. """ import sys import os import time from datetime import datetime, timedelta import csv import logging from tqdm import tqdm from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from webdriver_manager.chrome import ChromeDriverManager from bs4 import BeautifulSoup import pandas as pd # Configuration du logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('scraper_jour_par_jour.log'), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) class FFAScraperDaily: """Scraper FFA - Approche jour par jour""" def __init__(self, output_dir="data"): self.output_dir = output_dir self.base_url = "https://www.athle.fr" self.request_delay = 3.0 # Délai plus long pour éviter les erreurs 403/404 self.max_retries = 5 self.timeout = 45 # Créer les répertoires os.makedirs(output_dir, exist_ok=True) # Fichiers de sortie self.courses_file = os.path.join(output_dir, "courses_daily.csv") self.results_file = os.path.join(output_dir, "results_daily.csv") # Initialiser les en-têtes CSV si les fichiers n'existent pas self._init_csv_files() # Driver Selenium (initialisé plus tard) self.driver = None def _init_csv_files(self): """Initialiser les fichiers CSV avec les en-têtes""" # En-têtes pour les courses courses_headers = [ 'jour_recupere', # Jour pour lequel on a récupéré les courses 'nom', 'date', 'lieu', 'discipline', 'type', 'niveau', 'label', 'lien', 'fiche_detail', 'resultats_url' ] if not os.path.exists(self.courses_file): with open(self.courses_file, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.DictWriter(f, fieldnames=courses_headers) writer.writeheader() # En-têtes pour les résultats results_headers = [ 'jour_recupere', # Jour pour lequel on a récupéré les résultats 'course_nom', 'course_date', 'course_lieu', 'course_url', 'place', 'resultat', 'nom', 'prenom', 'club', 'dept', 'ligue', 'categorie', 'niveau', 'points', 'temps' ] if not os.path.exists(self.results_file): with open(self.results_file, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.DictWriter(f, fieldnames=results_headers) writer.writeheader() def _init_driver(self): """Initialiser le driver Selenium""" if self.driver is None: options = webdriver.ChromeOptions() options.add_argument('--headless') options.add_argument('--no-sandbox') options.add_argument('--disable-dev-shm-usage') options.add_argument('--disable-gpu') options.add_argument('--window-size=1920,1080') options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36') service = Service(ChromeDriverManager().install()) self.driver = webdriver.Chrome(service=service, options=options) logger.info("Driver Chrome initialisé") def _build_calendar_url(self, date_str): """Construire l'URL du calendrier pour une date spécifique""" base_url = "https://www.athle.fr/bases/liste.aspx" params = { 'frmpostback': 'true', 'frmbase': 'calendrier', 'frmmode': '1', 'frmespace': '0', 'frmsaisonffa': '', # Important: laisser vide pour ne pas filtrer par saison 'frmdate1': date_str, 'frmdate2': date_str, 'frmtype1': '', 'frmniveau': '', 'frmligue': '', 'frmdepartement': '', 'frmniveaulab': '', 'frmepreuve': '', 'frmtype2': '', 'frmtype3': '', 'frmtype4': '' } param_str = '&'.join([f"{k}={v}" for k, v in params.items()]) return f"{base_url}?{param_str}" def get_courses_for_day(self, date_str): """Récupérer les courses pour un jour spécifique""" self._init_driver() url = self._build_calendar_url(date_str) logger.info(f"Récupération des courses pour {date_str}") courses = [] for attempt in range(self.max_retries): try: self.driver.get(url) time.sleep(self.request_delay) html = self.driver.page_source soup = BeautifulSoup(html, 'html.parser') # Trouver la table principale main_table = soup.find('table', class_='reveal-table') if not main_table: logger.warning(f"Pas de table trouvée pour {date_str}") return courses rows = main_table.find_all('tr') for row in rows: cells = row.find_all('td') if len(cells) < 5 or row.find('table', class_='detail-inner-table'): continue try: # Récupérer la date (colonne 0) date = cells[0].get_text(strip=True) # Récupérer le nom de la course (colonne 1) libelle_cell = cells[1] nom = libelle_cell.get_text(strip=True) # Récupérer le lien de la course (colonne 1) course_url = '' link_element = libelle_cell.find('a', href=True) if link_element: href = link_element.get('href', '') if href.startswith('http'): course_url = href else: course_url = f"{self.base_url}{href}" # Récupérer le lieu (colonne 2) lieu = cells[2].get_text(strip=True) if len(cells) > 2 else '' # Récupérer le type (colonne 3) type_competition = cells[3].get_text(strip=True) if len(cells) > 3 else '' # Récupérer le niveau (colonne 4) niveau = cells[4].get_text(strip=True) if len(cells) > 4 else '' # Récupérer le label (colonne 5) label = cells[5].get_text(strip=True) if len(cells) > 5 else '' # Récupérer le lien vers la fiche détail (colonne 6 "Fiche") detail_url = '' if len(cells) > 6: fiche_cell = cells[6] fiche_link = fiche_cell.find('a', href=True) if fiche_link: href = fiche_link.get('href', '') if href.startswith('http'): detail_url = href else: detail_url = f"{self.base_url}{href}" # Récupérer le lien vers les résultats (colonne 7 "Résultats") result_url = '' if len(cells) > 7: result_cell = cells[7] result_link = result_cell.find('a', href=True) if result_link: href = result_link.get('href', '') if href.startswith('http'): result_url = href else: result_url = f"{self.base_url}{href}" course = { 'jour_recupere': date_str, 'nom': nom, 'date': date, 'lieu': lieu, 'discipline': '', 'type': type_competition, 'niveau': niveau, 'label': label, 'lien': course_url, 'fiche_detail': detail_url, 'resultats_url': result_url } courses.append(course) except Exception as e: logger.warning(f"Erreur parsing course: {e}") continue logger.info(f"{len(courses)} courses trouvées pour {date_str}") return courses except Exception as e: logger.error(f"Erreur tentative {attempt + 1}/{self.max_retries} pour {date_str}: {e}") if attempt < self.max_retries - 1: time.sleep(self.request_delay * (attempt + 1)) else: raise return courses def get_results_for_course(self, course): """Récupérer les résultats d'une course""" result_url = course.get('resultats_url', '') if not result_url: return [] logger.info(f"Récupération des résultats pour: {course['nom']}") results = [] for attempt in range(self.max_retries): try: self.driver.get(result_url) time.sleep(self.request_delay) html = self.driver.page_source soup = BeautifulSoup(html, 'html.parser') main_table = soup.find('table', class_='reveal-table') if not main_table: logger.warning(f"Pas de table de résultats pour {course['nom']}") return results rows = main_table.find_all('tr') header_found = False for row in rows: cells = row.find_all(['td', 'th']) if row.find('table', class_='detail-inner-table'): continue if not header_found and cells: first_cell_text = cells[0].get_text(strip=True) if first_cell_text == 'Place' and len(cells) >= 9: header_found = True continue if header_found and cells and len(cells) >= 9: try: place = cells[0].get_text(strip=True) resultat = cells[1].get_text(strip=True) nom_cell = cells[2].get_text(strip=True) if len(cells) > 2 else '' if nom_cell: parts = nom_cell.split(' ') if len(parts) >= 2: nom = parts[0] prenom = ' '.join(parts[1:]) else: nom = nom_cell prenom = '' else: nom = '' prenom = '' club = cells[3].get_text(strip=True) if len(cells) > 3 else '' dept = cells[4].get_text(strip=True) if len(cells) > 4 else '' ligue = cells[5].get_text(strip=True) if len(cells) > 5 else '' infos = cells[6].get_text(strip=True) if len(cells) > 6 else '' niveau = cells[7].get_text(strip=True) if len(cells) > 7 else '' points = cells[8].get_text(strip=True) if len(cells) > 8 else '' try: int(place) except (ValueError, TypeError): continue result = { 'jour_recupere': course['jour_recupere'], 'course_nom': course['nom'], 'course_date': course['date'], 'course_lieu': course['lieu'], 'course_url': result_url, 'place': place, 'resultat': resultat, 'nom': nom, 'prenom': prenom, 'club': club, 'dept': dept, 'ligue': ligue, 'categorie': infos, 'niveau': niveau, 'points': points, 'temps': resultat } results.append(result) except Exception as e: logger.warning(f"Erreur parsing résultat: {e}") continue logger.info(f"{len(results)} résultats trouvés pour {course['nom']}") return results except Exception as e: logger.error(f"Erreur tentative {attempt + 1}/{self.max_retries}: {e}") if attempt < self.max_retries - 1: time.sleep(self.request_delay * (attempt + 1)) else: logger.warning(f"Impossible de récupérer les résultats pour {course['nom']}") return results return results def append_to_csv(self, data, filename): """Ajouter des données à un fichier CSV""" if not data: return # Lire les en-têtes existants with open(filename, 'r', newline='', encoding='utf-8-sig') as f: reader = csv.DictReader(f) fieldnames = reader.fieldnames # Ajouter les nouvelles données with open(filename, 'a', newline='', encoding='utf-8-sig') as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writerows(data) logger.info(f"{len(data)} enregistrements ajoutés à {filename}") def run(self, start_date, end_date, fetch_results=True): """Exécuter le scraping jour par jour""" logger.info(f"Début du scraping du {start_date} au {end_date}") start_dt = datetime.strptime(start_date, '%Y-%m-%d') end_dt = datetime.strptime(end_date, '%Y-%m-%d') total_days = (end_dt - start_dt).days + 1 logger.info(f"Nombre total de jours à traiter: {total_days}") total_courses = 0 total_results = 0 # Barre de progression pour les jours day_bar = tqdm(range(total_days), desc="Jours traités", unit="jour") for day_offset in day_bar: current_date = start_dt + timedelta(days=day_offset) date_str = current_date.strftime('%Y-%m-%d') try: # Récupérer les courses du jour courses = self.get_courses_for_day(date_str) if courses: # Sauvegarder les courses self.append_to_csv(courses, self.courses_file) total_courses += len(courses) # Récupérer les résultats de chaque course if fetch_results: course_bar = tqdm(courses, desc=f"Résultats {date_str}", leave=False) for course in course_bar: try: results = self.get_results_for_course(course) if results: self.append_to_csv(results, self.results_file) total_results += len(results) except Exception as e: logger.error(f"Erreur récupération résultats pour {course['nom']}: {e}") continue else: logger.info(f"Aucune course trouvée pour {date_str}") # Mettre à jour la barre de progression day_bar.set_postfix({ 'courses': total_courses, 'résultats': total_results }) # Pause entre les jours pour éviter les erreurs 403/404 time.sleep(1) except Exception as e: logger.error(f"Erreur pour le jour {date_str}: {e}") continue # Fermer le driver if self.driver: self.driver.quit() logger.info(f"Scraping terminé!") logger.info(f"Total courses: {total_courses}") logger.info(f"Total résultats: {total_results}") return { 'total_days': total_days, 'total_courses': total_courses, 'total_results': total_results } def main(): """Point d'entrée principal""" # Période à scraper: du 01/01/2024 au 01/08/2026 start_date = "2024-01-01" end_date = "2026-08-01" print("=" * 60) print("🔄 Scraper FFA - Approche jour par jour") print("=" * 60) print(f"Période: {start_date} au {end_date}") print(f"Résultats: data/courses_daily.csv") print(f"Résultats: data/results_daily.csv") print("=" * 60) scraper = FFAScraperDaily(output_dir="data") stats = scraper.run(start_date, end_date, fetch_results=True) print("\n" + "=" * 60) print("✅ Scraping terminé!") print("=" * 60) print(f"Jours traités: {stats['total_days']}") print(f"Courses récupérées: {stats['total_courses']}") print(f"Résultats récupérés: {stats['total_results']}") print("=" * 60) if __name__ == "__main__": main()