#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Script pour générer des statistiques détaillées sur les blogs et créer des pages HTML avec graphiques pour chaque blog. """ import os import re import argparse import shutil from datetime import datetime from collections import defaultdict import locale try: import matplotlib matplotlib.use('Agg') # Backend non-interactif import matplotlib.pyplot as plt import matplotlib.dates as mdates HAS_MATPLOTLIB = True except ImportError: HAS_MATPLOTLIB = False print("Attention: matplotlib n'est pas installé. Les graphiques ne seront pas générés.") from jinja2 import Environment, FileSystemLoader # Importer les fonctions utilitaires from utils.utils import ( find_year_and_slug_on_filename, get_blog_template_conf, find_extract_in_content_org, format_date_str ) # Vitesse de lecture en mots par minute LECTURE_MOTS_PAR_MINUTE = 220 def extraire_date_du_contenu(content): """ Extrait la date de publication depuis les métadonnées du fichier org. Recherche #+CREATED ou #+post_date_published """ # Chercher #+CREATED match = re.search(r'#\+CREATED:\s*(.+)', content) if match: date_str = match.group(1).strip() # Formats possibles: YYYY-MM-DD HH:MM:SS, YYYY-MM-DD try: return datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S') except ValueError: try: return datetime.strptime(date_str, '%Y-%m-%d') except ValueError: pass # Chercher #+post_date_published match = re.search(r'#\+post_date_published:\s*<(.+)>', content) if match: date_str = match.group(1).strip() try: return datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S') except ValueError: try: return datetime.strptime(date_str, '%Y-%m-%d') except ValueError: pass return None def extraire_date_du_fichier(filename): """ Extrait la date du nom de fichier en utilisant find_year_and_slug_on_filename de utils.py. """ date_str, annee, slug = find_year_and_slug_on_filename(filename) if not date_str: return None try: # Format YYYYMMDDHHMMSS (14 caractères) if len(date_str) == 14: if 'T' not in date_str: return datetime.strptime(date_str, '%Y%m%d%H%M%S') else: # Format YYYYMMDDTHHMMSS ou YYYYMMDDTHHMM if len(date_str) == 15: return datetime.strptime(date_str, '%Y%m%dT%H%M%S') elif len(date_str) >= 13: base_date = date_str[:12] return datetime.strptime(base_date, '%Y%m%dT%H%M') # Format YYYYMMDD (8 caractères) elif len(date_str) >= 8: return datetime.strptime(date_str[:8], '%Y%m%d') except ValueError: pass return None def compter_mots(contenu): """Compte le nombre de mots dans le contenu (sans les métadonnées).""" # Utiliser find_extract_in_content_org pour nettoyer le contenu contenu_clean = find_extract_in_content_org(contenu) # Supprimer les liens org-mode pour ne compter que le texte contenu_clean = re.sub(r'\[\[([^\]]+)\]\[([^\]]+)\]\]', r'\2', contenu_clean) contenu_clean = re.sub(r'\[\[([^\]]+)\]\]', r'\1', contenu_clean) # Compter les mots mots = contenu_clean.split() return len([m for m in mots if len(m.strip()) > 0]) def compter_signes(contenu): """Compte le nombre de signes espaces compris dans le contenu.""" # Utiliser find_extract_in_content_org pour nettoyer le contenu contenu_clean = find_extract_in_content_org(contenu) return len(contenu_clean) def compter_liens(contenu): """ Compte le nombre de liens dans le contenu (format [[url]] ou [[url][texte]]). Distingue les liens vers des images des autres liens. Retourne un tuple (nb_liens_images, nb_liens_autres) """ # Compter les liens org-mode liens = re.findall(r'\[\[([^\]]+)\](\[[^\]]+\])?\]', contenu) nb_images = 0 nb_autres = 0 # Extensions d'images courantes extensions_images = ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.bmp', '.ico'] for lien_match in liens: url = lien_match[0] # Vérifier si c'est une image is_image = any(url.lower().endswith(ext) for ext in extensions_images) or \ '/image' in url.lower() or \ 'img' in url.lower() if is_image: nb_images += 1 else: nb_autres += 1 return nb_images, nb_autres def analyser_article(filepath): """ Analyse un fichier article et retourne ses statistiques. """ try: with open(filepath, 'r', encoding='utf-8') as f: contenu = f.read() # Extraire la date date_pub = extraire_date_du_contenu(contenu) if not date_pub: date_pub = extraire_date_du_fichier(os.path.basename(filepath)) if not date_pub: # Utiliser la date de modification du fichier en dernier recours date_pub = datetime.fromtimestamp(os.path.getmtime(filepath)) # Calculer les statistiques nb_mots = compter_mots(contenu) nb_signes = compter_signes(contenu) nb_liens_images, nb_liens_autres = compter_liens(contenu) # Temps de lecture (en minutes) temps_lecture = nb_mots / LECTURE_MOTS_PAR_MINUTE if nb_mots > 0 else 0 return { 'date': date_pub, 'fichier': os.path.basename(filepath), 'mots': nb_mots, 'signes': nb_signes, 'liens_images': nb_liens_images, 'liens_autres': nb_liens_autres, 'liens': nb_liens_images + nb_liens_autres, # Total pour compatibilité 'temps_lecture': temps_lecture } except Exception as e: print(f"Erreur lors de l'analyse de {filepath}: {e}") return None def analyser_blog(blog_path): """ Analyse tous les articles d'un blog et retourne les statistiques. """ articles = [] # Chercher dans lang_fr et lang_en for lang_dir in ['lang_fr', 'lang_en']: lang_path = os.path.join(blog_path, lang_dir) if not os.path.exists(lang_path): continue # Lister tous les fichiers .org for filename in os.listdir(lang_path): if filename.endswith('.org'): filepath = os.path.join(lang_path, filename) stats = analyser_article(filepath) if stats: articles.append(stats) return articles def calculer_statistiques_par_mois(articles): """ Calcule les statistiques agrégées par mois. """ stats_par_mois = defaultdict(lambda: { 'articles': [], 'mots_total': 0, 'signes_total': 0, 'liens_total': 0, 'liens_images_total': 0, 'liens_autres_total': 0, 'temps_lecture_total': 0 }) for article in articles: mois_cle = article['date'].strftime('%Y-%m') stats_par_mois[mois_cle]['articles'].append(article) stats_par_mois[mois_cle]['mots_total'] += article['mots'] stats_par_mois[mois_cle]['signes_total'] += article['signes'] stats_par_mois[mois_cle]['liens_total'] += article['liens'] stats_par_mois[mois_cle]['liens_images_total'] += article['liens_images'] stats_par_mois[mois_cle]['liens_autres_total'] += article['liens_autres'] stats_par_mois[mois_cle]['temps_lecture_total'] += article['temps_lecture'] # Formater les mois pour le template stats_formatees = {} try: locale.setlocale(locale.LC_TIME, 'fr_FR.UTF-8') except: try: locale.setlocale(locale.LC_TIME, 'fra') except: pass for mois_cle, stats in sorted(stats_par_mois.items()): mois_date = datetime.strptime(mois_cle, '%Y-%m') stats_formatees[mois_cle] = { **stats, 'nb_articles': len(stats['articles']), 'mois_formate': mois_date.strftime('%B %Y'), 'date_debut': mois_date } return stats_formatees def calculer_nanowrimo_mois(mois_cle, stats_mois, aujourdhui, objectif_quotidien=None, objectif_mensuel=None): """ Calcule les statistiques NaNoWriMo pour un mois donné. Objectif par défaut : 1667 signes (espaces compris) par jour. Si objectif_quotidien est fourni, il prend le pas sur le défaut. Si objectif_mensuel est fourni, il prend le pas sur tous les autres. """ import calendar mois_date = datetime.strptime(mois_cle, '%Y-%m') annee, mois = mois_date.year, mois_date.month # Nombre de jours dans le mois jours_dans_mois = calendar.monthrange(annee, mois)[1] # Nombre de jours écoulés dans le mois if mois == aujourdhui.month and annee == aujourdhui.year: jour_actuel = aujourdhui.day elif mois_date > aujourdhui: # Mois futur jour_actuel = 0 else: # Mois passé jour_actuel = jours_dans_mois # Déterminer l'objectif quotidien if objectif_mensuel is not None: # Objectif mensuel fourni : le diviser par le nombre de jours objectif_quotidien_utilise = objectif_mensuel / jours_dans_mois objectif_total = objectif_mensuel elif objectif_quotidien is not None: # Objectif quotidien fourni objectif_quotidien_utilise = objectif_quotidien objectif_total = objectif_quotidien * jours_dans_mois else: # Objectif par défaut NaNoWriMo objectif_quotidien_utilise = 1667 objectif_total = 1667 * jours_dans_mois # Objectif jusqu'à aujourd'hui (pour le mois courant) objectif_jusqu_aujourdhui = objectif_quotidien_utilise * jour_actuel if jour_actuel > 0 else 0 # Signes réalisés signes_realises = stats_mois.get('signes_total', 0) # Calculer les signes par jour pour ce mois signes_par_jour = {} for article in stats_mois.get('articles', []): jour = article['date'].day if jour not in signes_par_jour: signes_par_jour[jour] = 0 signes_par_jour[jour] += article['signes'] # Signes aujourd'hui (si on est dans ce mois) signes_aujourdhui = signes_par_jour.get(aujourdhui.day, 0) if \ (mois == aujourdhui.month and annee == aujourdhui.year) else 0 # Calculer le pourcentage if objectif_jusqu_aujourdhui > 0: pourcentage_realise = min(100, (signes_realises / objectif_jusqu_aujourdhui) * 100) else: pourcentage_realise = 0 # Dépassement depassement = max(0, signes_realises - objectif_total) depassement_pourcentage = (depassement / objectif_total * 100) if objectif_total > 0 else 0 return { 'mois_cle': mois_cle, 'mois_formate': mois_date.strftime('%B %Y'), 'jours_dans_mois': jours_dans_mois, 'jour_actuel': jour_actuel, 'objectif_total': objectif_total, 'objectif_jusqu_aujourdhui': objectif_jusqu_aujourdhui, 'signes_realises': signes_realises, 'signes_aujourdhui': signes_aujourdhui, 'pourcentage_realise': pourcentage_realise, 'depassement': depassement, 'depassement_pourcentage': depassement_pourcentage, 'est_mois_courant': (mois == aujourdhui.month and annee == aujourdhui.year), 'est_mois_futur': mois_date > aujourdhui, 'objectif_quotidien': objectif_quotidien_utilise } def calculer_stats_nanowrimo(stats_par_mois, objectif_articles_mois_courant=None, objectif_quotidien=None, objectif_mensuel=None): """ Calcule les statistiques NaNoWriMo pour les 3 derniers mois. Retourne les mois dans l'ordre chronologique inverse (du plus récent au plus ancien). """ aujourdhui = datetime.now() # Obtenir les 3 derniers mois (en incluant le mois courant) # Ordre : mois courant (i=0), mois précédent (i=1), 2 mois avant (i=2) derniers_mois = [] for i in range(3): mois_date = aujourdhui.replace(day=1) # Reculer de i mois for _ in range(i): # Mois précédent if mois_date.month == 1: mois_date = mois_date.replace(year=mois_date.year - 1, month=12) else: mois_date = mois_date.replace(month=mois_date.month - 1) mois_cle = mois_date.strftime('%Y-%m') # Récupérer les stats du mois, même s'il n'y a pas d'articles stats_mois = stats_par_mois.get(mois_cle, { 'articles': [], 'signes_total': 0, 'nb_articles': 0, 'mots_total': 0, 'liens_total': 0, 'liens_images_total': 0, 'liens_autres_total': 0, 'temps_lecture_total': 0 }) stats_nanowrimo = calculer_nanowrimo_mois(mois_cle, stats_mois, aujourdhui, objectif_quotidien, objectif_mensuel) # Ajouter l'objectif d'articles pour le mois courant if stats_nanowrimo['est_mois_courant'] and objectif_articles_mois_courant is not None: stats_nanowrimo['objectif_articles'] = objectif_articles_mois_courant stats_nanowrimo['articles_realises'] = stats_mois.get('nb_articles', 0) elif stats_nanowrimo['est_mois_courant']: # Si pas d'objectif spécifié, calculer la moyenne mensuelle if len(stats_par_mois) > 0: total_articles = sum(len(stats.get('articles', [])) for stats in stats_par_mois.values()) nb_mois = len(stats_par_mois) stats_nanowrimo['objectif_articles'] = int(total_articles / nb_mois) if nb_mois > 0 else 0 else: stats_nanowrimo['objectif_articles'] = None stats_nanowrimo['articles_realises'] = stats_mois.get('nb_articles', 0) else: stats_nanowrimo['objectif_articles'] = None stats_nanowrimo['articles_realises'] = stats_mois.get('nb_articles', 0) derniers_mois.append(stats_nanowrimo) # Inverser pour avoir du plus récent au plus ancien return derniers_mois def generer_graphiques(blog_name, stats_par_mois, output_dir): """ Génère les graphiques pour un blog. """ if not HAS_MATPLOTLIB: return [] graphiques = [] # Préparer les données mois = [] nb_articles = [] mots_totaux = [] signes_totaux = [] temps_lecture = [] for mois_cle, stats in stats_par_mois.items(): mois.append(datetime.strptime(mois_cle, '%Y-%m')) nb_articles.append(len(stats['articles'])) mots_totaux.append(stats['mots_total']) signes_totaux.append(stats['signes_total']) temps_lecture.append(stats['temps_lecture_total']) if not mois: return [] # Créer le dossier pour les graphiques graph_dir = os.path.join(output_dir, 'stats_graphs') os.makedirs(graph_dir, exist_ok=True) # Graphique 1: Articles par mois fig, ax = plt.subplots(figsize=(12, 6)) ax.bar(mois, nb_articles, width=20, color='#4CAF50', alpha=0.7) ax.set_xlabel('Mois', fontsize=12) ax.set_ylabel('Nombre d\'articles', fontsize=12) ax.set_title(f'Articles publiés par mois - {blog_name}', fontsize=14, fontweight='bold') ax.grid(True, alpha=0.3, axis='y') ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) ax.xaxis.set_major_locator(mdates.MonthLocator(interval=max(1, len(mois)//12))) plt.xticks(rotation=45, ha='right') plt.tight_layout() graph1_path = os.path.join(graph_dir, 'articles_par_mois.png') plt.savefig(graph1_path, dpi=150, bbox_inches='tight') plt.close() graphiques.append(('articles_par_mois.png', 'Articles publiés par mois')) # Graphique 2: Mots totaux par mois fig, ax = plt.subplots(figsize=(12, 6)) ax.plot(mois, mots_totaux, marker='o', linewidth=2, markersize=6, color='#2196F3') ax.fill_between(mois, mots_totaux, alpha=0.3, color='#2196F3') ax.set_xlabel('Mois', fontsize=12) ax.set_ylabel('Nombre de mots', fontsize=12) ax.set_title(f'Mots totaux par mois - {blog_name}', fontsize=14, fontweight='bold') ax.grid(True, alpha=0.3) ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) ax.xaxis.set_major_locator(mdates.MonthLocator(interval=max(1, len(mois)//12))) plt.xticks(rotation=45, ha='right') plt.tight_layout() graph2_path = os.path.join(graph_dir, 'mots_par_mois.png') plt.savefig(graph2_path, dpi=150, bbox_inches='tight') plt.close() graphiques.append(('mots_par_mois.png', 'Mots totaux par mois')) # Graphique 3: Temps de lecture par mois fig, ax = plt.subplots(figsize=(12, 6)) ax.bar(mois, temps_lecture, width=20, color='#FF9800', alpha=0.7) ax.set_xlabel('Mois', fontsize=12) ax.set_ylabel('Temps de lecture (minutes)', fontsize=12) ax.set_title(f'Temps de lecture par mois - {blog_name}', fontsize=14, fontweight='bold') ax.grid(True, alpha=0.3, axis='y') ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) ax.xaxis.set_major_locator(mdates.MonthLocator(interval=max(1, len(mois)//12))) plt.xticks(rotation=45, ha='right') plt.tight_layout() graph3_path = os.path.join(graph_dir, 'temps_lecture_par_mois.png') plt.savefig(graph3_path, dpi=150, bbox_inches='tight') plt.close() graphiques.append(('temps_lecture_par_mois.png', 'Temps de lecture par mois')) return graphiques def formater_duree(minutes): """Formate une durée en minutes en texte lisible.""" if minutes < 60: return f"{int(minutes)} min" heures = int(minutes // 60) mins = int(minutes % 60) if heures >= 24: jours = heures // 24 heures = heures % 24 if jours > 0 and heures > 0: return f"{jours}j {heures}h {mins}min" elif jours > 0: return f"{jours}j {mins}min" return f"{heures}h {mins}min" if heures > 0 else f"{int(mins)} min" def format_number(value): """Formate un nombre avec des séparateurs de milliers.""" return f"{value:,}".replace(',', ' ') def generer_statistiques_blog(blog_name, sources_dir, html_websites_dir, env, template, objectif_articles_mois_courant=None, objectif_quotidien=None, objectif_mensuel=None): """ Génère les statistiques pour un blog individuel. """ blog_path = os.path.join(sources_dir, blog_name) # Analyser les articles articles = analyser_blog(blog_path) if not articles: print(f" Aucun article trouvé pour {blog_name}") return None print(f" {len(articles)} articles trouvés") # Calculer les statistiques par mois stats_par_mois = calculer_statistiques_par_mois(articles) # Calculer les statistiques NaNoWriMo pour les 3 derniers mois stats_nanowrimo = calculer_stats_nanowrimo(stats_par_mois, objectif_articles_mois_courant, objectif_quotidien, objectif_mensuel) # Créer le dossier de sortie output_dir = os.path.join(html_websites_dir, blog_name) os.makedirs(output_dir, exist_ok=True) # Copier le fichier CSS css_source = 'templates/styles/stats.css' css_dest = os.path.join(output_dir, 'stats.css') if os.path.exists(css_source): shutil.copy2(css_source, css_dest) # Générer les graphiques graphiques = generer_graphiques(blog_name, stats_par_mois, output_dir) # Récupérer la config du blog avec get_blog_template_conf blog_config = get_blog_template_conf(blog_name) # Calculer les statistiques globales nb_articles_total = len(articles) mots_total = sum(a['mots'] for a in articles) signes_total = sum(a['signes'] for a in articles) liens_total = sum(a['liens'] for a in articles) liens_images_total = sum(a['liens_images'] for a in articles) liens_autres_total = sum(a['liens_autres'] for a in articles) temps_lecture_total = sum(a['temps_lecture'] for a in articles) # Moyennes mots_moyen = mots_total / nb_articles_total if nb_articles_total > 0 else 0 liens_moyen = liens_total / nb_articles_total if nb_articles_total > 0 else 0 liens_images_moyen = liens_images_total / nb_articles_total if nb_articles_total > 0 else 0 liens_autres_moyen = liens_autres_total / nb_articles_total if nb_articles_total > 0 else 0 signes_moyen = signes_total / nb_articles_total if nb_articles_total > 0 else 0 temps_lecture_par_article = temps_lecture_total / nb_articles_total if nb_articles_total > 0 else 0 # Calculer la fréquence de rédaction if len(articles) > 1: articles_tries = sorted(articles, key=lambda x: x['date']) premiere_date = articles_tries[0]['date'] derniere_date = articles_tries[-1]['date'] diff_jours = (derniere_date - premiere_date).days frequence = nb_articles_total / diff_jours * 30 if diff_jours > 0 else 0 # articles par mois else: frequence = 0 premiere_date = articles[0]['date'] if articles else None derniere_date = articles[0]['date'] if articles else None # Calculer la vitesse d'écriture (mots par mois) if len(stats_par_mois) > 0: mots_par_mois_moyen = sum(stats['mots_total'] for stats in stats_par_mois.values()) / len(stats_par_mois) else: mots_par_mois_moyen = 0 # Préparer les données pour le template premiere_date_str = premiere_date.strftime('%d/%m/%Y') if premiere_date else 'N/A' derniere_date_str = derniere_date.strftime('%d/%m/%Y') if derniere_date else 'N/A' date_gen = datetime.now().strftime('%d/%m/%Y à %H:%M:%S') # Générer le HTML avec le template html_content = template.render( blog_title=blog_config.get('BLOG_TITLE', blog_name), author=blog_config.get('AUTHOR', ''), nb_articles_total=nb_articles_total, mots_total=mots_total, signes_total=signes_total, liens_total=liens_total, liens_images_total=liens_images_total, liens_autres_total=liens_autres_total, temps_lecture_total=temps_lecture_total, mots_moyen=mots_moyen, liens_moyen=liens_moyen, liens_images_moyen=liens_images_moyen, liens_autres_moyen=liens_autres_moyen, signes_moyen=signes_moyen, temps_lecture_par_article=temps_lecture_par_article, frequence=frequence, mots_par_mois_moyen=mots_par_mois_moyen, premiere_date_str=premiere_date_str, derniere_date_str=derniere_date_str, stats_par_mois=stats_par_mois, stats_nanowrimo=stats_nanowrimo, graphiques=graphiques, date_gen=date_gen, lecture_mots_par_minute=LECTURE_MOTS_PAR_MINUTE, css_path='stats.css' ) # Sauvegarder le HTML html_path = os.path.join(output_dir, 'stats.html') with open(html_path, 'w', encoding='utf-8') as f: f.write(html_content) print(f" ✓ Statistiques générées pour {blog_name}") # Retourner les données pour une éventuelle combinaison return { 'blog_name': blog_name, 'blog_title': blog_config.get('BLOG_TITLE', blog_name), 'author': blog_config.get('AUTHOR', ''), 'articles': articles, 'stats_par_mois': stats_par_mois, 'graphiques': graphiques, 'graph_path': f'{blog_name}/stats_graphs', 'nb_articles_total': nb_articles_total, 'mots_total': mots_total, 'signes_total': signes_total, 'liens_total': liens_total, 'liens_images_total': liens_images_total, 'liens_autres_total': liens_autres_total, 'stats_nanowrimo': stats_nanowrimo, 'temps_lecture_total': temps_lecture_total, 'mots_moyen': mots_moyen, 'liens_moyen': liens_moyen, 'signes_moyen': signes_moyen, 'temps_lecture_par_article': temps_lecture_par_article, 'frequence': frequence, 'mots_par_mois_moyen': mots_par_mois_moyen, 'premiere_date_str': premiere_date_str, 'derniere_date_str': derniere_date_str } def calculer_nanowrimo_combine(blogs_data, objectif_quotidien=None, objectif_mensuel=None): """ Calcule les statistiques NaNoWriMo combinées pour tous les blogs sur les 3 derniers mois. Additionne les signes de tous les blogs pour chaque mois. """ aujourdhui = datetime.now() # Combiner les stats par mois de tous les blogs stats_par_mois_combines = defaultdict(lambda: { 'articles': [], 'signes_total': 0, 'nb_articles': 0, 'mots_total': 0, 'liens_total': 0, 'liens_images_total': 0, 'liens_autres_total': 0, 'temps_lecture_total': 0 }) # Pour chaque blog, récupérer les stats par mois et additionner for blog_data in blogs_data: for mois_cle, stats_mois in blog_data['stats_par_mois'].items(): stats_par_mois_combines[mois_cle]['articles'].extend(stats_mois.get('articles', [])) stats_par_mois_combines[mois_cle]['signes_total'] += stats_mois.get('signes_total', 0) stats_par_mois_combines[mois_cle]['nb_articles'] += stats_mois.get('nb_articles', 0) stats_par_mois_combines[mois_cle]['mots_total'] += stats_mois.get('mots_total', 0) stats_par_mois_combines[mois_cle]['liens_total'] += stats_mois.get('liens_total', 0) stats_par_mois_combines[mois_cle]['liens_images_total'] += stats_mois.get('liens_images_total', 0) stats_par_mois_combines[mois_cle]['liens_autres_total'] += stats_mois.get('liens_autres_total', 0) stats_par_mois_combines[mois_cle]['temps_lecture_total'] += stats_mois.get('temps_lecture_total', 0) # Calculer les stats NaNoWriMo pour les 3 derniers mois return calculer_stats_nanowrimo(stats_par_mois_combines, None, objectif_quotidien, objectif_mensuel) def generer_graphiques_combines(blogs_data, output_dir): """ Génère des graphiques combinés pour plusieurs blogs. Crée un graphique en barres groupées montrant les articles par mois pour chaque blog avec des couleurs différentes. """ if not HAS_MATPLOTLIB: return [] # Couleurs pour chaque blog (palette de couleurs distinctes) couleurs = ['#4CAF50', '#2196F3', '#FF9800', '#9C27B0', '#F44336', '#00BCD4', '#FFEB3B', '#795548'] # Récupérer tous les mois de tous les blogs tous_mois = set() for blog_data in blogs_data: for mois_cle in blog_data['stats_par_mois'].keys(): tous_mois.add(mois_cle) if not tous_mois: return [] # Trier les mois chronologiquement mois_tries = sorted(tous_mois) mois_dates = [datetime.strptime(m, '%Y-%m') for m in mois_tries] # Créer le dossier pour les graphiques graph_dir = os.path.join(output_dir, 'stats_graphs_combines') os.makedirs(graph_dir, exist_ok=True) graphiques = [] # Graphique 1: Articles par mois (barres groupées) fig, ax = plt.subplots(figsize=(14, 8)) nb_blogs = len(blogs_data) largeur_barre = 20 # Largeur de chaque groupe de barres espacement = largeur_barre / nb_blogs # Espacement entre les barres dans un groupe for i, blog_data in enumerate(blogs_data): nb_articles_par_mois = [] for mois_cle in mois_tries: stats_mois = blog_data['stats_par_mois'].get(mois_cle, {}) nb_articles_par_mois.append(len(stats_mois.get('articles', []))) # Position des barres pour ce blog (décalage par rapport au centre) positions = [mdates.date2num(m) - largeur_barre/2 + i * espacement + espacement/2 for m in mois_dates] ax.bar(positions, nb_articles_par_mois, width=espacement*0.8, label=blog_data['blog_title'], color=couleurs[i % len(couleurs)], alpha=0.7) ax.set_xlabel('Mois', fontsize=12) ax.set_ylabel('Nombre d\'articles', fontsize=12) ax.set_title('Articles publiés par mois - Comparaison des blogs', fontsize=14, fontweight='bold') ax.legend(loc='upper left') ax.grid(True, alpha=0.3, axis='y') ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) ax.xaxis.set_major_locator(mdates.MonthLocator(interval=max(1, len(mois_dates)//12))) plt.xticks([mdates.date2num(m) for m in mois_dates], rotation=45, ha='right') plt.tight_layout() graph1_path = os.path.join(graph_dir, 'articles_par_mois_combines.png') plt.savefig(graph1_path, dpi=150, bbox_inches='tight') plt.close() graphiques.append(('articles_par_mois_combines.png', 'Articles publiés par mois - Comparaison des blogs')) if not graphiques: return [], None return graphiques, graph_dir def generer_page_combinee(blogs_data, output_file, html_websites_dir, env, objectif_quotidien=None, objectif_mensuel=None): """ Génère une page HTML combinée pour plusieurs blogs. """ template_combined = env.get_template('templates/html/stats_combined.html.j2') # Calculer les statistiques globales combinées nb_articles_total = sum(data['nb_articles_total'] for data in blogs_data) mots_total = sum(data['mots_total'] for data in blogs_data) signes_total = sum(data['signes_total'] for data in blogs_data) liens_total = sum(data['liens_total'] for data in blogs_data) temps_lecture_total = sum(data['temps_lecture_total'] for data in blogs_data) # Moyennes globales mots_moyen = mots_total / nb_articles_total if nb_articles_total > 0 else 0 liens_moyen = liens_total / nb_articles_total if nb_articles_total > 0 else 0 signes_moyen = signes_total / nb_articles_total if nb_articles_total > 0 else 0 temps_lecture_par_article = temps_lecture_total / nb_articles_total if nb_articles_total > 0 else 0 # Calculer les statistiques NaNoWriMo combinées pour les 3 derniers mois stats_nanowrimo_combines = calculer_nanowrimo_combine(blogs_data, objectif_quotidien, objectif_mensuel) # Générer les graphiques combinés graphiques_combines, graph_path_combines = generer_graphiques_combines(blogs_data, html_websites_dir) # Copier le fichier CSS css_source = 'templates/styles/stats.css' css_dest = os.path.join(html_websites_dir, 'stats.css') if os.path.exists(css_source): shutil.copy2(css_source, css_dest) # Générer le HTML date_gen = datetime.now().strftime('%d/%m/%Y à %H:%M:%S') blogs_list = ', '.join(data['blog_title'] for data in blogs_data) page_title = ' | '.join(data['blog_title'] for data in blogs_data) html_content = template_combined.render( page_title=page_title, blogs_list=blogs_list, blogs_data=blogs_data, nb_articles_total=nb_articles_total, mots_total=mots_total, signes_total=signes_total, liens_total=liens_total, temps_lecture_total=temps_lecture_total, mots_moyen=mots_moyen, liens_moyen=liens_moyen, signes_moyen=signes_moyen, temps_lecture_par_article=temps_lecture_par_article, stats_nanowrimo_combines=stats_nanowrimo_combines, objectif_quotidien_custom=objectif_quotidien, objectif_mensuel_custom=objectif_mensuel, graphiques_combines=graphiques_combines, graph_path_combines='stats_graphs_combines' if graphiques_combines else None, date_gen=date_gen, lecture_mots_par_minute=LECTURE_MOTS_PAR_MINUTE, css_path='stats.css' ) # Sauvegarder le HTML html_path = os.path.join(html_websites_dir, output_file) with open(html_path, 'w', encoding='utf-8') as f: f.write(html_content) print(f"\n✓ Page combinée générée: {html_path}") def main(): """ Fonction principale qui analyse les blogs et génère les pages de statistiques. """ parser = argparse.ArgumentParser( description='Génère des statistiques détaillées sur les blogs' ) parser.add_argument( 'blogs', nargs='*', help='Noms des blogs à analyser (si vide, analyse tous les blogs)' ) parser.add_argument( '--output', '-o', help='Nom du fichier de sortie pour une page combinée (ex: tk_combinaison_stats.html)' ) parser.add_argument( '--objectif-articles', type=int, help='Objectif de nombre d\'articles pour le mois courant (par défaut: moyenne mensuelle)' ) parser.add_argument( '--objectif-signes-quotidien', type=int, help='Objectif de signes par jour (prend le pas sur l\'objectif NaNoWriMo par défaut de 1667)' ) parser.add_argument( '--objectif-signes-mensuel', type=int, help='Objectif de signes par mois (prend le pas sur l\'objectif quotidien et NaNoWriMo)' ) args = parser.parse_args() sources_dir = "sources" html_websites_dir = "html-websites" # Configurer Jinja2 env = Environment(loader=FileSystemLoader('.')) # Ajouter des filtres personnalisés pour le template env.filters['format_duree'] = formater_duree env.filters['format_number'] = format_number template = env.get_template('templates/html/stats.html.j2') # Lister tous les dossiers de blogs si aucun n'est spécifié if not os.path.exists(sources_dir): print(f"Erreur: Le dossier {sources_dir} n'existe pas") return if args.blogs: blogs = args.blogs else: blogs = [d for d in os.listdir(sources_dir) if os.path.isdir(os.path.join(sources_dir, d)) and d not in ['.', '..']] print(f"Blogs à analyser: {', '.join(blogs)}\n") # Calculer l'objectif d'articles par défaut (moyenne mensuelle) si non spécifié objectif_articles = args.objectif_articles if objectif_articles is None: # Calculer la moyenne mensuelle sur tous les blogs total_articles = 0 total_mois = 0 for blog_name in blogs: blog_path = os.path.join(sources_dir, blog_name) articles = analyser_blog(blog_path) if articles: stats_par_mois = calculer_statistiques_par_mois(articles) total_articles += len(articles) total_mois += len(stats_par_mois) if total_mois > 0: objectif_articles = int(total_articles / total_mois) print(f"Objectif d'articles par défaut (moyenne mensuelle): {objectif_articles}\n") # Récupérer les objectifs de signes personnalisés objectif_quotidien = args.objectif_signes_quotidien objectif_mensuel = args.objectif_signes_mensuel # Si un fichier de sortie est spécifié, générer une page combinée if args.output: blogs_data = [] for blog_name in blogs: print(f"Analyse de {blog_name}...") data = generer_statistiques_blog(blog_name, sources_dir, html_websites_dir, env, template, objectif_articles, objectif_quotidien, objectif_mensuel) if data: blogs_data.append(data) print() if blogs_data: generer_page_combinee(blogs_data, args.output, html_websites_dir, env, objectif_quotidien, objectif_mensuel) else: print("Aucune donnée à combiner.") else: # Générer une page pour chaque blog for blog_name in blogs: print(f"Analyse de {blog_name}...") generer_statistiques_blog(blog_name, sources_dir, html_websites_dir, env, template, objectif_articles, objectif_quotidien, objectif_mensuel) print() print("Terminé!") if __name__ == "__main__": main()