Feature: Add club results extractor and course details scraper

- Add get_club_results.py: Extract all results for a club from results CSV
  - Fuzzy search for club names
  - List clubs functionality
  - Export results to CSV
- Add scrape_course_details.py: Scrape detailed info from course pages
  - Extract competition ID (frmcompetition)
  - Extract event details (distance, category, sex)
  - Extract course website link
  - Extract location and organizer info
- Update README.md with new scripts and usage examples
- Update version to v1.3.0
This commit is contained in:
Muyue
2026-01-03 11:03:35 +01:00
parent cbe10fe525
commit 6de44556f4
3 changed files with 823 additions and 2 deletions

114
README.md
View File

@@ -52,7 +52,9 @@ ffa-calendar/
│ ├── post_process.py # Post-traitement des données
│ ├── monitor_scraping.py # Surveillance du scraping
│ ├── scrape_all_periods.py # Scraping par périodes
── athlete_summary.py # Résumé par athlète
── athlete_summary.py # Résumé par athlète
│ ├── get_club_results.py # Extraction des résultats par club
│ └── scrape_course_details.py # Scraping des détails des courses
├── config/ # Fichiers de configuration
│ └── config.env # Configuration du scraper
├── data/ # Données générées
@@ -277,6 +279,61 @@ python scripts/list_clubs.py
python scripts/athlete_summary.py --data-dir data --output athlete_summary.csv
```
#### Extraction des résultats par club
**Récupérer tous les résultats d'un club:**
```bash
python scripts/get_club_results.py --club "Haute Saintonge Athletisme"
```
**Recherche floue (contient le nom):**
```bash
python scripts/get_club_results.py --club "Saintonge" --fuzzy-match
```
**Lister tous les clubs contenant un terme:**
```bash
python scripts/get_club_results.py --list-clubs --search-term "Saintonge"
```
Options:
- `--club`: Nom du club à rechercher
- `--fuzzy-match`: Recherche floue (défaut: True)
- `--exact-match`: Recherche exacte du nom
- `--list-clubs`: Lister tous les clubs disponibles
- `--search-term`: Terme de recherche pour filtrer les clubs
- `--limit`: Limiter le nombre de clubs affichés
- `--output-dir`: Répertoire de sortie pour les exports
Les résultats sont exportés dans `data/exports/` avec un fichier CSV contenant tous les résultats du club.
#### Scraping des détails et épreuves des courses
**Scraper les détails de toutes les courses:**
```bash
python scripts/scrape_course_details.py
```
**Scraper un nombre limité de courses:**
```bash
python scripts/scrape_course_details.py --limit 100
```
**Reprendre le scraping à partir d'un index:**
```bash
python scripts/scrape_course_details.py --start-from 500
```
Ce script génère deux fichiers CSV:
- `data/course_details.csv`: Détails de chaque course (ID, lieu, site web, etc.)
- `data/course_epreuves.csv`: Épreuves de chaque course (distance, catégorie, sexe, participants)
Options:
- `--courses-file`: Fichier CSV des courses (défaut: data/courses_daily.csv)
- `--output-dir`: Répertoire de sortie (défaut: data)
- `--limit`: Limiter le nombre de courses à scraper
- `--start-from`: Index de départ pour reprendre le scraping
### Via les modules Python
```python
@@ -323,6 +380,49 @@ data/
- lien: URL vers la page de la course
- type: Type de course (Cross, Stade, Route, etc.)
- categorie: Catégorie de la compétition
- resultats_url: URL vers la page des résultats
#### results.csv
- place: Place obtenue
- nom: Nom de l'athlète (en majuscules)
- prenom: Prénom de l'athlète
- club: Club de l'athlète
- categorie: Catégorie de compétition
- temps: Temps réalisé
- course_url: URL de la course concernée
#### course_details.csv (généré par scrape_course_details.py)
- course_id: ID unique de la course
- course_url: URL de la page des résultats
- course_nom: Nom de la course
- course_date: Date de la course
- course_lieu: Lieu de la course
- course_lieu_complet: Lieu détaillé
- course_site_web: Lien vers le site web de la course
- competition_id: Numéro de compétition FFA (frmcompetition)
- organisateur: Organisateur de la course
- contact: Informations de contact
- date_heure: Date et heure de la course
- lieu_details: Détails du lieu
- nb_epreuves: Nombre d'épreuves
#### course_epreuves.csv (généré par scrape_course_details.py)
- course_id: ID unique de la course
- epreuve_id: ID unique de l'épreuve
- epreuve_nom: Nom complet de l'épreuve
- epreuve_numero: Numéro de l'épreuve dans la course
- epreuve_distance: Distance (ex: 100m, 10km)
- epreuve_categorie: Catégorie (ex: Seniors, Vétérans)
- epreuve_sexe: Sexe (M/F)
- epreuve_type: Type d'épreuve (Piste, Cross, Trail, Route, Relais)
- participants: Nombre de participants
- url_resultats: URL des résultats
- nom: Nom de la course
- date: Date de la course
- lieu: Lieu de la course
- lien: URL vers la page de la course
- type: Type de course (Cross, Stade, Route, etc.)
- categorie: Catégorie de la compétition
#### results.csv
- place: Place obtenue
@@ -353,6 +453,8 @@ data/
| `monitor_scraping.py` | Surveillance en temps réel du scraping |
| `scrape_all_periods.py` | Scraping complet par périodes |
| `athlete_summary.py` | Génération de résumés par athlète |
| `get_club_results.py` | Extraction des résultats d'un club |
| `scrape_course_details.py` | Scraping des détails et épreuves des courses |
## 🚀 Performance
@@ -374,7 +476,15 @@ Note: Chaque driver Chrome consomme ~200-300 Mo de RAM.
## 📝 Version
### v1.2.0 (Dernière mise à jour)
### v1.3.0 (Dernière mise à jour)
- **Nouveau**: Script `get_club_results.py` pour extraire les résultats d'un club depuis le CSV
- **Nouveau**: Script `scrape_course_details.py` pour récupérer les détails et épreuves des courses
- **Nouveau**: Extraction des numéros de compétition (frmcompetition)
- **Nouveau**: Extraction des épreuves avec distance, catégorie et sexe
- **Nouveau**: Extraction des liens vers les sites web des courses
- **Amélioré**: Recherche flexible de clubs avec correspondance floue
### v1.2.0
- **Nouveau**: Multithreading pour accélérer le scraping (4 workers par défaut)
- **Nouveau**: Commande `check` pour détecter le nombre total de pages et de courses
- **Nouveau**: Détection automatique de la pagination et estimation du temps

