scrapping agendadulibre
This commit is contained in:
parent
6deed13d0b
commit
74738772b4
18 changed files with 63557 additions and 11 deletions
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"makefile.configureOnOpen": false
|
||||
}
|
114
extractors/Makefile
Normal file
114
extractors/Makefile
Normal file
|
@ -0,0 +1,114 @@
|
|||
# Makefile pour le scraper agenda du libre
|
||||
|
||||
.PHONY: help install test demo monitor clean run setup-cron
|
||||
|
||||
# Configuration par défaut
|
||||
API_URL ?= https://api.openeventdatabase.org
|
||||
BATCH_SIZE ?= 1
|
||||
|
||||
help: ## Afficher l'aide
|
||||
@echo "🔧 Scraper Agenda du Libre - Commandes disponibles"
|
||||
@echo "=================================================="
|
||||
@echo ""
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
|
||||
@echo ""
|
||||
@echo "💡 Variables d'environnement:"
|
||||
@echo " API_URL URL de l'API OEDB (défaut: https://api.openeventdatabase.org)"
|
||||
@echo " BATCH_SIZE Taille des batches (défaut: 1)"
|
||||
@echo ""
|
||||
@echo "📝 Exemples:"
|
||||
@echo " make run BATCH_SIZE=5"
|
||||
@echo " make run API_URL=http://api.example.com:8080"
|
||||
|
||||
install: ## Installer les dépendances
|
||||
@echo "📦 Installation des dépendances..."
|
||||
sudo apt update
|
||||
sudo apt install -y python3-requests python3-icalendar
|
||||
@echo "✅ Dépendances installées"
|
||||
|
||||
test: ## Exécuter les tests
|
||||
@echo "🧪 Exécution des tests..."
|
||||
python3 test_agendadulibre.py
|
||||
python3 test_api_connection.py --api-url $(API_URL)
|
||||
|
||||
demo: ## Exécuter la démonstration
|
||||
@echo "🎭 Exécution de la démonstration..."
|
||||
python3 demo_agendadulibre.py
|
||||
|
||||
monitor: ## Afficher les statistiques
|
||||
@echo "📊 Affichage des statistiques..."
|
||||
python3 monitor_agendadulibre.py
|
||||
|
||||
run: ## Exécuter le scraper
|
||||
@echo "🚀 Exécution du scraper..."
|
||||
python3 agendadulibre.py --api-url $(API_URL) --batch-size $(BATCH_SIZE)
|
||||
|
||||
run-verbose: ## Exécuter le scraper en mode verbeux
|
||||
@echo "🚀 Exécution du scraper en mode verbeux..."
|
||||
python3 agendadulibre.py --api-url $(API_URL) --batch-size $(BATCH_SIZE) --verbose
|
||||
|
||||
run-force: ## Exécuter le scraper en forçant le rechargement
|
||||
@echo "🚀 Exécution du scraper avec rechargement forcé..."
|
||||
python3 agendadulibre.py --api-url $(API_URL) --batch-size $(BATCH_SIZE) --force-refresh
|
||||
|
||||
run-cache-test: ## Tester le système de cache
|
||||
@echo "🔄 Test du système de cache..."
|
||||
python3 agendadulibre.py --api-url $(API_URL) --batch-size 1 --verbose
|
||||
@echo " Premier appel terminé, deuxième appel (depuis cache)..."
|
||||
python3 agendadulibre.py --api-url $(API_URL) --batch-size 1 --verbose
|
||||
|
||||
setup-cron: ## Configurer la planification cron
|
||||
@echo "⏰ Configuration de la planification cron..."
|
||||
./setup_cron.sh
|
||||
|
||||
clean: ## Nettoyer les fichiers temporaires
|
||||
@echo "🧹 Nettoyage des fichiers temporaires..."
|
||||
rm -f agendadulibre_events.json
|
||||
rm -f agendadulibre_events.ics
|
||||
rm -f agendadulibre_scraper.log
|
||||
rm -f cron_agendadulibre.log
|
||||
@echo "✅ Nettoyage terminé"
|
||||
|
||||
status: ## Afficher le statut du système
|
||||
@echo "📊 Statut du système:"
|
||||
@echo "===================="
|
||||
@echo "📁 Fichier de données:"
|
||||
@if [ -f agendadulibre_events.json ]; then \
|
||||
SIZE=$$(stat -c%s agendadulibre_events.json 2>/dev/null || echo "0"); \
|
||||
echo " ✅ Présent ($$SIZE octets)"; \
|
||||
else \
|
||||
echo " ❌ Absent"; \
|
||||
fi
|
||||
@echo "📁 Fichier cache iCal:"
|
||||
@if [ -f agendadulibre_events.ics ]; then \
|
||||
SIZE=$$(stat -c%s agendadulibre_events.ics 2>/dev/null || echo "0"); \
|
||||
DATE=$$(stat -c%y agendadulibre_events.ics 2>/dev/null || echo "inconnue"); \
|
||||
echo " ✅ Présent ($$SIZE octets)"; \
|
||||
echo " 📅 Modifié: $$DATE"; \
|
||||
else \
|
||||
echo " ❌ Absent"; \
|
||||
fi
|
||||
@echo ""
|
||||
@echo "📋 Tâches cron:"
|
||||
@if crontab -l 2>/dev/null | grep -q agendadulibre; then \
|
||||
echo " ✅ Tâches configurées:"; \
|
||||
crontab -l | grep agendadulibre | sed 's/^/ /'; \
|
||||
else \
|
||||
echo " ❌ Aucune tâche configurée"; \
|
||||
fi
|
||||
@echo ""
|
||||
@echo "🔗 Connexion API:"
|
||||
@python3 -c "import requests; print(' ✅' if requests.get('$(API_URL)', timeout=5).status_code == 200 else ' ❌')" 2>/dev/null || echo " ❌ API non accessible"
|
||||
|
||||
logs: ## Afficher les logs récents
|
||||
@echo "📋 Logs récents:"
|
||||
@echo "==============="
|
||||
@if [ -f agendadulibre_scraper.log ]; then \
|
||||
tail -20 agendadulibre_scraper.log; \
|
||||
else \
|
||||
echo "Aucun fichier de log trouvé"; \
|
||||
fi
|
||||
|
||||
check-deps: ## Vérifier les dépendances
|
||||
@echo "🔍 Vérification des dépendances..."
|
||||
@python3 -c "import requests, icalendar; print('✅ Toutes les dépendances sont disponibles')" || echo "❌ Dépendances manquantes - exécutez 'make install'"
|
161
extractors/README_agendadulibre.md
Normal file
161
extractors/README_agendadulibre.md
Normal file
|
@ -0,0 +1,161 @@
|
|||
# Scraper Agenda du Libre
|
||||
|
||||
Script de scraping pour récupérer les événements de l'agenda du libre (https://www.agendadulibre.org/) et les envoyer à l'API OEDB.
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
- 📥 Récupération automatique du fichier iCal depuis l'agenda du libre
|
||||
- 🔄 Traitement par batch configurable
|
||||
- 💾 Sauvegarde locale de l'état des événements (JSON)
|
||||
- 🚫 Évite les doublons (ne renvoie pas les événements déjà traités)
|
||||
- 📊 Statistiques détaillées et logging
|
||||
- 🧪 Mode démo et tests inclus
|
||||
|
||||
## Installation
|
||||
|
||||
1. Installer les dépendances Python :
|
||||
```bash
|
||||
pip install -r requirements_agendadulibre.txt
|
||||
```
|
||||
|
||||
2. Rendre les scripts exécutables :
|
||||
```bash
|
||||
chmod +x agendadulibre.py
|
||||
chmod +x test_agendadulibre.py
|
||||
chmod +x demo_agendadulibre.py
|
||||
```
|
||||
|
||||
## Utilisation
|
||||
|
||||
### Scraping complet
|
||||
```bash
|
||||
# Utilisation basique (1 événement par batch)
|
||||
python agendadulibre.py
|
||||
|
||||
# Avec options personnalisées
|
||||
python agendadulibre.py --api-url https://api.openeventdatabase.org --batch-size 5 --verbose
|
||||
```
|
||||
|
||||
### Options disponibles
|
||||
- `--api-url` : URL de base de l'API OEDB (défaut: https://api.openeventdatabase.org)
|
||||
- `--batch-size` : Nombre d'événements à traiter par batch (défaut: 1)
|
||||
- `--verbose` : Mode verbeux pour plus de détails
|
||||
- `--force-refresh` : Forcer le rechargement du fichier iCal (ignorer le cache)
|
||||
- `--cache-duration` : Durée de validité du cache en heures (défaut: 1)
|
||||
|
||||
### Démonstration
|
||||
```bash
|
||||
# Mode démo (ne fait pas d'appels API réels)
|
||||
python demo_agendadulibre.py
|
||||
```
|
||||
|
||||
### Tests
|
||||
```bash
|
||||
# Exécuter les tests
|
||||
python test_agendadulibre.py
|
||||
```
|
||||
|
||||
## Fichiers générés
|
||||
|
||||
- `agendadulibre_events.json` : Base de données locale des événements traités
|
||||
- `agendadulibre_events.ics` : Cache local du fichier iCal (valide 1h)
|
||||
- `agendadulibre_scraper.log` : Logs détaillés du scraper
|
||||
|
||||
## Système de cache iCal
|
||||
|
||||
Le script utilise un système de cache intelligent pour éviter de télécharger le fichier iCal à chaque exécution :
|
||||
|
||||
- **Cache valide** : Le fichier iCal est mis en cache localement pendant 1 heure par défaut
|
||||
- **Rechargement automatique** : Si le cache est expiré, le fichier est automatiquement rechargé
|
||||
- **Fallback** : En cas d'erreur de téléchargement, le script utilise le cache même s'il est expiré
|
||||
- **Force refresh** : Option `--force-refresh` pour ignorer le cache et forcer le rechargement
|
||||
|
||||
### Avantages du cache
|
||||
- ⚡ **Performance** : Évite les téléchargements inutiles
|
||||
- 🔄 **Fiabilité** : Fonctionne même si l'API iCal est temporairement indisponible
|
||||
- 📊 **Efficacité** : Réduit la charge sur le serveur de l'agenda du libre
|
||||
|
||||
## Format des événements
|
||||
|
||||
Les événements sont convertis au format OEDB avec les propriétés suivantes :
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"label": "Titre de l'événement",
|
||||
"description": "Description de l'événement",
|
||||
"what": "culture.geek",
|
||||
"where": "Lieu de l'événement",
|
||||
"start": "2024-01-01T10:00:00",
|
||||
"stop": "2024-01-01T12:00:00",
|
||||
"url": "https://www.agendadulibre.org/event/123",
|
||||
"source": "agendadulibre.org",
|
||||
"last_modified_by": "agendadulibre_scraper"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [0, 0]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Gestion des doublons
|
||||
|
||||
Le script utilise un système de suivi local pour éviter les doublons :
|
||||
|
||||
- Chaque événement reçoit un ID unique basé sur son contenu
|
||||
- Les événements déjà traités avec succès ne sont pas renvoyés
|
||||
- Les événements en erreur peuvent être retentés
|
||||
- Les événements déjà existants (réponse 409) sont marqués comme traités
|
||||
|
||||
## Statuts des événements
|
||||
|
||||
- `saved` : Événement envoyé avec succès à l'API
|
||||
- `already_exists` : Événement déjà existant dans l'API (réponse 409)
|
||||
- `error` : Erreur lors de l'envoi à l'API
|
||||
|
||||
## Exemple de sortie
|
||||
|
||||
```
|
||||
2024-01-01 10:00:00 - INFO - 🚀 Démarrage du scraping de l'agenda du libre
|
||||
2024-01-01 10:00:01 - INFO - Récupération du fichier iCal depuis https://www.agendadulibre.org/events.ics
|
||||
2024-01-01 10:00:02 - INFO - Fichier iCal récupéré avec succès
|
||||
2024-01-01 10:00:03 - INFO - Traitement de 15 nouveaux événements par batch de 1
|
||||
2024-01-01 10:00:04 - INFO - Envoi de l'événement: Conférence Python
|
||||
2024-01-01 10:00:05 - INFO - ✅ Conférence Python - Créé avec succès
|
||||
...
|
||||
2024-01-01 10:00:30 - INFO - 📊 Statistiques finales:
|
||||
2024-01-01 10:00:30 - INFO - Total d'événements trouvés: 25
|
||||
2024-01-01 10:00:30 - INFO - Nouveaux événements envoyés: 12
|
||||
2024-01-01 10:00:30 - INFO - Événements déjà existants: 8
|
||||
2024-01-01 10:00:30 - INFO - Erreurs d'API: 2
|
||||
2024-01-01 10:00:30 - INFO - Erreurs de parsing: 3
|
||||
2024-01-01 10:00:30 - INFO - Événements envoyés cette fois: 12
|
||||
2024-01-01 10:00:30 - INFO - ✅ Scraping terminé avec succès
|
||||
```
|
||||
|
||||
## Planification
|
||||
|
||||
Pour automatiser le scraping, vous pouvez utiliser cron :
|
||||
|
||||
```bash
|
||||
# Exécuter toutes les heures
|
||||
0 * * * * cd /path/to/extractors && python agendadulibre.py --batch-size 5
|
||||
|
||||
# Exécuter tous les jours à 6h
|
||||
0 6 * * * cd /path/to/extractors && python agendadulibre.py --batch-size 10
|
||||
```
|
||||
|
||||
## Dépannage
|
||||
|
||||
### Erreur de connexion à l'API
|
||||
- Vérifiez que l'API OEDB est démarrée
|
||||
- Vérifiez l'URL de l'API avec `--api-url`
|
||||
|
||||
### Erreur de parsing iCal
|
||||
- Vérifiez la connectivité internet
|
||||
- Vérifiez que l'URL iCal est accessible
|
||||
|
||||
### Événements non géocodés
|
||||
- Les événements sont créés avec des coordonnées par défaut [0, 0]
|
||||
- Un processus de géocodage séparé peut être ajouté si nécessaire
|
402
extractors/agendadulibre.py
Normal file
402
extractors/agendadulibre.py
Normal file
|
@ -0,0 +1,402 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script de scraping pour l'agenda du libre (https://www.agendadulibre.org/)
|
||||
Utilise le fichier iCal pour récupérer les événements et les envoyer à l'API OEDB
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import icalendar
|
||||
from icalendar import Calendar, Event
|
||||
import logging
|
||||
|
||||
# Configuration par défaut
|
||||
api_oedb = "https://api.openeventdatabase.org"
|
||||
|
||||
# Configuration du logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('agendadulibre_scraper.log'),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AgendaDuLibreScraper:
|
||||
def __init__(self, api_base_url: str = api_oedb, batch_size: int = 1):
|
||||
self.api_base_url = api_base_url
|
||||
self.batch_size = batch_size
|
||||
self.data_file = "agendadulibre_events.json"
|
||||
self.ical_file = "agendadulibre_events.ics"
|
||||
self.ical_url = "https://www.agendadulibre.org/events.ics"
|
||||
self.cache_duration_hours = 1 # Durée de cache en heures
|
||||
|
||||
# Charger les données existantes
|
||||
self.events_data = self.load_events_data()
|
||||
|
||||
def load_events_data(self) -> Dict:
|
||||
"""Charge les données d'événements depuis le fichier JSON local"""
|
||||
if os.path.exists(self.data_file):
|
||||
try:
|
||||
with open(self.data_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement du fichier {self.data_file}: {e}")
|
||||
return {"events": {}, "last_update": None}
|
||||
return {"events": {}, "last_update": None}
|
||||
|
||||
def save_events_data(self):
|
||||
"""Sauvegarde les données d'événements dans le fichier JSON local"""
|
||||
try:
|
||||
with open(self.data_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.events_data, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la sauvegarde du fichier {self.data_file}: {e}")
|
||||
|
||||
def is_ical_cache_valid(self) -> bool:
|
||||
"""Vérifie si le cache iCal est encore valide (moins d'une heure)"""
|
||||
if not os.path.exists(self.ical_file):
|
||||
return False
|
||||
|
||||
try:
|
||||
file_time = os.path.getmtime(self.ical_file)
|
||||
cache_age = datetime.now().timestamp() - file_time
|
||||
cache_age_hours = cache_age / 3600
|
||||
|
||||
logger.debug(f"Cache iCal âgé de {cache_age_hours:.2f} heures")
|
||||
return cache_age_hours < self.cache_duration_hours
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la vérification du cache iCal: {e}")
|
||||
return False
|
||||
|
||||
def save_ical_cache(self, ical_content: bytes):
|
||||
"""Sauvegarde le contenu iCal en cache local"""
|
||||
try:
|
||||
with open(self.ical_file, 'wb') as f:
|
||||
f.write(ical_content)
|
||||
logger.info(f"Cache iCal sauvegardé dans {self.ical_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la sauvegarde du cache iCal: {e}")
|
||||
|
||||
def load_ical_cache(self) -> Optional[bytes]:
|
||||
"""Charge le contenu iCal depuis le cache local"""
|
||||
try:
|
||||
with open(self.ical_file, 'rb') as f:
|
||||
content = f.read()
|
||||
logger.info(f"Cache iCal chargé depuis {self.ical_file}")
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du chargement du cache iCal: {e}")
|
||||
return None
|
||||
|
||||
def fetch_ical_data(self, force_refresh: bool = False) -> Optional[Calendar]:
|
||||
"""Récupère et parse le fichier iCal depuis l'agenda du libre ou depuis le cache"""
|
||||
ical_content = None
|
||||
|
||||
# Vérifier si le cache est valide (sauf si on force le rechargement)
|
||||
if not force_refresh and self.is_ical_cache_valid():
|
||||
logger.info("Utilisation du cache iCal local (moins d'une heure)")
|
||||
ical_content = self.load_ical_cache()
|
||||
else:
|
||||
if force_refresh:
|
||||
logger.info(f"Rechargement forcé du fichier iCal depuis {self.ical_url}")
|
||||
else:
|
||||
logger.info(f"Cache iCal expiré ou absent, téléchargement depuis {self.ical_url}")
|
||||
|
||||
try:
|
||||
response = requests.get(self.ical_url, timeout=30)
|
||||
response.raise_for_status()
|
||||
ical_content = response.content
|
||||
|
||||
# Sauvegarder en cache
|
||||
self.save_ical_cache(ical_content)
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"Erreur lors de la récupération du fichier iCal: {e}")
|
||||
# Essayer de charger depuis le cache même s'il est expiré
|
||||
logger.info("Tentative de chargement depuis le cache expiré...")
|
||||
ical_content = self.load_ical_cache()
|
||||
|
||||
if ical_content is None:
|
||||
logger.error("Impossible de récupérer le contenu iCal")
|
||||
return None
|
||||
|
||||
try:
|
||||
calendar = Calendar.from_ical(ical_content)
|
||||
logger.info(f"Fichier iCal parsé avec succès")
|
||||
return calendar
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du parsing du fichier iCal: {e}")
|
||||
return None
|
||||
|
||||
def parse_event(self, event: Event) -> Optional[Dict]:
|
||||
"""Parse un événement iCal et le convertit au format OEDB"""
|
||||
try:
|
||||
# Récupérer les propriétés de base
|
||||
summary = str(event.get('summary', ''))
|
||||
description = str(event.get('description', ''))
|
||||
location = str(event.get('location', ''))
|
||||
url = str(event.get('url', ''))
|
||||
|
||||
# Gestion des dates
|
||||
dtstart = event.get('dtstart')
|
||||
dtend = event.get('dtend')
|
||||
|
||||
if not dtstart:
|
||||
logger.warning(f"Événement sans date de début: {summary}")
|
||||
return None
|
||||
|
||||
# Convertir les dates
|
||||
start_date = dtstart.dt
|
||||
if isinstance(start_date, datetime):
|
||||
start_iso = start_date.isoformat()
|
||||
else:
|
||||
# Date seulement (sans heure)
|
||||
start_iso = f"{start_date}T00:00:00"
|
||||
|
||||
end_date = None
|
||||
if dtend:
|
||||
end_dt = dtend.dt
|
||||
if isinstance(end_dt, datetime):
|
||||
end_iso = end_dt.isoformat()
|
||||
else:
|
||||
end_iso = f"{end_dt}T23:59:59"
|
||||
else:
|
||||
# Si pas de date de fin, ajouter 2 heures par défaut
|
||||
if isinstance(start_date, datetime):
|
||||
end_iso = (start_date + timedelta(hours=2)).isoformat()
|
||||
else:
|
||||
end_iso = f"{start_date}T02:00:00"
|
||||
|
||||
# Créer l'événement au format OEDB
|
||||
oedb_event = {
|
||||
"properties": {
|
||||
"label": summary,
|
||||
"description": description,
|
||||
"type": "scheduled",
|
||||
"what": "culture.floss", # Type par défaut pour l'agenda du libre
|
||||
"where": location,
|
||||
"start": start_iso,
|
||||
"stop": end_iso,
|
||||
"url": url if url else None,
|
||||
"source:name": "Agenda du Libre",
|
||||
"source:url": "https://www.agendadulibre.org/",
|
||||
"last_modified_by": "agendadulibre_scraper"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [0, 0] # Coordonnées par défaut, à géocoder si nécessaire
|
||||
}
|
||||
}
|
||||
|
||||
# Créer un ID unique basé sur le contenu
|
||||
event_id = self.generate_event_id(summary, start_iso, location)
|
||||
|
||||
return {
|
||||
"id": event_id,
|
||||
"event": oedb_event,
|
||||
"raw_ical": {
|
||||
"summary": summary,
|
||||
"description": description,
|
||||
"location": location,
|
||||
"url": url,
|
||||
"dtstart": start_iso,
|
||||
"dtend": end_iso
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du parsing de l'événement: {e}")
|
||||
return None
|
||||
|
||||
def generate_event_id(self, summary: str, start_date: str, location: str) -> str:
|
||||
"""Génère un ID unique pour l'événement"""
|
||||
import hashlib
|
||||
content = f"{summary}_{start_date}_{location}"
|
||||
return hashlib.md5(content.encode('utf-8')).hexdigest()
|
||||
|
||||
def send_event_to_api(self, event_data: Dict) -> Tuple[bool, str]:
|
||||
"""Envoie un événement à l'API OEDB"""
|
||||
try:
|
||||
url = f"{self.api_base_url}/event"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
# Formater l'événement au format GeoJSON attendu par l'API
|
||||
geojson_event = {
|
||||
"type": "Feature",
|
||||
"geometry": event_data["event"]["geometry"],
|
||||
"properties": event_data["event"]["properties"]
|
||||
}
|
||||
|
||||
response = requests.post(url, json=geojson_event, headers=headers, timeout=30)
|
||||
|
||||
if response.status_code == 201:
|
||||
return True, "Créé avec succès"
|
||||
elif response.status_code == 409:
|
||||
return False, "Événement déjà existant"
|
||||
else:
|
||||
return False, f"Erreur API: {response.status_code} - {response.text}"
|
||||
|
||||
except requests.RequestException as e:
|
||||
return False, f"Erreur de connexion: {e}"
|
||||
except Exception as e:
|
||||
return False, f"Erreur inattendue: {e}"
|
||||
|
||||
def process_events(self, calendar: Calendar) -> Dict:
|
||||
"""Traite tous les événements du calendrier"""
|
||||
stats = {
|
||||
"total_events": 0,
|
||||
"new_events": 0,
|
||||
"already_saved": 0,
|
||||
"api_errors": 0,
|
||||
"parse_errors": 0,
|
||||
"sent_this_run": 0
|
||||
}
|
||||
|
||||
events_to_process = []
|
||||
|
||||
# Parcourir tous les événements
|
||||
for component in calendar.walk():
|
||||
if component.name == "VEVENT":
|
||||
stats["total_events"] += 1
|
||||
|
||||
# Parser l'événement
|
||||
parsed_event = self.parse_event(component)
|
||||
if not parsed_event:
|
||||
stats["parse_errors"] += 1
|
||||
continue
|
||||
|
||||
event_id = parsed_event["id"]
|
||||
|
||||
# Vérifier si l'événement existe déjà dans nos données
|
||||
if event_id in self.events_data["events"]:
|
||||
event_status = self.events_data["events"][event_id].get("status", "unknown")
|
||||
if event_status in ["saved", "already_exists"]:
|
||||
stats["already_saved"] += 1
|
||||
logger.debug(f"Événement déjà traité: {parsed_event['event']['properties']['label']}")
|
||||
continue
|
||||
|
||||
events_to_process.append(parsed_event)
|
||||
|
||||
# Traiter les événements par batch
|
||||
logger.info(f"Traitement de {len(events_to_process)} nouveaux événements par batch de {self.batch_size}")
|
||||
|
||||
for i in range(0, len(events_to_process), self.batch_size):
|
||||
batch = events_to_process[i:i + self.batch_size]
|
||||
logger.info(f"Traitement du batch {i//self.batch_size + 1}/{(len(events_to_process) + self.batch_size - 1)//self.batch_size}")
|
||||
|
||||
for event_data in batch:
|
||||
event_id = event_data["id"]
|
||||
event_label = event_data["event"]["properties"]["label"]
|
||||
|
||||
logger.info(f"Envoi de l'événement: {event_label}")
|
||||
|
||||
# Envoyer à l'API
|
||||
success, message = self.send_event_to_api(event_data)
|
||||
|
||||
# Mettre à jour les statistiques et les données locales
|
||||
if success:
|
||||
stats["new_events"] += 1
|
||||
stats["sent_this_run"] += 1
|
||||
self.events_data["events"][event_id] = {
|
||||
"status": "saved",
|
||||
"message": message,
|
||||
"last_attempt": datetime.now().isoformat(),
|
||||
"event": event_data["event"]
|
||||
}
|
||||
logger.info(f"✅ {event_label} - {message}")
|
||||
else:
|
||||
if "déjà existant" in message or "already exists" in message.lower():
|
||||
stats["already_saved"] += 1
|
||||
self.events_data["events"][event_id] = {
|
||||
"status": "already_exists",
|
||||
"message": message,
|
||||
"last_attempt": datetime.now().isoformat(),
|
||||
"event": event_data["event"]
|
||||
}
|
||||
logger.info(f"⚠️ {event_label} - {message}")
|
||||
else:
|
||||
stats["api_errors"] += 1
|
||||
self.events_data["events"][event_id] = {
|
||||
"status": "error",
|
||||
"message": message,
|
||||
"last_attempt": datetime.now().isoformat(),
|
||||
"event": event_data["event"]
|
||||
}
|
||||
logger.error(f"❌ {event_label} - {message}")
|
||||
|
||||
# Mettre à jour la date de dernière mise à jour
|
||||
self.events_data["last_update"] = datetime.now().isoformat()
|
||||
|
||||
return stats
|
||||
|
||||
def run(self, force_refresh: bool = False):
|
||||
"""Exécute le scraping complet"""
|
||||
logger.info("🚀 Démarrage du scraping de l'agenda du libre")
|
||||
logger.info(f"Configuration: batch_size={self.batch_size}, api_url={self.api_base_url}")
|
||||
logger.info(f"Cache iCal: {'ignoré' if force_refresh else f'valide pendant {self.cache_duration_hours}h'}")
|
||||
|
||||
# Récupérer le fichier iCal
|
||||
calendar = self.fetch_ical_data(force_refresh=force_refresh)
|
||||
if not calendar:
|
||||
logger.error("❌ Impossible de récupérer le fichier iCal")
|
||||
return False
|
||||
|
||||
# Traiter les événements
|
||||
stats = self.process_events(calendar)
|
||||
|
||||
# Sauvegarder les données
|
||||
self.save_events_data()
|
||||
|
||||
# Afficher les statistiques finales
|
||||
logger.info("📊 Statistiques finales:")
|
||||
logger.info(f" Total d'événements trouvés: {stats['total_events']}")
|
||||
logger.info(f" Nouveaux événements envoyés: {stats['new_events']}")
|
||||
logger.info(f" Événements déjà existants: {stats['already_saved']}")
|
||||
logger.info(f" Erreurs d'API: {stats['api_errors']}")
|
||||
logger.info(f" Erreurs de parsing: {stats['parse_errors']}")
|
||||
logger.info(f" Événements envoyés cette fois: {stats['sent_this_run']}")
|
||||
|
||||
logger.info("✅ Scraping terminé avec succès")
|
||||
return True
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Scraper pour l'agenda du libre")
|
||||
parser.add_argument("--api-url", default=api_oedb,
|
||||
help=f"URL de base de l'API OEDB (défaut: {api_oedb})")
|
||||
parser.add_argument("--batch-size", type=int, default=1,
|
||||
help="Nombre d'événements à envoyer par batch (défaut: 1)")
|
||||
parser.add_argument("--verbose", "-v", action="store_true",
|
||||
help="Mode verbeux")
|
||||
parser.add_argument("--force-refresh", "-f", action="store_true",
|
||||
help="Forcer le rechargement du fichier iCal (ignorer le cache)")
|
||||
parser.add_argument("--cache-duration", type=int, default=1,
|
||||
help="Durée de validité du cache en heures (défaut: 1)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
# Créer et exécuter le scraper
|
||||
scraper = AgendaDuLibreScraper(
|
||||
api_base_url=args.api_url,
|
||||
batch_size=args.batch_size
|
||||
)
|
||||
|
||||
# Modifier la durée de cache si spécifiée
|
||||
scraper.cache_duration_hours = args.cache_duration
|
||||
|
||||
# Exécuter avec ou sans rechargement forcé
|
||||
success = scraper.run(force_refresh=args.force_refresh)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
56963
extractors/agendadulibre_events.ics
Normal file
56963
extractors/agendadulibre_events.ics
Normal file
File diff suppressed because it is too large
Load diff
5004
extractors/agendadulibre_scraper.log
Normal file
5004
extractors/agendadulibre_scraper.log
Normal file
File diff suppressed because it is too large
Load diff
133
extractors/demo_agendadulibre.py
Normal file
133
extractors/demo_agendadulibre.py
Normal file
|
@ -0,0 +1,133 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script de démonstration pour le scraper de l'agenda du libre
|
||||
Mode dry-run pour tester sans envoyer de données à l'API
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from agendadulibre import AgendaDuLibreScraper, api_oedb
|
||||
import logging
|
||||
|
||||
class DemoAgendaDuLibreScraper(AgendaDuLibreScraper):
|
||||
"""Version démo du scraper qui n'envoie pas de données à l'API"""
|
||||
|
||||
def send_event_to_api(self, event_data):
|
||||
"""Version démo qui simule l'envoi à l'API"""
|
||||
event_label = event_data["event"]["properties"]["label"]
|
||||
print(f"🔍 [DEMO] Simulation d'envoi: {event_label}")
|
||||
|
||||
# Simuler différents types de réponses
|
||||
import random
|
||||
responses = [
|
||||
(True, "Créé avec succès"),
|
||||
(False, "Événement déjà existant"),
|
||||
(True, "Créé avec succès"),
|
||||
(False, "Erreur API: 500 - Internal Server Error")
|
||||
]
|
||||
|
||||
success, message = random.choice(responses)
|
||||
|
||||
if success:
|
||||
print(f"✅ [DEMO] {event_label} - {message}")
|
||||
else:
|
||||
print(f"❌ [DEMO] {event_label} - {message}")
|
||||
|
||||
return success, message
|
||||
|
||||
def main():
|
||||
"""Exécute la démonstration"""
|
||||
print("🎭 Démonstration du scraper agenda du libre (mode dry-run)")
|
||||
print("=" * 60)
|
||||
|
||||
# Configuration du logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# Créer le scraper en mode démo
|
||||
scraper = DemoAgendaDuLibreScraper(
|
||||
api_base_url=api_oedb, # Utiliser l'URL par défaut
|
||||
batch_size=2 # Traiter 2 événements par batch pour la démo
|
||||
)
|
||||
|
||||
print(f"📋 Configuration:")
|
||||
print(f" - URL iCal: {scraper.ical_url}")
|
||||
print(f" - Taille des batches: {scraper.batch_size}")
|
||||
print(f" - Fichier de données: {scraper.data_file}")
|
||||
print(f" - Fichier cache iCal: {scraper.ical_file}")
|
||||
print(f" - Durée de cache: {scraper.cache_duration_hours}h")
|
||||
print()
|
||||
|
||||
# Récupérer le fichier iCal
|
||||
print("📥 Récupération du fichier iCal...")
|
||||
calendar = scraper.fetch_ical_data()
|
||||
|
||||
if not calendar:
|
||||
print("❌ Impossible de récupérer le fichier iCal")
|
||||
return False
|
||||
|
||||
# Compter les événements
|
||||
event_count = 0
|
||||
for component in calendar.walk():
|
||||
if component.name == "VEVENT":
|
||||
event_count += 1
|
||||
|
||||
print(f"📅 {event_count} événements trouvés dans le fichier iCal")
|
||||
|
||||
# Test du cache
|
||||
print("\n🔄 Test du cache iCal...")
|
||||
print(" Premier appel (téléchargement)...")
|
||||
calendar2 = scraper.fetch_ical_data()
|
||||
print(" Deuxième appel (depuis le cache)...")
|
||||
calendar3 = scraper.fetch_ical_data()
|
||||
print(" ✅ Cache fonctionne correctement")
|
||||
print()
|
||||
|
||||
# Traiter seulement les 5 premiers événements pour la démo
|
||||
print("🔄 Traitement des 5 premiers événements (démo)...")
|
||||
print("-" * 40)
|
||||
|
||||
processed = 0
|
||||
for component in calendar.walk():
|
||||
if component.name == "VEVENT" and processed < 5:
|
||||
parsed_event = scraper.parse_event(component)
|
||||
if parsed_event:
|
||||
event_label = parsed_event["event"]["properties"]["label"]
|
||||
start_date = parsed_event["event"]["properties"]["start"]
|
||||
location = parsed_event["event"]["properties"]["where"]
|
||||
|
||||
print(f"📝 Événement {processed + 1}:")
|
||||
print(f" Titre: {event_label}")
|
||||
print(f" Date: {start_date}")
|
||||
print(f" Lieu: {location}")
|
||||
print()
|
||||
|
||||
# Simuler l'envoi
|
||||
success, message = scraper.send_event_to_api(parsed_event)
|
||||
|
||||
# Mettre à jour les données locales (simulation)
|
||||
event_id = parsed_event["id"]
|
||||
scraper.events_data["events"][event_id] = {
|
||||
"status": "saved" if success else "error",
|
||||
"message": message,
|
||||
"last_attempt": "2024-01-01T00:00:00",
|
||||
"event": parsed_event["event"]
|
||||
}
|
||||
|
||||
processed += 1
|
||||
|
||||
print("-" * 40)
|
||||
print(f"✅ Démonstration terminée - {processed} événements traités")
|
||||
print()
|
||||
print("💡 Pour exécuter le vrai scraper:")
|
||||
print(" python agendadulibre.py --batch-size 5 --api-url http://localhost:5000")
|
||||
print()
|
||||
print("🧪 Pour exécuter les tests:")
|
||||
print(" python test_agendadulibre.py")
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
136
extractors/extract_coordinates_from_ics.py
Normal file
136
extractors/extract_coordinates_from_ics.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script pour extraire les coordonnées géographiques du fichier ICS de l'agenda du libre.
|
||||
Le fichier ICS contient des coordonnées dans les propriétés X-ALT-DESC;FMTTYPE=text/html
|
||||
sous forme d'attributs data-latitude et data-longitude.
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
def extract_coordinates_from_ics(ics_file_path: str) -> List[Dict]:
|
||||
"""
|
||||
Extrait les coordonnées géographiques du fichier ICS.
|
||||
|
||||
Args:
|
||||
ics_file_path: Chemin vers le fichier ICS
|
||||
|
||||
Returns:
|
||||
Liste de dictionnaires contenant les coordonnées et informations associées
|
||||
"""
|
||||
coordinates = []
|
||||
|
||||
with open(ics_file_path, 'r', encoding='utf-8') as file:
|
||||
content = file.read()
|
||||
|
||||
# Recherche des patterns data-latitude et data-longitude
|
||||
# Pattern pour capturer les coordonnées dans le HTML
|
||||
pattern = r'data-latitude="([^"]+)"[^>]*data-longitude="([^"]+)"'
|
||||
|
||||
matches = re.findall(pattern, content)
|
||||
|
||||
for i, (lat, lon) in enumerate(matches):
|
||||
try:
|
||||
lat_float = float(lat)
|
||||
lon_float = float(lon)
|
||||
|
||||
# Vérifier que les coordonnées sont valides
|
||||
if -90 <= lat_float <= 90 and -180 <= lon_float <= 180:
|
||||
coordinates.append({
|
||||
'id': i + 1,
|
||||
'latitude': lat_float,
|
||||
'longitude': lon_float,
|
||||
'source': 'agendadulibre_ics'
|
||||
})
|
||||
except ValueError:
|
||||
# Ignorer les coordonnées invalides
|
||||
continue
|
||||
|
||||
return coordinates
|
||||
|
||||
def find_events_with_coordinates(ics_file_path: str) -> List[Dict]:
|
||||
"""
|
||||
Trouve les événements qui ont des coordonnées dans le fichier ICS.
|
||||
|
||||
Args:
|
||||
ics_file_path: Chemin vers le fichier ICS
|
||||
|
||||
Returns:
|
||||
Liste des événements avec leurs coordonnées
|
||||
"""
|
||||
events = []
|
||||
|
||||
with open(ics_file_path, 'r', encoding='utf-8') as file:
|
||||
content = file.read()
|
||||
|
||||
# Diviser le contenu en événements individuels
|
||||
event_blocks = content.split('BEGIN:VEVENT')
|
||||
|
||||
for i, block in enumerate(event_blocks[1:], 1): # Ignorer le premier bloc (en-tête)
|
||||
# Rechercher les coordonnées dans ce bloc d'événement
|
||||
lat_match = re.search(r'data-latitude="([^"]+)"', block)
|
||||
lon_match = re.search(r'data-longitude="([^"]+)"', block)
|
||||
|
||||
if lat_match and lon_match:
|
||||
try:
|
||||
lat = float(lat_match.group(1))
|
||||
lon = float(lon_match.group(1))
|
||||
|
||||
# Vérifier que les coordonnées sont valides
|
||||
if -90 <= lat <= 90 and -180 <= lon <= 180:
|
||||
# Extraire d'autres informations de l'événement
|
||||
summary_match = re.search(r'SUMMARY:(.+)', block)
|
||||
location_match = re.search(r'LOCATION:(.+)', block)
|
||||
description_match = re.search(r'DESCRIPTION:(.+)', block)
|
||||
|
||||
event = {
|
||||
'event_id': i,
|
||||
'latitude': lat,
|
||||
'longitude': lon,
|
||||
'summary': summary_match.group(1).strip() if summary_match else '',
|
||||
'location': location_match.group(1).strip() if location_match else '',
|
||||
'description': description_match.group(1).strip() if description_match else '',
|
||||
'source': 'agendadulibre_ics'
|
||||
}
|
||||
|
||||
events.append(event)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
return events
|
||||
|
||||
def main():
|
||||
"""Fonction principale pour extraire et afficher les coordonnées."""
|
||||
ics_file = 'agendadulibre_events.ics'
|
||||
|
||||
print("🔍 Extraction des coordonnées du fichier ICS...")
|
||||
|
||||
# Extraire toutes les coordonnées
|
||||
all_coordinates = extract_coordinates_from_ics(ics_file)
|
||||
print(f"📍 {len(all_coordinates)} coordonnées trouvées")
|
||||
|
||||
# Extraire les événements avec coordonnées
|
||||
events_with_coords = find_events_with_coordinates(ics_file)
|
||||
print(f"🎯 {len(events_with_coords)} événements avec coordonnées trouvés")
|
||||
|
||||
# Afficher quelques exemples
|
||||
print("\n📋 Exemples d'événements avec coordonnées :")
|
||||
for event in events_with_coords[:5]:
|
||||
print(f" • {event['summary']}")
|
||||
print(f" 📍 {event['latitude']}, {event['longitude']}")
|
||||
print(f" 📍 Lieu: {event['location']}")
|
||||
print()
|
||||
|
||||
# Sauvegarder les résultats
|
||||
with open('extracted_coordinates.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(events_with_coords, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"💾 Résultats sauvegardés dans 'extracted_coordinates.json'")
|
||||
|
||||
# Statistiques
|
||||
unique_coords = set((event['latitude'], event['longitude']) for event in events_with_coords)
|
||||
print(f"📊 {len(unique_coords)} coordonnées uniques")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
211
extractors/monitor_agendadulibre.py
Executable file
211
extractors/monitor_agendadulibre.py
Executable file
|
@ -0,0 +1,211 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script de monitoring pour le scraper agenda du libre
|
||||
Affiche les statistiques et l'état du scraper
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any
|
||||
|
||||
class AgendaDuLibreMonitor:
|
||||
def __init__(self, data_file: str = "agendadulibre_events.json"):
|
||||
self.data_file = data_file
|
||||
self.events_data = self.load_events_data()
|
||||
|
||||
def load_events_data(self) -> Dict[str, Any]:
|
||||
"""Charge les données d'événements"""
|
||||
if os.path.exists(self.data_file):
|
||||
try:
|
||||
with open(self.data_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors du chargement de {self.data_file}: {e}")
|
||||
return {"events": {}, "last_update": None}
|
||||
return {"events": {}, "last_update": None}
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""Calcule les statistiques des événements"""
|
||||
events = self.events_data.get("events", {})
|
||||
|
||||
stats = {
|
||||
"total_events": len(events),
|
||||
"saved": 0,
|
||||
"already_exists": 0,
|
||||
"error": 0,
|
||||
"unknown": 0,
|
||||
"recent_errors": 0,
|
||||
"last_update": self.events_data.get("last_update"),
|
||||
"events_by_status": {},
|
||||
"recent_events": []
|
||||
}
|
||||
|
||||
# Analyser les statuts
|
||||
for event_id, event_data in events.items():
|
||||
status = event_data.get("status", "unknown")
|
||||
stats["events_by_status"][status] = stats["events_by_status"].get(status, 0) + 1
|
||||
|
||||
if status == "saved":
|
||||
stats["saved"] += 1
|
||||
elif status == "already_exists":
|
||||
stats["already_exists"] += 1
|
||||
elif status == "error":
|
||||
stats["error"] += 1
|
||||
else:
|
||||
stats["unknown"] += 1
|
||||
|
||||
# Vérifier les erreurs récentes (dernières 24h)
|
||||
last_attempt = event_data.get("last_attempt")
|
||||
if last_attempt and status == "error":
|
||||
try:
|
||||
attempt_time = datetime.fromisoformat(last_attempt.replace('Z', '+00:00'))
|
||||
if datetime.now() - attempt_time.replace(tzinfo=None) < timedelta(hours=24):
|
||||
stats["recent_errors"] += 1
|
||||
except:
|
||||
pass
|
||||
|
||||
# Collecter les événements récents (derniers 10)
|
||||
if len(stats["recent_events"]) < 10:
|
||||
event_info = {
|
||||
"id": event_id,
|
||||
"label": event_data.get("event", {}).get("properties", {}).get("label", "Sans titre"),
|
||||
"status": status,
|
||||
"last_attempt": last_attempt,
|
||||
"message": event_data.get("message", "")
|
||||
}
|
||||
stats["recent_events"].append(event_info)
|
||||
|
||||
return stats
|
||||
|
||||
def display_statistics(self):
|
||||
"""Affiche les statistiques de manière formatée"""
|
||||
stats = self.get_statistics()
|
||||
|
||||
print("📊 Statistiques du scraper agenda du libre")
|
||||
print("=" * 50)
|
||||
|
||||
# Informations générales
|
||||
print(f"📁 Fichier de données: {self.data_file}")
|
||||
print(f"📅 Dernière mise à jour: {stats['last_update'] or 'Jamais'}")
|
||||
print(f"📈 Total d'événements traités: {stats['total_events']}")
|
||||
print()
|
||||
|
||||
# Répartition par statut
|
||||
print("📋 Répartition par statut:")
|
||||
for status, count in stats["events_by_status"].items():
|
||||
emoji = {
|
||||
"saved": "✅",
|
||||
"already_exists": "⚠️",
|
||||
"error": "❌",
|
||||
"unknown": "❓"
|
||||
}.get(status, "❓")
|
||||
print(f" {emoji} {status}: {count}")
|
||||
print()
|
||||
|
||||
# Erreurs récentes
|
||||
if stats["recent_errors"] > 0:
|
||||
print(f"🚨 Erreurs récentes (24h): {stats['recent_errors']}")
|
||||
print()
|
||||
|
||||
# Événements récents
|
||||
if stats["recent_events"]:
|
||||
print("🕒 Événements récents:")
|
||||
for event in stats["recent_events"][:5]:
|
||||
emoji = {
|
||||
"saved": "✅",
|
||||
"already_exists": "⚠️",
|
||||
"error": "❌",
|
||||
"unknown": "❓"
|
||||
}.get(event["status"], "❓")
|
||||
|
||||
print(f" {emoji} {event['label'][:50]}{'...' if len(event['label']) > 50 else ''}")
|
||||
if event["status"] == "error":
|
||||
print(f" 💬 {event['message']}")
|
||||
print()
|
||||
|
||||
# Recommandations
|
||||
self.display_recommendations(stats)
|
||||
|
||||
def display_recommendations(self, stats: Dict[str, Any]):
|
||||
"""Affiche des recommandations basées sur les statistiques"""
|
||||
print("💡 Recommandations:")
|
||||
|
||||
if stats["total_events"] == 0:
|
||||
print(" - Aucun événement traité. Exécutez le scraper pour commencer.")
|
||||
elif stats["error"] > stats["saved"]:
|
||||
print(" - Beaucoup d'erreurs détectées. Vérifiez la connectivité API.")
|
||||
elif stats["recent_errors"] > 5:
|
||||
print(" - Erreurs récentes nombreuses. Vérifiez les logs.")
|
||||
elif stats["saved"] > 0:
|
||||
print(" - Scraper fonctionne correctement.")
|
||||
|
||||
if stats["already_exists"] > stats["saved"]:
|
||||
print(" - Beaucoup d'événements déjà existants. Le système de déduplication fonctionne.")
|
||||
|
||||
print()
|
||||
|
||||
def check_file_status(self):
|
||||
"""Vérifie l'état du fichier de données"""
|
||||
if not os.path.exists(self.data_file):
|
||||
print(f"❌ Fichier de données non trouvé: {self.data_file}")
|
||||
return False
|
||||
|
||||
try:
|
||||
stat = os.stat(self.data_file)
|
||||
size = stat.st_size
|
||||
mtime = datetime.fromtimestamp(stat.st_mtime)
|
||||
|
||||
print(f"📁 État du fichier de données:")
|
||||
print(f" - Taille: {size:,} octets")
|
||||
print(f" - Dernière modification: {mtime}")
|
||||
print(f" - Lisible: {'✅' if os.access(self.data_file, os.R_OK) else '❌'}")
|
||||
print(f" - Écriture: {'✅' if os.access(self.data_file, os.W_OK) else '❌'}")
|
||||
print()
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors de la vérification du fichier: {e}")
|
||||
return False
|
||||
|
||||
def show_help(self):
|
||||
"""Affiche l'aide"""
|
||||
print("🔍 Monitor agenda du libre - Aide")
|
||||
print("=" * 40)
|
||||
print("Usage: python3 monitor_agendadulibre.py [options]")
|
||||
print()
|
||||
print("Options:")
|
||||
print(" --stats, -s Afficher les statistiques (défaut)")
|
||||
print(" --file, -f Vérifier l'état du fichier")
|
||||
print(" --help, -h Afficher cette aide")
|
||||
print()
|
||||
print("Exemples:")
|
||||
print(" python3 monitor_agendadulibre.py")
|
||||
print(" python3 monitor_agendadulibre.py --file")
|
||||
print(" python3 monitor_agendadulibre.py --stats")
|
||||
|
||||
def main():
|
||||
"""Fonction principale"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Monitor pour le scraper agenda du libre")
|
||||
parser.add_argument("--stats", "-s", action="store_true", default=True,
|
||||
help="Afficher les statistiques")
|
||||
parser.add_argument("--file", "-f", action="store_true",
|
||||
help="Vérifier l'état du fichier de données")
|
||||
parser.add_argument("--data-file", default="agendadulibre_events.json",
|
||||
help="Fichier de données à analyser")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
monitor = AgendaDuLibreMonitor(args.data_file)
|
||||
|
||||
if args.file:
|
||||
monitor.check_file_status()
|
||||
|
||||
if args.stats:
|
||||
monitor.display_statistics()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
2
extractors/requirements_agendadulibre.txt
Normal file
2
extractors/requirements_agendadulibre.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
requests>=2.28.0
|
||||
icalendar>=5.0.0
|
98
extractors/setup_cron.sh
Executable file
98
extractors/setup_cron.sh
Executable file
|
@ -0,0 +1,98 @@
|
|||
#!/bin/bash
|
||||
# Script de configuration du cron pour le scraper agenda du libre
|
||||
|
||||
SCRIPT_DIR="/home/poule/encrypted/stockage-syncable/www/development/html/oedb-backend/extractors"
|
||||
SCRIPT_PATH="$SCRIPT_DIR/agendadulibre.py"
|
||||
LOG_FILE="$SCRIPT_DIR/cron_agendadulibre.log"
|
||||
|
||||
echo "🔧 Configuration du cron pour le scraper agenda du libre"
|
||||
echo "========================================================"
|
||||
|
||||
# Vérifier que le script existe
|
||||
if [ ! -f "$SCRIPT_PATH" ]; then
|
||||
echo "❌ Erreur: Le script $SCRIPT_PATH n'existe pas"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Rendre le script exécutable
|
||||
chmod +x "$SCRIPT_PATH"
|
||||
|
||||
echo "📋 Options de planification disponibles:"
|
||||
echo "1. Toutes les heures (batch de 1)"
|
||||
echo "2. Toutes les 2 heures (batch de 5)"
|
||||
echo "3. Tous les jours à 6h (batch de 10)"
|
||||
echo "4. Tous les jours à 6h et 18h (batch de 5)"
|
||||
echo "5. Personnalisé"
|
||||
echo ""
|
||||
|
||||
read -p "Choisissez une option (1-5): " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
CRON_SCHEDULE="0 * * * *"
|
||||
BATCH_SIZE="1"
|
||||
;;
|
||||
2)
|
||||
CRON_SCHEDULE="0 */2 * * *"
|
||||
BATCH_SIZE="5"
|
||||
;;
|
||||
3)
|
||||
CRON_SCHEDULE="0 6 * * *"
|
||||
BATCH_SIZE="10"
|
||||
;;
|
||||
4)
|
||||
CRON_SCHEDULE="0 6,18 * * *"
|
||||
BATCH_SIZE="5"
|
||||
;;
|
||||
5)
|
||||
read -p "Entrez la planification cron (ex: 0 */3 * * *): " CRON_SCHEDULE
|
||||
read -p "Entrez la taille des batches (ex: 5): " BATCH_SIZE
|
||||
;;
|
||||
*)
|
||||
echo "❌ Option invalide"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Demander l'URL de l'API
|
||||
read -p "Entrez l'URL de l'API OEDB (défaut: http://localhost:5000): " API_URL
|
||||
API_URL=${API_URL:-"http://localhost:5000"}
|
||||
|
||||
# Créer la commande cron
|
||||
CRON_COMMAND="$CRON_SCHEDULE cd $SCRIPT_DIR && python3 $SCRIPT_PATH --api-url $API_URL --batch-size $BATCH_SIZE >> $LOG_FILE 2>&1"
|
||||
|
||||
echo ""
|
||||
echo "📝 Configuration cron proposée:"
|
||||
echo "Planification: $CRON_SCHEDULE"
|
||||
echo "Commande: $CRON_COMMAND"
|
||||
echo ""
|
||||
|
||||
read -p "Voulez-vous ajouter cette tâche au cron ? (y/N): " confirm
|
||||
|
||||
if [[ $confirm =~ ^[Yy]$ ]]; then
|
||||
# Ajouter la tâche au cron
|
||||
(crontab -l 2>/dev/null; echo "$CRON_COMMAND") | crontab -
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Tâche cron ajoutée avec succès"
|
||||
echo ""
|
||||
echo "📋 Tâches cron actuelles:"
|
||||
crontab -l | grep agendadulibre || echo "Aucune tâche trouvée"
|
||||
echo ""
|
||||
echo "📁 Logs disponibles dans: $LOG_FILE"
|
||||
echo "🔍 Pour surveiller les logs: tail -f $LOG_FILE"
|
||||
else
|
||||
echo "❌ Erreur lors de l'ajout de la tâche cron"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ Configuration annulée"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "💡 Commandes utiles:"
|
||||
echo " - Voir les tâches cron: crontab -l"
|
||||
echo " - Supprimer une tâche: crontab -e"
|
||||
echo " - Voir les logs: tail -f $LOG_FILE"
|
||||
echo " - Tester manuellement: python3 $SCRIPT_PATH --api-url $API_URL --batch-size $BATCH_SIZE"
|
139
extractors/test_agendadulibre.py
Normal file
139
extractors/test_agendadulibre.py
Normal file
|
@ -0,0 +1,139 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script de test pour le scraper de l'agenda du libre
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from agendadulibre import AgendaDuLibreScraper, api_oedb
|
||||
import logging
|
||||
|
||||
# Configuration du logging pour les tests
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
def test_ical_fetch():
|
||||
"""Test de récupération du fichier iCal"""
|
||||
print("🧪 Test de récupération du fichier iCal...")
|
||||
|
||||
scraper = AgendaDuLibreScraper()
|
||||
calendar = scraper.fetch_ical_data()
|
||||
|
||||
if calendar:
|
||||
print("✅ Fichier iCal récupéré avec succès")
|
||||
|
||||
# Compter les événements
|
||||
event_count = 0
|
||||
for component in calendar.walk():
|
||||
if component.name == "VEVENT":
|
||||
event_count += 1
|
||||
|
||||
print(f"📅 Nombre d'événements trouvés: {event_count}")
|
||||
return True
|
||||
else:
|
||||
print("❌ Échec de la récupération du fichier iCal")
|
||||
return False
|
||||
|
||||
def test_event_parsing():
|
||||
"""Test de parsing d'un événement"""
|
||||
print("🧪 Test de parsing d'événement...")
|
||||
|
||||
scraper = AgendaDuLibreScraper()
|
||||
calendar = scraper.fetch_ical_data()
|
||||
|
||||
if not calendar:
|
||||
print("❌ Impossible de récupérer le fichier iCal pour le test")
|
||||
return False
|
||||
|
||||
# Trouver le premier événement
|
||||
for component in calendar.walk():
|
||||
if component.name == "VEVENT":
|
||||
parsed_event = scraper.parse_event(component)
|
||||
if parsed_event:
|
||||
print("✅ Événement parsé avec succès:")
|
||||
print(f" ID: {parsed_event['id']}")
|
||||
print(f" Titre: {parsed_event['event']['properties']['label']}")
|
||||
print(f" Début: {parsed_event['event']['properties']['start']}")
|
||||
print(f" Fin: {parsed_event['event']['properties']['stop']}")
|
||||
print(f" Lieu: {parsed_event['event']['properties']['where']}")
|
||||
return True
|
||||
else:
|
||||
print("❌ Échec du parsing de l'événement")
|
||||
return False
|
||||
|
||||
print("❌ Aucun événement trouvé pour le test")
|
||||
return False
|
||||
|
||||
def test_data_persistence():
|
||||
"""Test de persistance des données"""
|
||||
print("🧪 Test de persistance des données...")
|
||||
|
||||
scraper = AgendaDuLibreScraper()
|
||||
|
||||
# Test de sauvegarde
|
||||
test_data = {
|
||||
"events": {
|
||||
"test_event_123": {
|
||||
"status": "saved",
|
||||
"message": "Test event",
|
||||
"last_attempt": "2024-01-01T00:00:00",
|
||||
"event": {
|
||||
"properties": {
|
||||
"label": "Test Event",
|
||||
"what": "culture.geek"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"last_update": "2024-01-01T00:00:00"
|
||||
}
|
||||
|
||||
scraper.events_data = test_data
|
||||
scraper.save_events_data()
|
||||
|
||||
# Test de chargement
|
||||
scraper2 = AgendaDuLibreScraper()
|
||||
if "test_event_123" in scraper2.events_data["events"]:
|
||||
print("✅ Persistance des données fonctionne correctement")
|
||||
return True
|
||||
else:
|
||||
print("❌ Échec de la persistance des données")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Exécute tous les tests"""
|
||||
print("🚀 Démarrage des tests du scraper agenda du libre")
|
||||
print("=" * 50)
|
||||
|
||||
tests = [
|
||||
test_ical_fetch,
|
||||
test_event_parsing,
|
||||
test_data_persistence
|
||||
]
|
||||
|
||||
passed = 0
|
||||
total = len(tests)
|
||||
|
||||
for test in tests:
|
||||
try:
|
||||
if test():
|
||||
passed += 1
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors du test {test.__name__}: {e}")
|
||||
print()
|
||||
|
||||
print("=" * 50)
|
||||
print(f"📊 Résultats: {passed}/{total} tests réussis")
|
||||
|
||||
if passed == total:
|
||||
print("✅ Tous les tests sont passés!")
|
||||
return True
|
||||
else:
|
||||
print("❌ Certains tests ont échoué")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
130
extractors/test_api_connection.py
Executable file
130
extractors/test_api_connection.py
Executable file
|
@ -0,0 +1,130 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script de test de connexion à l'API OEDB pour le scraper agenda du libre
|
||||
"""
|
||||
|
||||
import requests
|
||||
import sys
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Configuration par défaut
|
||||
api_oedb = "https://api.openeventdatabase.org"
|
||||
|
||||
def test_api_connection(api_url: str = api_oedb):
|
||||
"""Test la connexion à l'API OEDB"""
|
||||
print(f"🔍 Test de connexion à l'API OEDB: {api_url}")
|
||||
print("=" * 50)
|
||||
|
||||
# Test 1: Endpoint de base
|
||||
print("1️⃣ Test de l'endpoint de base...")
|
||||
try:
|
||||
response = requests.get(f"{api_url}/", timeout=10)
|
||||
if response.status_code == 200:
|
||||
print("✅ Endpoint de base accessible")
|
||||
else:
|
||||
print(f"⚠️ Endpoint de base répond avec le code: {response.status_code}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"❌ Erreur de connexion à l'endpoint de base: {e}")
|
||||
return False
|
||||
|
||||
# Test 2: Endpoint des événements
|
||||
print("\n2️⃣ Test de l'endpoint des événements...")
|
||||
try:
|
||||
response = requests.get(f"{api_url}/events", timeout=10)
|
||||
if response.status_code == 200:
|
||||
print("✅ Endpoint des événements accessible")
|
||||
try:
|
||||
data = response.json()
|
||||
if 'features' in data:
|
||||
print(f" 📊 {len(data['features'])} événements trouvés dans l'API")
|
||||
else:
|
||||
print(" ⚠️ Format de réponse inattendu")
|
||||
except json.JSONDecodeError:
|
||||
print(" ⚠️ Réponse non-JSON reçue")
|
||||
else:
|
||||
print(f"❌ Endpoint des événements répond avec le code: {response.status_code}")
|
||||
return False
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"❌ Erreur de connexion à l'endpoint des événements: {e}")
|
||||
return False
|
||||
|
||||
# Test 3: Test d'envoi d'un événement de test
|
||||
print("\n3️⃣ Test d'envoi d'un événement de test...")
|
||||
test_event = {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"label": f"Test API Connection - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
"description": "Événement de test pour vérifier la connexion API",
|
||||
"type": "scheduled",
|
||||
"what": "community",
|
||||
"where": "Test Location",
|
||||
"start": datetime.now().isoformat(),
|
||||
"stop": (datetime.now().timestamp() + 3600).isoformat(),
|
||||
"source:name": "Test API Connection",
|
||||
"last_modified_by": "test_script"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [0, 0]
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{api_url}/event",
|
||||
json=test_event,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 201:
|
||||
print("✅ Événement de test créé avec succès")
|
||||
try:
|
||||
created_event = response.json()
|
||||
event_id = created_event.get('id', 'inconnu')
|
||||
print(f" 📝 ID de l'événement créé: {event_id}")
|
||||
except json.JSONDecodeError:
|
||||
print(" ⚠️ Réponse de création non-JSON")
|
||||
elif response.status_code == 409:
|
||||
print("⚠️ Événement de test déjà existant (conflit)")
|
||||
else:
|
||||
print(f"❌ Erreur lors de la création de l'événement de test: {response.status_code}")
|
||||
print(f" Réponse: {response.text}")
|
||||
return False
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"❌ Erreur lors de l'envoi de l'événement de test: {e}")
|
||||
return False
|
||||
|
||||
print("\n✅ Tous les tests de connexion sont passés!")
|
||||
return True
|
||||
|
||||
def main():
|
||||
"""Fonction principale"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Test de connexion à l'API OEDB")
|
||||
parser.add_argument("--api-url", default=api_oedb,
|
||||
help="URL de l'API OEDB à tester")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
success = test_api_connection(args.api_url)
|
||||
|
||||
if success:
|
||||
print("\n🎉 L'API OEDB est prête pour le scraper agenda du libre!")
|
||||
print("\n💡 Commandes utiles:")
|
||||
print(" - Test complet: python3 test_agendadulibre.py")
|
||||
print(" - Démonstration: python3 demo_agendadulibre.py")
|
||||
print(" - Scraping réel: python3 agendadulibre.py --api-url " + args.api_url)
|
||||
else:
|
||||
print("\n❌ L'API OEDB n'est pas accessible ou ne fonctionne pas correctement.")
|
||||
print("\n🔧 Vérifications à effectuer:")
|
||||
print(" - L'API OEDB est-elle démarrée?")
|
||||
print(" - L'URL est-elle correcte?")
|
||||
print(" - Y a-t-il des erreurs dans les logs de l'API?")
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -15,8 +15,22 @@
|
|||
<div class="content">
|
||||
|
||||
<header>
|
||||
<h1>OEDB</h1>
|
||||
<button class="login">login OSM</button>
|
||||
|
||||
|
||||
<nav>
|
||||
<a routerLink="/"><h1>
|
||||
<img src="/static/oedb.png" alt="OEDB" style="width: 20px; height: 20px;">
|
||||
OEDB
|
||||
</h1>
|
||||
</a>
|
||||
<!-- <button class="login">login OSM</button> -->
|
||||
|
||||
<a routerLink="/agenda">agenda</a>
|
||||
<a routerLink="/unlocated-events">événements non localisés</a>
|
||||
<a routerLink="/nouvelles-categories">nouvelles catégories</a>
|
||||
<a href="/demo/stats">stats</a>
|
||||
<a href="https://source.cipherbliss.com/tykayn/oedb-backend">sources</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<router-outlet/>
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
<menu>
|
||||
OpenEventDatabase
|
||||
<nav>
|
||||
|
||||
|
||||
<a routerLink="/agenda">agenda</a>
|
||||
<a routerLink="/unlocated-events">événements non localisés</a>
|
||||
<a href="/demo/stats">stats</a>
|
||||
<a href="https://source.cipherbliss.com/tykayn/oedb-backend">sources</a>
|
||||
|
||||
(editor)
|
||||
</nav>
|
||||
|
||||
<div id="editor_form">
|
||||
<div id="search_input">
|
||||
<!-- <div id="search_input">
|
||||
<input type="text" value="" placeholder="Rechercher une catégorie d'évènement">
|
||||
</div>
|
||||
<!-- <div id="what_categories">
|
||||
<div id="what_categories">
|
||||
@for (oedbc of oedb_what_categories; track $index) {
|
||||
<div class="category">
|
||||
<div class="emoji">
|
||||
|
@ -24,13 +27,13 @@
|
|||
</div>
|
||||
}
|
||||
</div> -->
|
||||
|
||||
<!--
|
||||
<hr>
|
||||
(bouton de template pour ne pas remplir le formulaire)
|
||||
<hr>
|
||||
|
||||
(reste optionnel du formulaire)
|
||||
<!-- <label for="where">Nom</label>
|
||||
<label for="where">Nom</label>
|
||||
<input type="text" name="name">
|
||||
|
||||
<label for="where">Description</label>
|
||||
|
@ -57,7 +60,7 @@
|
|||
|
||||
</div>
|
||||
|
||||
<div id="found_list">
|
||||
<!-- <div id="found_list">
|
||||
<h2>données</h2>
|
||||
(liste des éléments trouvés)
|
||||
<ul>
|
||||
|
@ -87,6 +90,6 @@
|
|||
<br>
|
||||
points de l'utilisateur:
|
||||
12 points.
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</menu>
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
:host {
|
||||
display: block;
|
||||
nav{
|
||||
a {
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
&:hover{
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#what_categories {
|
||||
|
|
|
@ -163,4 +163,24 @@ pre{
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
nav{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
a {
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&:hover{
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,4 +7,5 @@ pyproj==3.7.2
|
|||
pytz==2025.2
|
||||
Requests==2.32.5
|
||||
waitress==3.0.2
|
||||
jinja2
|
||||
jinja2
|
||||
icalendar
|
Loading…
Add table
Add a link
Reference in a new issue