✨ Feature: Add new daily scraper approach for FFA data
- New scraper_jour_par_jour.py: Day-by-day scraping approach * Fixes 403/404 errors from previous method * Uses frmsaisonffa= (empty) parameter to avoid season filtering * Scrapes courses and results for each day from 01/01/2024 to 01/08/2026 * Progressive CSV saving with 'jour_recupere' column for traceability - New scraper_jour_par_jour_cli.py: CLI version with customizable dates * --start-date: Custom start date (default: 2024-01-01) * --end-date: Custom end date (default: 2026-08-01) * --no-results: Skip result fetching for faster scraping * --output-dir: Custom output directory - Documentation in docs/NOUVEAU_SCRAPER.md * Explains problems with old approach * Details new day-by-day methodology * Usage instructions and examples - Cleaned up: Removed temporary test scripts and debug files
This commit is contained in:
102
docs/NOUVEAU_SCRAPER.md
Normal file
102
docs/NOUVEAU_SCRAPER.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Nouveau Scraper FFA - Approche Jour par Jour
|
||||||
|
|
||||||
|
Ce script remplace l'ancienne méthode de scraping qui avait des problèmes d'erreurs 403/404.
|
||||||
|
|
||||||
|
## Ce qui a changé
|
||||||
|
|
||||||
|
### Problèmes de l'ancienne approche
|
||||||
|
- Utilisait des périodes de 15 jours
|
||||||
|
- Paramètre `frmsaisonffa=2026` fixe qui causait des erreurs
|
||||||
|
- Erreurs 403/404 fréquentes
|
||||||
|
- Difficile de relancer sur une date spécifique
|
||||||
|
|
||||||
|
### Nouvelle approche
|
||||||
|
- Scraping **jour par jour** (plus robuste)
|
||||||
|
- Utilise `frmsaisonffa=` (vide) pour éviter les filtres par saison
|
||||||
|
- Récupère les courses et leurs résultats pour chaque jour
|
||||||
|
- Sauvegarde progressive dans des CSV
|
||||||
|
- **Colonne `jour_recupere`** pour savoir quel jour on a scrapé
|
||||||
|
- Facile de relancer à partir d'une date spécifique
|
||||||
|
|
||||||
|
## Fichiers générés
|
||||||
|
|
||||||
|
### data/courses_daily.csv
|
||||||
|
Contient toutes les courses récupérées avec les colonnes:
|
||||||
|
- `jour_recupere`: Date pour laquelle on a récupéré la course (format YYYY-MM-DD)
|
||||||
|
- `nom`: Nom de la course
|
||||||
|
- `date`: Date de la course (format affiché par le site)
|
||||||
|
- `lieu`: Lieu de la course
|
||||||
|
- `discipline`: Discipline
|
||||||
|
- `type`: Type de compétition
|
||||||
|
- `niveau`: Niveau (Départemental, Régional, etc.)
|
||||||
|
- `label`: Label
|
||||||
|
- `lien`: URL principale de la course
|
||||||
|
- `fiche_detail`: URL vers la fiche détaillée
|
||||||
|
- `resultats_url`: URL vers la page des résultats
|
||||||
|
|
||||||
|
### data/results_daily.csv
|
||||||
|
Contient tous les résultats des courses avec les colonnes:
|
||||||
|
- `jour_recupere`: Date pour laquelle on a récupéré les résultats
|
||||||
|
- `course_nom`: Nom de la course
|
||||||
|
- `course_date`: Date de la course
|
||||||
|
- `course_lieu`: Lieu de la course
|
||||||
|
- `course_url`: URL de la page des résultats
|
||||||
|
- `place`: Place obtenue
|
||||||
|
- `resultat`: Résultat (temps/points)
|
||||||
|
- `nom`: Nom de l'athlète
|
||||||
|
- `prenom`: Prénom de l'athlète
|
||||||
|
- `club`: Club de l'athlète
|
||||||
|
- `dept`: Département
|
||||||
|
- `ligue`: Ligue
|
||||||
|
- `categorie`: Catégorie
|
||||||
|
- `niveau`: Niveau
|
||||||
|
- `points`: Points
|
||||||
|
- `temps`: Temps réalisé
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
### Scraping complet (du 01/01/2024 au 01/08/2026)
|
||||||
|
```bash
|
||||||
|
python scripts/scraper_jour_par_jour.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Estimation du temps
|
||||||
|
- Environ 5-6 secondes par jour (avec récupération des résultats)
|
||||||
|
- 944 jours à scraper = ~1.3 à 1.5 heures
|
||||||
|
|
||||||
|
### Reprise après interruption
|
||||||
|
Si le script est interrompu, il continue d'ajouter aux fichiers CSV existants.
|
||||||
|
Pour recommencer à zéro, supprimez les fichiers:
|
||||||
|
```bash
|
||||||
|
rm data/courses_daily.csv data/results_daily.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
Le script génère des logs dans `scraper_jour_par_jour.log` et affiche la progression en temps réel.
|
||||||
|
|
||||||
|
## Modifications pour tests
|
||||||
|
|
||||||
|
Si vous voulez tester sur une période plus courte, modifiez les dates dans `scripts/scraper_jour_par_jour.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Ligne 276-277 dans la fonction main()
|
||||||
|
start_date = "2024-01-01" # Modifier ici
|
||||||
|
end_date = "2024-01-31" # Modifier ici
|
||||||
|
```
|
||||||
|
|
||||||
|
## Avantages de cette approche
|
||||||
|
|
||||||
|
1. **Robustesse**: Scraping jour par jour évite les problèmes de pagination
|
||||||
|
2. **Transparence**: La colonne `jour_recupere` permet de savoir exactement ce qui a été récupéré
|
||||||
|
3. **Reprise facile**: On peut relancer à n'importe quel moment
|
||||||
|
4. **Progressive sauvegarde**: Les données sont sauvegardées au fur et à mesure
|
||||||
|
5. **Pas de duplication**: Les courses sont clairement identifiées par leur jour de récupération
|
||||||
|
|
||||||
|
## URL utilisée
|
||||||
|
|
||||||
|
Le script utilise cette URL pour chaque jour:
|
||||||
|
```
|
||||||
|
https://www.athle.fr/bases/liste.aspx?frmpostback=true&frmbase=calendrier&frmmode=1&frmespace=0&frmsaisonffa=&frmdate1=YYYY-MM-DD&frmdate2=YYYY-MM-DD&frmtype1=&frmniveau=&frmligue=&frmdepartement=&frmniveaulab=&frmepreuve=&frmtype2=&frmtype3=&frmtype4=
|
||||||
|
```
|
||||||
|
|
||||||
|
Le paramètre clé est `frmsaisonffa=` (vide) qui permet de récupérer les résultats sans filtrer par saison.
|
||||||
464
scripts/scraper_jour_par_jour.py
Normal file
464
scripts/scraper_jour_par_jour.py
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
#!/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()
|
||||||
63
scripts/scraper_jour_par_jour_cli.py
Normal file
63
scripts/scraper_jour_par_jour_cli.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Scraper FFA - Version avec arguments en ligne de commande
|
||||||
|
|
||||||
|
Ce script scrape les données de la FFA jour par jour.
|
||||||
|
Les dates peuvent être spécifiées en ligne de commande.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime
|
||||||
|
from scripts.scraper_jour_par_jour import FFAScraperDaily
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Point d'entrée principal avec arguments"""
|
||||||
|
parser = argparse.ArgumentParser(description='Scraper FFA - Approche jour par jour')
|
||||||
|
parser.add_argument('--start-date', type=str, default='2024-01-01',
|
||||||
|
help='Date de début (format YYYY-MM-DD). Défaut: 2024-01-01')
|
||||||
|
parser.add_argument('--end-date', type=str, default='2026-08-01',
|
||||||
|
help='Date de fin (format YYYY-MM-DD). Défaut: 2026-08-01')
|
||||||
|
parser.add_argument('--output-dir', type=str, default='data',
|
||||||
|
help='Répertoire de sortie. Défaut: data')
|
||||||
|
parser.add_argument('--no-results', action='store_true',
|
||||||
|
help='Ne pas récupérer les résultats (uniquement les courses)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Valider les dates
|
||||||
|
try:
|
||||||
|
datetime.strptime(args.start_date, '%Y-%m-%d')
|
||||||
|
datetime.strptime(args.end_date, '%Y-%m-%d')
|
||||||
|
except ValueError:
|
||||||
|
print("❌ Erreur: Les dates doivent être au format YYYY-MM-DD")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("🔄 Scraper FFA - Approche jour par jour")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"Période: {args.start_date} au {args.end_date}")
|
||||||
|
print(f"Récupération des résultats: {'Non' if args.no_results else 'Oui'}")
|
||||||
|
print(f"Répertoire de sortie: {args.output_dir}")
|
||||||
|
print(f"Fichiers de sortie:")
|
||||||
|
print(f" - {args.output_dir}/courses_daily.csv")
|
||||||
|
print(f" - {args.output_dir}/results_daily.csv")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
scraper = FFAScraperDaily(output_dir=args.output_dir)
|
||||||
|
stats = scraper.run(args.start_date, args.end_date, fetch_results=not args.no_results)
|
||||||
|
|
||||||
|
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()
|
||||||
Reference in New Issue
Block a user