218
scripts/get_club_results.py Executable file
View File

@@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""
Script pour récupérer tous les résultats d'un club depuis le CSV des résultats
Filtre simple sur le nom du club dans results_daily.csv
"""
import sys
import os
import pandas as pd
import csv
import argparse
from datetime import datetime
def get_club_results(club_name, results_file="data/results_daily.csv", output_dir="data/exports", fuzzy_match=True):
"""
Récupérer tous les résultats d'un club
Args:
club_name: Nom du club à rechercher
results_file: Fichier CSV des résultats
output_dir: Répertoire de sortie pour l'export
fuzzy_match: Utiliser la recherche floue (contient) ou exacte
Returns:
DataFrame pandas avec les résultats du club
"""
print(f"Recherche du club: {club_name}")
print(f"Fichier source: {results_file}")
# Lire le fichier CSV
try:
# Lire par chunks si le fichier est très gros
df = pd.read_csv(results_file, encoding='utf-8-sig')
print(f"{len(df)} résultats chargés")
except Exception as e:
print(f"✗ Erreur lecture CSV: {e}")
return None
# Filtrer par club
if fuzzy_match:
# Recherche floue: contient le nom
mask = df['club'].str.contains(club_name, case=False, na=False)
else:
# Recherche exacte
mask = df['club'].str.lower() == club_name.lower()
club_results = df[mask]
if len(club_results) == 0:
print(f"\n✗ Aucun résultat trouvé pour '{club_name}'")
print(f"\n💡 Suggestions:")
print(" - Vérifiez l'orthographe exacte du club")
print(" - Essayez une recherche partielle avec --fuzzy-match")
print(" - Utilisez --list-clubs pour voir tous les clubs disponibles")
return None
print(f"{len(club_results)} résultats trouvés")
# Afficher les variantes de noms de club trouvées
club_variants = club_results['club'].unique()
if len(club_variants) > 1:
print(f"\n📋 Variantes du nom trouvées ({len(club_variants)}):")
for variant in sorted(club_variants):
count = len(club_results[club_results['club'] == variant])
print(f" - {variant} ({count} résultats)")
# Statistiques
athletes = club_results[['nom', 'prenom']].drop_duplicates()
print(f"\n📊 Statistiques:")
print(f" - Athlètes différents: {len(athletes)}")
print(f" - Courses différentes: {len(club_results['course_nom'].unique())}")
print(f" - Dates couvertes: {club_results['course_date'].min()} à {club_results['course_date'].max()}")
return club_results
def export_club_results(club_results, club_name, output_dir="data/exports"):
"""Exporter les résultats d'un club en CSV"""
os.makedirs(output_dir, exist_ok=True)
# Nom du fichier de sortie
safe_name = club_name.replace('/', '-').replace('\\', '-').replace(' ', '_')
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"club_{safe_name}_{timestamp}.csv"
filepath = os.path.join(output_dir, filename)
# Sauvegarder
club_results.to_csv(filepath, index=False, encoding='utf-8-sig')
print(f"\n💾 Exporté: {filepath}")
print(f" {len(club_results)} lignes")
return filepath
def list_unique_clubs(results_file="data/results_daily.csv", search_term=None, limit=50):
"""Lister tous les clubs uniques dans les résultats"""
print(f"Lecture des clubs depuis {results_file}...")
try:
df = pd.read_csv(results_file, encoding='utf-8-sig')
except Exception as e:
print(f"✗ Erreur lecture CSV: {e}")
return
if search_term:
clubs = df[df['club'].str.contains(search_term, case=False, na=False)]['club'].unique()
print(f"\n📋 Clubs contenant '{search_term}' ({len(clubs)} trouvés):")
else:
clubs = df['club'].unique()
print(f"\n📋 Liste des clubs ({len(clubs)} clubs):")
# Trier et limiter l'affichage
clubs_sorted = sorted(clubs)[:limit] if limit else sorted(clubs)
for i, club in enumerate(clubs_sorted, 1):
count = len(df[df['club'] == club])
print(f" {i:3d}. {club} ({count} résultats)")
if len(clubs) > limit:
print(f"\n... et {len(clubs) - limit} autres clubs")
print(f"Utilisez --search-term pour filtrer")
def main():
parser = argparse.ArgumentParser(
description='Récupérer tous les résultats d\'un club FFA',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Exemples:
# Résultats du club Haute Saintonge Athletisme
python get_club_results.py --club "Haute Saintonge Athletisme"
# Recherche floue
python get_club_results.py --club "Saintonge" --fuzzy-match
# Liste tous les clubs contenant "Saintonge"
python get_club_results.py --list-clubs --search-term "Saintonge"
# Sauvegarder dans un fichier spécifique
python get_club_results.py --club "Paris Athletic" --output custom_output.csv
"""
)
parser.add_argument('--club', type=str,
help='Nom du club à rechercher')
parser.add_argument('--fuzzy-match', action='store_true', default=True,
help='Recherche floue (contient le nom, défaut: True)')
parser.add_argument('--exact-match', action='store_true',
help='Recherche exacte (désactive fuzzy-match)')
parser.add_argument('--export', action='store_true', default=True,
help='Exporter en CSV (défaut: True)')
parser.add_argument('--list-clubs', action='store_true',
help='Lister tous les clubs disponibles')
parser.add_argument('--search-term', type=str,
help='Terme de recherche pour lister les clubs')
parser.add_argument('--limit', type=int, default=50,
help='Limiter le nombre de clubs affichés (défaut: 50)')
parser.add_argument('--output-dir', default='data/exports',
help='Répertoire de sortie pour les exports')
parser.add_argument('--results-file', default='data/results_daily.csv',
help='Fichier CSV des résultats')
parser.add_argument('--output', type=str,
help='Nom du fichier de sortie (si --export)')
args = parser.parse_args()
# Si recherche exacte, désactiver fuzzy match
if args.exact_match:
args.fuzzy_match = False
# Lister les clubs
if args.list_clubs:
list_unique_clubs(args.results_file, args.search_term, args.limit)
return
# Vérifier qu'un nom de club est fourni
if not args.club:
parser.error("--club est requis (sauf si --list-clubs est utilisé)")
print("=" * 70)
print("🏃 Récupération des résultats par club FFA")
print("=" * 70)
# Récupérer les résultats
club_results = get_club_results(
args.club,
args.results_file,
args.output_dir,
args.fuzzy_match
)
if club_results is None or len(club_results) == 0:
return
# Exporter si demandé
if args.export:
if args.output:
# Utiliser le nom de fichier spécifié
os.makedirs(args.output_dir, exist_ok=True)
filepath = os.path.join(args.output_dir, args.output)
club_results.to_csv(filepath, index=False, encoding='utf-8-sig')
print(f"\n💾 Exporté: {filepath}")
else:
export_club_results(club_results, args.club, args.output_dir)
print("\n✅ Terminé!")
if __name__ == "__main__":
main()

493
scripts/scrape_course_details.py Executable file
View File

@@ -0,0 +1,493 @@
#!/usr/bin/env python3
"""
Script pour récupérer les détails des pages de courses FFA
Récupère:
- Le numéro de course (frmcompetition)
- Les différentes épreuves
- Le lieu
- Le lien vers le site de la course
"""
import sys
import os
import time
import csv
import logging
import re
import argparse
from tqdm import tqdm
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from bs4 import BeautifulSoup
import pandas as pd
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../src'))
# Configuration du logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('scrape_course_details.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
class CourseDetailsScraper:
"""Scraper pour récupérer les détails des pages de courses"""
def __init__(self, output_dir="data"):
self.output_dir = output_dir
self.base_url = "https://www.athle.fr"
self.request_delay = 2.0
self.max_retries = 3
# Fichiers de sortie
self.details_file = os.path.join(output_dir, "course_details.csv")
self.epreuves_file = os.path.join(output_dir, "course_epreuves.csv")
# Initialiser le driver Selenium
self.driver = None
# En-têtes CSV
self.details_headers = [
'course_id', 'course_url', 'course_nom', 'course_date', 'course_lieu',
'course_lieu_complet', 'course_site_web', 'competition_id',
'organisateur', 'contact', 'date_heure', 'lieu_details',
'nb_epreuves', 'url_source'
]
self.epreuves_headers = [
'course_id', 'epreuve_id', 'epreuve_nom', 'epreuve_numero',
'epreuve_distance', 'epreuve_categorie', 'epreuve_sexe',
'epreuve_type', 'participants', 'url_resultats'
]
# Initialiser les fichiers CSV
self._init_csv_files()
def _init_csv_file(self, filename, headers):
"""Initialiser un fichier CSV avec les en-têtes"""
if not os.path.exists(filename):
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
writer = csv.DictWriter(f, fieldnames=headers)
writer.writeheader()
logger.info(f"Fichier CSV initialisé: {filename}")
def _init_csv_files(self):
"""Initialiser tous les fichiers CSV"""
self._init_csv_file(self.details_file, self.details_headers)
self._init_csv_file(self.epreuves_file, self.epreuves_headers)
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 extract_competition_id(self, url):
"""Extraire l'ID de competition depuis l'URL (frmcompetition=XXX)"""
match = re.search(r'frmcompetition=(\d+)', url)
if match:
return match.group(1)
return ""
def get_courses_from_csv(self, courses_file):
"""Lire les courses depuis le fichier CSV"""
courses = []
try:
df = pd.read_csv(courses_file, encoding='utf-8-sig')
for idx, row in df.iterrows():
if pd.notna(row.get('resultats_url')) and row['resultats_url']:
courses.append({
'id': idx,
'nom': row.get('nom', ''),
'date': row.get('date', ''),
'lieu': row.get('lieu', ''),
'resultats_url': row['resultats_url'],
'lien': row.get('lien', '')
})
logger.info(f"{len(courses)} courses trouvées dans {courses_file}")
return courses
except Exception as e:
logger.error(f"Erreur lecture du fichier CSV: {e}")
return []
def parse_course_page(self, url, course_info):
"""Parser la page d'une course pour extraire les détails et épreuves"""
course_details = None
epreuves = []
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')
# Extraire l'ID de compétition
competition_id = self.extract_competition_id(url)
# ===== EXTRAIRE LES DÉTAILS DE LA COURSE =====
course_details = {
'course_id': course_info['id'],
'course_url': url,
'course_nom': course_info['nom'],
'course_date': course_info['date'],
'course_lieu': course_info['lieu'],
'course_lieu_complet': '',
'course_site_web': '',
'competition_id': competition_id,
'organisateur': '',
'contact': '',
'date_heure': '',
'lieu_details': '',
'nb_epreuves': 0,
'url_source': url
}
# Chercher les informations générales de la course
# Ces informations peuvent être dans des divs spécifiques ou le titre de la page
title = soup.find('h1')
if title:
course_details['course_nom'] = title.get_text(strip=True)
# Chercher les blocs d'information (lieu, date, etc.)
info_divs = soup.find_all('div', class_=re.compile(r'info|detail|location|lieu', re.IGNORECASE))
for div in info_divs:
text = div.get_text(strip=True)
if 'site web' in text.lower() or 'http' in text:
# Extraire le lien vers le site
links = div.find_all('a', href=True)
for link in links:
href = link['href']
if href.startswith('http'):
course_details['course_site_web'] = href
break
elif 'contact' in text.lower():
course_details['contact'] = text
elif 'organisateur' in text.lower():
course_details['organisateur'] = text
# Chercher le site web dans les liens
all_links = soup.find_all('a', href=True)
for link in all_links:
href = link['href']
link_text = link.get_text(strip=True).lower()
if (href.startswith('http') and
'athle.fr' not in href and
any(keyword in link_text for keyword in ['site', 'web', 'inscription', 'organisateur'])):
course_details['course_site_web'] = href
break
# ===== EXTRAIRE LES ÉPREUVES =====
main_table = soup.find('table', class_='reveal-table')
if not main_table:
logger.warning(f"Pas de table trouvée pour {url}")
return course_details, epreuves
rows = main_table.find_all('tr')
current_epreuve = None
epreuve_numero = 0
for row in rows:
cells = row.find_all(['td', 'th'])
# Ignorer les tables imbriquées
if row.find('table', class_='detail-inner-table'):
continue
# Détecter une nouvelle épreuve (en-tête de section)
if cells and len(cells) == 1:
cell = cells[0]
cell_text = cell.get_text(strip=True)
# Vérifier si c'est un en-tête d'épreuve
# Les épreuves FFA ont souvent: distance, catégorie, sexe
if cell_text and self._is_epreuve_header(cell_text):
epreuve_numero += 1
current_epreuve = {
'course_id': course_info['id'],
'epreuve_id': f"{course_info['id']}_E{epreuve_numero}",
'epreuve_nom': cell_text,
'epreuve_numero': epreuve_numero,
'epreuve_distance': self._extract_distance(cell_text),
'epreuve_categorie': self._extract_categorie(cell_text),
'epreuve_sexe': self._extract_sexe(cell_text),
'epreuve_type': self._extract_type(cell_text),
'participants': 0,
'url_resultats': url
}
# Compter les participants si c'est une ligne de résultats
if current_epreuve and cells and len(cells) >= 9:
try:
place = cells[0].get_text(strip=True)
int(place) # Vérifier que c'est un nombre
current_epreuve['participants'] += 1
except (ValueError, TypeError):
continue
# Ajouter l'épreuve si elle a des participants
if current_epreuve and current_epreuve['participants'] > 0:
epreuves.append(current_epreuve)
current_epreuve = None
course_details['nb_epreuves'] = len(epreuves)
logger.info(f"Course: {course_info['nom']} - {len(epreuves)} épreuves")
return course_details, epreuves
except Exception as e:
logger.error(f"Erreur tentative {attempt + 1}/{self.max_retries} pour {url}: {e}")
if attempt < self.max_retries - 1:
time.sleep(self.request_delay * (attempt + 1))
else:
logger.warning(f"Impossible de récupérer les détails pour {url}")
return course_details, epreuves
return course_details, epreuves
def _is_epreuve_header(self, text):
"""Vérifier si le texte est un en-tête d'épreuve"""
# Les épreuves contiennent généralement:
# - Une distance (ex: 100m, 5km, marathon)
# - Un sexe (F/H, Hommes/Femmes, Masculin/Féminin)
# - Une catégorie (Seniors, Vétérans, etc.)
keywords_distance = ['m ', 'm$', 'km', 'marathon', 'semi']
keywords_sexe = ['femmes', 'hommes', 'masculin', 'féminin', 'féminines', 'masculines', 'F ', 'H ', 'SEF', 'SEM']
keywords_categorie = ['seniors', 'vétérans', 'juniors', 'cadets', 'minimes', 'benjamins', 'poussins',
'espoirs', 'u20', 'u18', 'u16', 'u14', 'u23', 'm0', 'm1', 'm2', 'm3', 'm4']
# Vérifier la présence de mots-clés
has_distance = any(re.search(kw, text, re.IGNORECASE) for kw in keywords_distance)
has_sexe = any(re.search(kw, text, re.IGNORECASE) for kw in keywords_sexe)
has_categorie = any(re.search(kw, text, re.IGNORECASE) for kw in keywords_categorie)
# C'est probablement une épreuve si elle a au moins un de ces éléments
# et que le texte n'est pas trop court
return len(text) > 10 and (has_distance or has_sexe or has_categorie)
def _extract_distance(self, text):
"""Extraire la distance depuis le texte de l'épreuve"""
# Distance en mètres
match = re.search(r'(\d+)\s*m\b', text, re.IGNORECASE)
if match:
return f"{match.group(1)}m"
# Distance en km
match = re.search(r'(\d+(?:\.\d+)?)\s*km\b', text, re.IGNORECASE)
if match:
return f"{match.group(1)}km"
# Marathon ou semi-marathon
if 'marathon' in text.lower() and 'semi' not in text.lower():
return "42.195km"
elif 'semi' in text.lower():
return "21.1km"
return ""
def _extract_categorie(self, text):
"""Extraire la catégorie depuis le texte de l'épreuve"""
categories = ['Vétérans', 'Seniors', 'Juniors', 'Cadets', 'Minimes', 'Benjamins',
'Poussins', 'Espoirs', 'U20', 'U18', 'U16', 'U14', 'U23']
for cat in categories:
if cat in text:
return cat
return ""
def _extract_sexe(self, text):
"""Extraire le sexe depuis le texte de l'épreuve"""
text_upper = text.upper()
if any(kw in text_upper for kw in ['FEMMES', 'FÉMININ', 'FÉMININES', ' F ', 'SEF']):
return 'F'
elif any(kw in text_upper for kw in ['HOMMES', 'MASCULIN', 'MASCULINES', ' H ', 'SEM']):
return 'M'
else:
return ""
def _extract_type(self, text):
"""Extraire le type d'épreuve"""
if any(kw in text.lower() for kw in ['relais', 'relay']):
return 'Relais'
elif any(kw in text.lower() for kw in ['cross']):
return 'Cross'
elif any(kw in text.lower() for kw in ['trail']):
return 'Trail'
elif any(kw in text.lower() for kw in ['marathon', 'semi']):
return 'Route'
elif any(kw in text.lower() for kw in ['saut', 'lancer', 'lancé']):
return 'Épreuves combinées'
else:
return 'Piste'
def append_to_csv(self, data, filename, headers):
"""Ajouter des données au fichier CSV"""
if not data:
return
with open(filename, 'a', newline='', encoding='utf-8-sig') as f:
writer = csv.DictWriter(f, fieldnames=headers)
# data peut être un dict ou une liste
if isinstance(data, dict):
writer.writerow(data)
else:
writer.writerows(data)
logger.info(f"{len(data) if isinstance(data, list) else 1} enregistrement(s) ajouté(s) à {filename}")
def run(self, courses_file, limit=None, start_from=0):
"""Exécuter le scraping des détails pour toutes les courses"""
logger.info(f"Début du scraping des détails depuis {courses_file}")
# Charger les courses
courses = self.get_courses_from_csv(courses_file)
if not courses:
logger.error("Aucune course trouvée")
return
# Limiter ou démarrer à un index spécifique
if limit:
courses = courses[:limit]
logger.info(f"Limitation à {limit} courses")
if start_from > 0:
courses = courses[start_from:]
logger.info(f"Démarrage à l'index {start_from}")
# Initialiser le driver
self._init_driver()
total_details = 0
total_epreuves = 0
skipped = 0
# Scraper les détails pour chaque course
with tqdm(courses, desc="Courses scrapées", unit="course") as pbar:
for course in pbar:
try:
details, epreuves = self.parse_course_page(course['resultats_url'], course)
if details:
self.append_to_csv(details, self.details_file, self.details_headers)
total_details += 1
if epreuves:
self.append_to_csv(epreuves, self.epreuves_file, self.epreuves_headers)
total_epreuves += len(epreuves)
else:
skipped += 1
pbar.set_postfix({
'details': total_details,
'épreuves': total_epreuves,
'skipped': skipped
})
# Pause entre les requêtes
time.sleep(1)
except Exception as e:
logger.error(f"Erreur pour la course {course['nom']}: {e}")
skipped += 1
continue
# Fermer le driver
if self.driver:
self.driver.quit()
logger.info(f"Scraping terminé!")
logger.info(f"Details récupérés: {total_details}")
logger.info(f"Epreuves récupérées: {total_epreuves}")
logger.info(f"Courses sans épreuves: {skipped}")
return {
'total_courses': len(courses),
'total_details': total_details,
'total_epreuves': total_epreuves,
'skipped': skipped
}
def main():
"""Point d'entrée principal"""
parser = argparse.ArgumentParser(
description='Récupérer les détails des pages de courses FFA',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Exemples:
# Scraper toutes les courses
python scrape_course_details.py
# Scraper seulement les 100 premières courses
python scrape_course_details.py --limit 100
# Reprendre à partir de l'index 500
python scrape_course_details.py --start-from 500
# Utiliser un autre fichier de courses
python scrape_course_details.py --courses-file data/courses_daily.csv
"""
)
parser.add_argument('--courses-file', default='data/courses_daily.csv',
help='Fichier CSV des courses (défaut: data/courses_daily.csv)')
parser.add_argument('--output-dir', default='data',
help='Répertoire de sortie (défaut: data)')
parser.add_argument('--limit', type=int,
help='Limiter le nombre de courses à scraper')
parser.add_argument('--start-from', type=int, default=0,
help='Index de départ pour reprendre le scraping')
args = parser.parse_args()
print("=" * 70)
print("🔄 Scraper des détails de courses FFA")
print("=" * 70)
print(f"Fichier des courses: {args.courses_file}")
print(f"Répertoire de sortie: {args.output_dir}")
if args.limit:
print(f"Limitation: {args.limit} courses")
if args.start_from > 0:
print(f"Démarrage à l'index: {args.start_from}")
print("=" * 70)
scraper = CourseDetailsScraper(output_dir=args.output_dir)
# Lancer le scraping
stats = scraper.run(
args.courses_file,
limit=args.limit,
start_from=args.start_from
)
print("\n" + "=" * 70)
print("✅ Scraping terminé!")
print("=" * 70)
print(f"Courses traitées: {stats['total_courses']}")
print(f"Détails récupérés: {stats['total_details']}")
print(f"Epreuves récupérées: {stats['total_epreuves']}")
print(f"Courses sans épreuves: {stats['skipped']}")
print("=" * 70)
if __name__ == "__main__":
main()