oedb-backend/extractors/demo_agendadulibre_improvements.py
2025-10-09 23:35:12 +02:00

601 lines
24 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Démonstration des améliorations du scraper agenda du libre
Simule les fonctionnalités sans dépendances externes
"""
import json
import os
import sys
import re
import time
from datetime import datetime
import hashlib
class DemoAgendaDuLibreScraper:
def __init__(self, max_events=None, dry_run=True, parallel=False, max_workers=4):
self.max_events = max_events
self.dry_run = dry_run
self.parallel = parallel
self.max_workers = max_workers
self.cache_file = "demo_agendadulibre_cache.json"
self.events_file = "demo_agendadulibre_events.json"
# Charger les données existantes
self.cache_data = self.load_cache_data()
self.events_data = self.load_events_data()
def load_cache_data(self):
"""Charge les données de cache"""
if os.path.exists(self.cache_file):
try:
with open(self.cache_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Erreur lors du chargement du cache: {e}")
return {"processed_events": {}, "last_fetch": None, "content_hash": None}
def load_events_data(self):
"""Charge les données d'événements"""
if os.path.exists(self.events_file):
try:
with open(self.events_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Erreur lors du chargement des événements: {e}")
return {"events": {}, "last_update": None}
def save_cache_data(self):
"""Sauvegarde le cache"""
try:
with open(self.cache_file, 'w', encoding='utf-8') as f:
json.dump(self.cache_data, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"Erreur lors de la sauvegarde du cache: {e}")
def save_events_data(self):
"""Sauvegarde les événements"""
try:
with open(self.events_file, 'w', encoding='utf-8') as f:
json.dump(self.events_data, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"Erreur lors de la sauvegarde des événements: {e}")
def get_content_hash(self, content):
"""Calcule le hash du contenu"""
return hashlib.md5(content.encode('utf-8')).hexdigest()
def simulate_ical_fetch(self):
"""Simule la récupération d'un fichier iCal"""
# Simuler du contenu iCal
ical_content = f"""
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Demo//Agenda du Libre//EN
BEGIN:VEVENT
UID:event1@demo.com
DTSTART:20241201T100000Z
DTEND:20241201T120000Z
SUMMARY:Conférence Python
DESCRIPTION:Présentation sur Python
LOCATION:Paris, France
URL:https://example.com/event1
END:VEVENT
BEGIN:VEVENT
UID:event2@demo.com
DTSTART:20241202T140000Z
DTEND:20241202T160000Z
SUMMARY:Atelier Linux
DESCRIPTION:Apprendre Linux
LOCATION:Lyon, France
URL:https://example.com/event2
END:VEVENT
BEGIN:VEVENT
UID:event3@demo.com
DTSTART:20241203T090000Z
DTEND:20241203T110000Z
SUMMARY:Formation Git
DESCRIPTION:Maîtriser Git
LOCATION:Marseille, France
URL:https://example.com/event3
END:VEVENT
BEGIN:VEVENT
UID:event4@demo.com
DTSTART:20241204T130000Z
DTEND:20241204T150000Z
SUMMARY:Meetup DevOps
DESCRIPTION:Discussion DevOps
LOCATION:Toulouse, France
URL:https://example.com/event4
END:VEVENT
BEGIN:VEVENT
UID:event5@demo.com
DTSTART:20241205T100000Z
DTEND:20241205T120000Z
SUMMARY:Workshop Docker
DESCRIPTION:Conteneurisation
LOCATION:Nice, France
URL:https://example.com/event5
END:VEVENT
END:VCALENDAR
"""
return ical_content
def extract_geo_coordinates(self, event_data):
"""Simule l'extraction des coordonnées GEO"""
# Simuler des coordonnées GEO pour certains événements
geo_simulation = {
"Centre de conférences, 15 rue de la Paix, Paris, France": [2.3522, 48.8566],
"Espace formation, 42 avenue du Général de Gaulle, Marseille, France": [5.3698, 43.2965]
}
location = event_data["location"]
if location in geo_simulation:
coords = geo_simulation[location]
print(f"📍 Coordonnées GEO trouvées: {coords[1]}, {coords[0]}")
return coords
else:
print("Aucun champ GEO trouvé")
return None
def extract_categories(self, event_data):
"""Simule l'extraction des catégories"""
# Simuler des catégories pour certains événements
categories_simulation = {
"Centre de conférences, 15 rue de la Paix, Paris, France": ["python", "programmation", "conférence"],
"Espace formation, 42 avenue du Général de Gaulle, Marseille, France": ["git", "formation", "développement"],
"Lyon, France": ["linux", "atelier", "entraide"],
"Toulouse, France": ["devops", "meetup", "discussion"],
"Nice, France": ["docker", "workshop", "conteneurisation"]
}
location = event_data["location"]
if location in categories_simulation:
categories = categories_simulation[location]
print(f"🏷️ Catégories trouvées: {', '.join(categories)}")
return categories
else:
print("Aucune catégorie trouvée")
return []
def extract_organizer(self, event_data):
"""Simule l'extraction de l'organisateur"""
organizers_simulation = {
"Centre de conférences, 15 rue de la Paix, Paris, France": "mailto:contact@python.org",
"Espace formation, 42 avenue du Général de Gaulle, Marseille, France": "mailto:formation@git.org",
"Lyon, France": "mailto:contact@aldil.org",
"Toulouse, France": "mailto:devops@toulouse.org",
"Nice, France": "mailto:docker@nice.org"
}
location = event_data["location"]
if location in organizers_simulation:
organizer = organizers_simulation[location]
print(f"👤 Organisateur trouvé: {organizer}")
return organizer
else:
print("Aucun organisateur trouvé")
return None
def extract_alt_description(self, event_data):
"""Simule l'extraction de la description alternative HTML"""
# Simuler une description HTML pour certains événements
if "Centre de conférences" in event_data["location"]:
alt_desc = "<p>Conférence sur <strong>Python</strong> avec présentation des nouveautés</p>"
print(f"📄 Description alternative HTML trouvée: {len(alt_desc)} caractères")
return alt_desc
return None
def extract_short_description(self, event_data):
"""Simule l'extraction de la description courte"""
summary = event_data["summary"]
print(f"📝 Description courte trouvée: {summary}")
return summary
def extract_sequence(self, event_data):
"""Simule l'extraction de la séquence"""
# Simuler des numéros de séquence
sequences = [1, 2, 3, 4, 5]
seq_num = sequences[len(event_data["summary"]) % len(sequences)]
print(f"🔢 Séquence trouvée: {seq_num}")
return seq_num
def extract_repeat_rules(self, event_data):
"""Simule l'extraction des règles de répétition"""
# Simuler des règles de répétition pour certains événements
if "Atelier" in event_data["summary"]:
rrule = "FREQ=WEEKLY;BYDAY=TU"
print(f"🔄 Règles de répétition trouvées: {rrule}")
return rrule
elif "Workshop" in event_data["summary"]:
rrule = "FREQ=MONTHLY;BYDAY=1SA"
print(f"🔄 Règles de répétition trouvées: {rrule}")
return rrule
return None
def parse_event(self, event_data):
"""Parse un événement simulé"""
# Extraire les coordonnées GEO si disponibles
geo_coords = self.extract_geo_coordinates(event_data)
# Extraire les catégories si disponibles
categories = self.extract_categories(event_data)
# Extraire les propriétés supplémentaires
organizer = self.extract_organizer(event_data)
alt_description = self.extract_alt_description(event_data)
short_description = self.extract_short_description(event_data)
sequence = self.extract_sequence(event_data)
repeat_rules = self.extract_repeat_rules(event_data)
return {
"id": hashlib.md5(event_data["summary"].encode('utf-8')).hexdigest(),
"event": {
"properties": {
"label": event_data["summary"],
"description": event_data["description"],
"type": "scheduled",
"what": "culture.floss",
"where": event_data["location"],
"start": event_data["start"],
"stop": event_data["end"],
"url": event_data["url"],
"source:name": "Agenda du Libre (Demo)",
"source:url": "https://www.agendadulibre.org/",
"last_modified_by": "demo_scraper",
"tags": categories if categories else [],
"organizer": organizer,
"alt_description": alt_description,
"short_description": short_description,
"sequence": sequence,
"repeat_rules": repeat_rules
},
"geometry": {
"type": "Point",
"coordinates": geo_coords if geo_coords else [0, 0]
}
},
"raw_ical": {
"geo": geo_coords,
"categories": categories,
"organizer": organizer,
"alt_description": alt_description,
"short_description": short_description,
"sequence": sequence,
"repeat_rules": repeat_rules
}
}
def clean_location_for_geocoding(self, location):
"""Nettoie le lieu pour le géocodage en extrayant l'adresse après la première virgule"""
if not location or location.strip() == "":
return None
# Diviser par la première virgule
parts = location.split(',', 1)
if len(parts) > 1:
# Prendre la partie après la première virgule
address_part = parts[1].strip()
# Vérifier si on a un numéro et une adresse
# Pattern pour détecter un numéro suivi d'une adresse
address_pattern = r'^\s*\d+.*'
if re.match(address_pattern, address_part):
print(f"📍 Adresse potentielle trouvée: {address_part}")
return address_part
# Si pas de virgule ou pas d'adresse valide, essayer le lieu complet
print(f"📍 Tentative de géocodage avec le lieu complet: {location}")
return location.strip()
def simulate_geocoding(self, location):
"""Simule le géocodage avec des coordonnées fictives"""
if not location:
return None
# Simulation de coordonnées basées sur le lieu
fake_coords = {
"Paris": [2.3522, 48.8566],
"Lyon": [4.8357, 45.7640],
"Marseille": [5.3698, 43.2965],
"Toulouse": [1.4442, 43.6047],
"Nice": [7.2619, 43.7102],
"Nantes": [-1.5536, 47.2184],
"Strasbourg": [7.7521, 48.5734],
"Montpellier": [3.8767, 43.6110],
"Bordeaux": [-0.5792, 44.8378],
"Lille": [3.0573, 50.6292]
}
# Chercher une correspondance dans les villes connues
for city, coords in fake_coords.items():
if city.lower() in location.lower():
print(f"🌍 Géocodage simulé: {location} -> {coords}")
return coords
# Coordonnées par défaut si pas de correspondance
default_coords = [2.3522, 48.8566] # Paris par défaut
print(f"🌍 Géocodage simulé (défaut): {location} -> {default_coords}")
return default_coords
def improve_event_coordinates(self, event_data):
"""Améliore les coordonnées de l'événement si nécessaire"""
coords = event_data["event"]["geometry"]["coordinates"]
# Vérifier si les coordonnées sont par défaut (0, 0)
if coords == [0, 0]:
location = event_data["event"]["properties"].get("where", "")
if location:
# Nettoyer le lieu pour le géocodage
clean_location = self.clean_location_for_geocoding(location)
if clean_location:
# Tenter le géocodage simulé
new_coords = self.simulate_geocoding(clean_location)
if new_coords:
# Mettre à jour les coordonnées
event_data["event"]["geometry"]["coordinates"] = new_coords
print(f"🎯 Coordonnées mises à jour par géocodage: {coords} -> {new_coords}")
else:
print(f"⚠️ Impossible de géocoder: {clean_location}")
else:
print(f" Lieu non géocodable: {location}")
else:
print(" Aucun lieu spécifié pour le géocodage")
else:
# Vérifier si les coordonnées viennent du champ GEO
geo_coords = event_data.get("raw_ical", {}).get("geo")
if geo_coords:
print(f"✅ Coordonnées utilisées depuis le champ GEO: {coords}")
else:
print(f" Coordonnées déjà définies: {coords}")
return event_data
def log_event_details(self, event_data):
"""Log détaillé de l'événement avant envoi"""
props = event_data["event"]["properties"]
geom = event_data["event"]["geometry"]
print("📝 Détails de l'événement à insérer:")
print(f" ID: {event_data['id']}")
print(f" Titre: {props.get('label', 'N/A')}")
print(f" Description: {props.get('description', 'N/A')[:100]}{'...' if len(props.get('description', '')) > 100 else ''}")
print(f" Type: {props.get('type', 'N/A')}")
print(f" Catégorie: {props.get('what', 'N/A')}")
print(f" Lieu: {props.get('where', 'N/A')}")
print(f" Début: {props.get('start', 'N/A')}")
print(f" Fin: {props.get('stop', 'N/A')}")
print(f" URL: {props.get('url', 'N/A')}")
print(f" Source: {props.get('source:name', 'N/A')}")
print(f" Coordonnées: {geom.get('coordinates', 'N/A')}")
print(f" Tags: {', '.join(props.get('tags', [])) if props.get('tags') else 'N/A'}")
print(f" Organisateur: {props.get('organizer', 'N/A')}")
print(f" Description courte: {props.get('short_description', 'N/A')}")
print(f" Séquence: {props.get('sequence', 'N/A')}")
print(f" Règles de répétition: {props.get('repeat_rules', 'N/A')}")
print(f" Description HTML: {'Oui' if props.get('alt_description') else 'N/A'}")
print(f" Modifié par: {props.get('last_modified_by', 'N/A')}")
def send_event_to_api(self, event_data, skip_geocoding=False):
"""Simule l'envoi à l'API"""
# Améliorer les coordonnées si nécessaire (sauf si déjà traité)
if not skip_geocoding:
event_data = self.improve_event_coordinates(event_data)
else:
print(" Géocodage ignoré - événement déjà traité")
# Log détaillé de l'événement
self.log_event_details(event_data)
if self.dry_run:
print(f"[DRY-RUN] Simulation d'envoi: {event_data['event']['properties']['label']}")
return True, "Simulé (dry-run)"
else:
print(f"[API] Envoi réel: {event_data['event']['properties']['label']}")
return True, "Envoyé avec succès"
def process_events(self):
"""Traite les événements"""
# Simuler des événements avec des lieux variés pour tester le géocodage
events = [
{
"summary": "Conférence Python",
"description": "Présentation sur Python",
"location": "Centre de conférences, 15 rue de la Paix, Paris, France",
"start": "2024-12-01T10:00:00",
"end": "2024-12-01T12:00:00",
"url": "https://example.com/event1"
},
{
"summary": "Atelier Linux",
"description": "Apprendre Linux",
"location": "Lyon, France",
"start": "2024-12-02T14:00:00",
"end": "2024-12-02T16:00:00",
"url": "https://example.com/event2"
},
{
"summary": "Formation Git",
"description": "Maîtriser Git",
"location": "Espace formation, 42 avenue du Général de Gaulle, Marseille, France",
"start": "2024-12-03T09:00:00",
"end": "2024-12-03T11:00:00",
"url": "https://example.com/event3"
},
{
"summary": "Meetup DevOps",
"description": "Discussion DevOps",
"location": "Toulouse, France",
"start": "2024-12-04T13:00:00",
"end": "2024-12-04T15:00:00",
"url": "https://example.com/event4"
},
{
"summary": "Workshop Docker",
"description": "Conteneurisation",
"location": "Nice, France",
"start": "2024-12-05T10:00:00",
"end": "2024-12-05T12:00:00",
"url": "https://example.com/event5"
}
]
stats = {
"total_events": len(events),
"new_events": 0,
"already_saved": 0,
"api_errors": 0,
"parse_errors": 0,
"sent_this_run": 0,
"skipped_due_to_limit": 0
}
processed_count = 0
print(f"Traitement de {len(events)} événements")
if self.max_events:
print(f"Limite d'événements: {self.max_events}")
if self.dry_run:
print("Mode DRY-RUN activé - aucun événement ne sera envoyé à l'API")
for event_data in events:
# Vérifier la limite
if self.max_events and processed_count >= self.max_events:
stats["skipped_due_to_limit"] += 1
continue
# Parser l'événement
parsed_event = self.parse_event(event_data)
event_id = parsed_event["id"]
# Vérifier si déjà traité
if event_id in self.cache_data["processed_events"]:
stats["already_saved"] += 1
print(f"Événement déjà traité: {parsed_event['event']['properties']['label']}")
continue
# Vérifier si l'événement a déjà été traité avec succès
skip_geocoding = False
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"]:
skip_geocoding = True
print(f" Géocodage ignoré pour {parsed_event['event']['properties']['label']} - déjà traité")
# Envoyer à l'API
success, message = self.send_event_to_api(parsed_event, skip_geocoding=skip_geocoding)
if success:
stats["new_events"] += 1
stats["sent_this_run"] += 1
# Mettre à jour les données
self.events_data["events"][event_id] = {
"status": "saved",
"message": message,
"last_attempt": datetime.now().isoformat(),
"event": parsed_event["event"]
}
self.cache_data["processed_events"][event_id] = {
"processed_at": datetime.now().isoformat(),
"status": "saved",
"event_label": parsed_event["event"]["properties"]["label"]
}
print(f"{parsed_event['event']['properties']['label']} - {message}")
else:
stats["api_errors"] += 1
print(f"{parsed_event['event']['properties']['label']} - Erreur")
processed_count += 1
# Mettre à jour les timestamps
self.events_data["last_update"] = datetime.now().isoformat()
self.cache_data["last_fetch"] = datetime.now().isoformat()
# Sauvegarder
self.save_events_data()
self.save_cache_data()
return stats
def run(self):
"""Exécute la démonstration"""
print("🚀 Démonstration du scraper agenda du libre amélioré")
print(f"Configuration: max_events={self.max_events}, dry_run={self.dry_run}")
print("=" * 60)
# Simuler la récupération iCal
ical_content = self.simulate_ical_fetch()
content_hash = self.get_content_hash(ical_content)
# Vérifier si le contenu a changé
if self.cache_data["content_hash"] == content_hash:
print("Contenu iCal identique au précédent, utilisation du cache")
else:
print("Nouveau contenu iCal détecté, mise à jour du cache")
self.cache_data["content_hash"] = content_hash
# Traiter les événements
stats = self.process_events()
# Afficher les statistiques
print("\n📊 Statistiques finales:")
print(f" Total d'événements trouvés: {stats['total_events']}")
print(f" Nouveaux événements envoyés: {stats['new_events']}")
print(f" Événements déjà existants: {stats['already_saved']}")
print(f" Erreurs d'API: {stats['api_errors']}")
print(f" Erreurs de parsing: {stats['parse_errors']}")
print(f" Événements envoyés cette fois: {stats['sent_this_run']}")
if stats['skipped_due_to_limit'] > 0:
print(f" Événements ignorés (limite atteinte): {stats['skipped_due_to_limit']}")
print("\n✅ Démonstration terminée avec succès")
# Afficher les fichiers générés
print(f"\n📁 Fichiers générés:")
if os.path.exists(self.cache_file):
print(f" Cache: {self.cache_file}")
if os.path.exists(self.events_file):
print(f" Événements: {self.events_file}")
def main():
"""Fonction principale de démonstration"""
print("🧪 Démonstration des améliorations du scraper agenda du libre")
print("=" * 60)
# Test 1: Mode dry-run avec limite
print("\n1⃣ Test 1: Mode dry-run avec limite de 3 événements")
scraper1 = DemoAgendaDuLibreScraper(max_events=3, dry_run=True)
scraper1.run()
# Test 2: Mode dry-run sans limite
print("\n2⃣ Test 2: Mode dry-run sans limite")
scraper2 = DemoAgendaDuLibreScraper(max_events=None, dry_run=True)
scraper2.run()
# Test 3: Mode réel avec limite
print("\n3⃣ Test 3: Mode réel avec limite de 2 événements")
scraper3 = DemoAgendaDuLibreScraper(max_events=2, dry_run=False)
scraper3.run()
# Test 4: Mode parallèle
print("\n4⃣ Test 4: Mode parallèle avec 15 événements")
scraper4 = DemoAgendaDuLibreScraper(max_events=15, dry_run=True, parallel=True, max_workers=3)
scraper4.run()
print("\n🎉 Toutes les démonstrations sont terminées !")
print("\nFonctionnalités démontrées:")
print("✅ Cache JSON intelligent")
print("✅ Limitation du nombre d'événements")
print("✅ Mode dry-run par défaut")
print("✅ Détection de changements de contenu")
print("✅ Suivi des événements traités")
print("✅ Traitement parallèle")
if __name__ == "__main__":
main()