#!/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, Counter from urllib.parse import urlparse 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 nettoyer_contenu(contenu, est_markdown=False): """ Nettoie le contenu en supprimant les métadonnées. Pour org: supprime les lignes #+, les :PROPERTIES:/:END:, le logbook Pour markdown: supprime le frontmatter YAML """ if est_markdown: # Supprimer le frontmatter YAML si présent contenu = re.sub(r'^---\n.*?\n---\n', '', contenu, flags=re.DOTALL | re.MULTILINE) # Supprimer les lignes qui commencent par # (commentaires markdown) contenu = re.sub(r'^\s*#.*\n', '', contenu, flags=re.MULTILINE) return contenu.strip() else: # Utiliser find_extract_in_content_org qui supprime déjà: # - Les lignes #+ # - Les :PROPERTIES:/:END: # - Le logbook return find_extract_in_content_org(contenu) def compter_mots(contenu, est_markdown=False): """ Compte le nombre de mots dans le contenu (sans les métadonnées). Exclut : - Les liens (images et autres) - Les lignes de propriété orgmode (:PROPERTIES: à :END:) - Les lignes qui commencent par "#+" """ # Nettoyer le contenu (supprime déjà #+ et :PROPERTIES:) contenu_clean = nettoyer_contenu(contenu, est_markdown) if est_markdown: # Supprimer complètement les liens markdown [texte](url) et images ![alt](url) contenu_clean = re.sub(r'!?\[([^\]]*)\]\([^\)]+\)', '', contenu_clean) else: # Supprimer complètement les liens org-mode [[url]] et [[url][texte]] contenu_clean = re.sub(r'\[\[[^\]]+\](?:\[[^\]]+\])?\]', '', 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, est_markdown=False): """ Compte le nombre de signes espaces compris dans le contenu. Exclut : - Les liens (images et autres) - Les lignes de propriété orgmode (:PROPERTIES: à :END:) - Les lignes qui commencent par "#+" """ # Nettoyer le contenu (supprime déjà #+ et :PROPERTIES:) contenu_clean = nettoyer_contenu(contenu, est_markdown) # Exclure aussi les liens du décompte de signes if est_markdown: # Supprimer complètement les liens markdown [texte](url) et images ![alt](url) contenu_clean = re.sub(r'!?\[([^\]]*)\]\([^\)]+\)', '', contenu_clean) else: # Supprimer complètement les liens org-mode [[url]] et [[url][texte]] contenu_clean = re.sub(r'\[\[[^\]]+\](?:\[[^\]]+\])?\]', '', contenu_clean) return len(contenu_clean) def extraire_domaine(url): """ Extrait le domaine d'une URL. Retourne None si ce n'est pas une URL valide. """ try: # Ajouter http:// si l'URL ne commence pas par http:// ou https:// if not url.startswith(('http://', 'https://')): if url.startswith('www.'): url = 'http://' + url else: return None parsed = urlparse(url) domain = parsed.netloc # Supprimer www. si présent if domain.startswith('www.'): domain = domain[4:] # Ignorer les URLs locales ou non valides if not domain or domain in ['localhost', '']: return None return domain except Exception: return None def extraire_liens(contenu, est_markdown=False): """ Extrait tous les liens du contenu et retourne une liste d'URLs. """ urls = [] if est_markdown: # Extraire les liens markdown: [texte](url) ou ![alt](url) liens_images = re.findall(r'!\[([^\]]*)\]\(([^\)]+)\)', contenu) liens_autres = re.findall(r'(? 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, 'contenu': contenu, # Contenu brut pour extraction des domaines 'est_markdown': est_markdown } 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 # Reste à faire reste_a_faire = max(0, objectif_total - signes_realises) # Signes réalisés par jour (moyenne) # Pour les mois passés : signes / jours dans le mois # Pour le mois courant : signes / jours écoulés if jour_actuel > 0: signes_par_jour_moyen = signes_realises / jour_actuel elif jours_dans_mois > 0: signes_par_jour_moyen = signes_realises / jours_dans_mois else: signes_par_jour_moyen = 0 # Récupérer les articles du mois (triés par date) articles_du_mois = sorted(stats_mois.get('articles', []), key=lambda x: x['date']) 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, 'reste_a_faire': reste_a_faire, 'signes_par_jour_moyen': signes_par_jour_moyen, 'articles_du_mois': articles_du_mois } def calculer_stats_nanowrimo(stats_par_mois, objectif_articles_mois_courant=None, objectif_quotidien=None, objectif_mensuel=None): """ Calcule les statistiques NaNoWriMo pour les 12 derniers mois. Retourne les mois dans l'ordre chronologique inverse (du plus récent au plus ancien). """ aujourdhui = datetime.now() # Obtenir les 12 derniers mois (en incluant le mois courant) # Ordre : mois courant (i=0), mois précédent (i=1), ..., 11 mois avant (i=11) derniers_mois = [] for i in range(12): 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, label='Articles') # Calculer la moyenne globale moyenne_globale = sum(nb_articles) / len(nb_articles) if nb_articles else 0 if moyenne_globale > 0: ax.axhline(y=moyenne_globale, color='red', linestyle='--', linewidth=2, label=f'Moyenne globale: {moyenne_globale:.1f}') # Calculer la moyenne glissante sur 6 mois if len(nb_articles) >= 6: moyenne_glissante = [] for i in range(len(nb_articles)): debut = max(0, i - 5) fenetre = nb_articles[debut:i+1] moyenne_glissante.append(sum(fenetre) / len(fenetre) if fenetre else 0) ax.plot(mois, moyenne_glissante, color='orange', linestyle='-', linewidth=2, marker='o', markersize=4, label='Moyenne glissante (6 mois)') 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.legend(loc='upper left') ax.grid(True, alpha=0.3, axis='y') # Améliorer le formatage des dates pour éviter la superposition nb_mois = len(mois) if nb_mois > 24: interval = max(3, nb_mois // 12) format_str = '%Y' elif nb_mois > 12: interval = max(2, nb_mois // 12) format_str = '%Y-%m' else: interval = 1 format_str = '%Y-%m' ax.xaxis.set_major_formatter(mdates.DateFormatter(format_str)) ax.xaxis.set_major_locator(mdates.MonthLocator(interval=interval)) plt.xticks(rotation=45, ha='right') plt.setp(ax.xaxis.get_majorticklabels(), 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', label='Mots') ax.fill_between(mois, mots_totaux, alpha=0.3, color='#2196F3') # Calculer la moyenne globale moyenne_globale = sum(mots_totaux) / len(mots_totaux) if mots_totaux else 0 if moyenne_globale > 0: ax.axhline(y=moyenne_globale, color='red', linestyle='--', linewidth=2, label=f'Moyenne globale: {moyenne_globale:.0f}') # Calculer la moyenne glissante sur 6 mois if len(mots_totaux) >= 6: moyenne_glissante = [] for i in range(len(mots_totaux)): debut = max(0, i - 5) fenetre = mots_totaux[debut:i+1] moyenne_glissante.append(sum(fenetre) / len(fenetre) if fenetre else 0) ax.plot(mois, moyenne_glissante, color='orange', linestyle='-', linewidth=2, marker='s', markersize=4, label='Moyenne glissante (6 mois)') 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.legend(loc='upper left') ax.grid(True, alpha=0.3) # Améliorer le formatage des dates pour éviter la superposition nb_mois = len(mois) if nb_mois > 24: interval = max(3, nb_mois // 12) format_str = '%Y' elif nb_mois > 12: interval = max(2, nb_mois // 12) format_str = '%Y-%m' else: interval = 1 format_str = '%Y-%m' ax.xaxis.set_major_formatter(mdates.DateFormatter(format_str)) ax.xaxis.set_major_locator(mdates.MonthLocator(interval=interval)) plt.xticks(rotation=45, ha='right') plt.setp(ax.xaxis.get_majorticklabels(), 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') # Améliorer le formatage des dates pour éviter la superposition nb_mois = len(mois) if nb_mois > 24: interval = max(3, nb_mois // 12) format_str = '%Y' elif nb_mois > 12: interval = max(2, nb_mois // 12) format_str = '%Y-%m' else: interval = 1 format_str = '%Y-%m' ax.xaxis.set_major_formatter(mdates.DateFormatter(format_str)) ax.xaxis.set_major_locator(mdates.MonthLocator(interval=interval)) plt.xticks(rotation=45, ha='right') plt.setp(ax.xaxis.get_majorticklabels(), 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 # Calculer les domaines des liens domaines_liens = calculer_domaines_liens(articles) 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', sections_stats=None, # Pas de sections pour l'analyse de blog complet domaines_liens=domaines_liens ) # 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 [], None # 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 [], None # 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 # Calculer les totaux par mois pour les moyennes totaux_articles_par_mois = [] for mois_cle in mois_tries: total_mois = 0 for blog_data in blogs_data: stats_mois = blog_data['stats_par_mois'].get(mois_cle, {}) total_mois += len(stats_mois.get('articles', [])) totaux_articles_par_mois.append(total_mois) 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) # Ajouter la moyenne globale moyenne_globale = sum(totaux_articles_par_mois) / len(totaux_articles_par_mois) if totaux_articles_par_mois else 0 if moyenne_globale > 0: ax.axhline(y=moyenne_globale, color='red', linestyle='--', linewidth=2, label=f'Moyenne globale: {moyenne_globale:.1f}') # Ajouter la moyenne glissante sur 6 mois if len(totaux_articles_par_mois) >= 6: moyenne_glissante = [] for i in range(len(totaux_articles_par_mois)): debut = max(0, i - 5) fenetre = totaux_articles_par_mois[debut:i+1] moyenne_glissante.append(sum(fenetre) / len(fenetre) if fenetre else 0) ax.plot(mois_dates, moyenne_glissante, color='orange', linestyle='-', linewidth=2, marker='o', markersize=4, label='Moyenne glissante (6 mois)') 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') # Améliorer le formatage des dates pour éviter la superposition nb_mois = len(mois_dates) if nb_mois > 24: interval = max(3, nb_mois // 12) format_str = '%Y' elif nb_mois > 12: interval = max(2, nb_mois // 12) format_str = '%Y-%m' else: interval = 1 format_str = '%Y-%m' ax.xaxis.set_major_formatter(mdates.DateFormatter(format_str)) ax.xaxis.set_major_locator(mdates.MonthLocator(interval=interval)) plt.setp(ax.xaxis.get_majorticklabels(), 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')) # Graphique 2: Mots par mois (barres groupées) fig, ax = plt.subplots(figsize=(14, 8)) # Calculer les totaux par mois pour les moyennes totaux_mots_par_mois = [] for mois_cle in mois_tries: total_mois = 0 for blog_data in blogs_data: stats_mois = blog_data['stats_par_mois'].get(mois_cle, {}) total_mois += stats_mois.get('mots_total', 0) totaux_mots_par_mois.append(total_mois) for i, blog_data in enumerate(blogs_data): mots_par_mois = [] for mois_cle in mois_tries: stats_mois = blog_data['stats_par_mois'].get(mois_cle, {}) mots_par_mois.append(stats_mois.get('mots_total', 0)) # 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, mots_par_mois, width=espacement*0.8, label=blog_data['blog_title'], color=couleurs[i % len(couleurs)], alpha=0.7) # Ajouter la moyenne globale moyenne_globale = sum(totaux_mots_par_mois) / len(totaux_mots_par_mois) if totaux_mots_par_mois else 0 if moyenne_globale > 0: ax.axhline(y=moyenne_globale, color='red', linestyle='--', linewidth=2, label=f'Moyenne globale: {moyenne_globale:.0f}') # Ajouter la moyenne glissante sur 6 mois if len(totaux_mots_par_mois) >= 6: moyenne_glissante = [] for i in range(len(totaux_mots_par_mois)): debut = max(0, i - 5) fenetre = totaux_mots_par_mois[debut:i+1] moyenne_glissante.append(sum(fenetre) / len(fenetre) if fenetre else 0) ax.plot(mois_dates, moyenne_glissante, color='orange', linestyle='-', linewidth=2, marker='s', markersize=4, label='Moyenne glissante (6 mois)') ax.set_xlabel('Mois', fontsize=12) ax.set_ylabel('Nombre de mots', fontsize=12) ax.set_title('Mots publiés par mois - Comparaison des blogs', fontsize=14, fontweight='bold') ax.legend(loc='upper left') ax.grid(True, alpha=0.3, axis='y') # Améliorer le formatage des dates pour éviter la superposition nb_mois = len(mois_dates) if nb_mois > 24: interval = max(3, nb_mois // 12) format_str = '%Y' elif nb_mois > 12: interval = max(2, nb_mois // 12) format_str = '%Y-%m' else: interval = 1 format_str = '%Y-%m' ax.xaxis.set_major_formatter(mdates.DateFormatter(format_str)) ax.xaxis.set_major_locator(mdates.MonthLocator(interval=interval)) plt.setp(ax.xaxis.get_majorticklabels(), rotation=45, ha='right') plt.tight_layout() graph2_path = os.path.join(graph_dir, 'mots_par_mois_combines.png') plt.savefig(graph2_path, dpi=150, bbox_inches='tight') plt.close() graphiques.append(('mots_par_mois_combines.png', 'Mots publiés par mois - Comparaison des blogs')) # Graphique 3: Signes par mois (barres groupées) fig, ax = plt.subplots(figsize=(14, 8)) # Calculer les totaux par mois pour les moyennes totaux_signes_par_mois = [] for mois_cle in mois_tries: total_mois = 0 for blog_data in blogs_data: stats_mois = blog_data['stats_par_mois'].get(mois_cle, {}) total_mois += stats_mois.get('signes_total', 0) totaux_signes_par_mois.append(total_mois) for i, blog_data in enumerate(blogs_data): signes_par_mois = [] for mois_cle in mois_tries: stats_mois = blog_data['stats_par_mois'].get(mois_cle, {}) signes_par_mois.append(stats_mois.get('signes_total', 0)) # 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, signes_par_mois, width=espacement*0.8, label=blog_data['blog_title'], color=couleurs[i % len(couleurs)], alpha=0.7) # Ajouter la moyenne globale moyenne_globale = sum(totaux_signes_par_mois) / len(totaux_signes_par_mois) if totaux_signes_par_mois else 0 if moyenne_globale > 0: ax.axhline(y=moyenne_globale, color='red', linestyle='--', linewidth=2, label=f'Moyenne globale: {moyenne_globale:.0f}') # Ajouter la moyenne glissante sur 6 mois if len(totaux_signes_par_mois) >= 6: moyenne_glissante = [] for i in range(len(totaux_signes_par_mois)): debut = max(0, i - 5) fenetre = totaux_signes_par_mois[debut:i+1] moyenne_glissante.append(sum(fenetre) / len(fenetre) if fenetre else 0) ax.plot(mois_dates, moyenne_glissante, color='orange', linestyle='-', linewidth=2, marker='s', markersize=4, label='Moyenne glissante (6 mois)') ax.set_xlabel('Mois', fontsize=12) ax.set_ylabel('Nombre de signes (espaces inclus)', fontsize=12) ax.set_title('Signes publiés par mois - Comparaison des blogs', fontsize=14, fontweight='bold') ax.legend(loc='upper left') ax.grid(True, alpha=0.3, axis='y') # Améliorer le formatage des dates pour éviter la superposition nb_mois = len(mois_dates) if nb_mois > 24: interval = max(3, nb_mois // 12) format_str = '%Y' elif nb_mois > 12: interval = max(2, nb_mois // 12) format_str = '%Y-%m' else: interval = 1 format_str = '%Y-%m' ax.xaxis.set_major_formatter(mdates.DateFormatter(format_str)) ax.xaxis.set_major_locator(mdates.MonthLocator(interval=interval)) plt.setp(ax.xaxis.get_majorticklabels(), rotation=45, ha='right') plt.tight_layout() graph3_path = os.path.join(graph_dir, 'signes_par_mois_combines.png') plt.savefig(graph3_path, dpi=150, bbox_inches='tight') plt.close() graphiques.append(('signes_par_mois_combines.png', 'Signes 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 décomptes par jour, mois, année articles_par_jour = defaultdict(int) articles_par_mois = defaultdict(int) articles_par_annee = defaultdict(int) for blog_data in blogs_data: for article in blog_data['articles']: date = article['date'] jour_cle = date.strftime('%Y-%m-%d') mois_cle = date.strftime('%Y-%m') annee_cle = date.strftime('%Y') articles_par_jour[jour_cle] += 1 articles_par_mois[mois_cle] += 1 articles_par_annee[annee_cle] += 1 nb_jours_avec_articles = len(articles_par_jour) nb_mois_avec_articles = len(articles_par_mois) nb_annees_avec_articles = len(articles_par_annee) # Calculer les moyennes par jour, mois, année articles_par_jour_moyen = nb_articles_total / nb_jours_avec_articles if nb_jours_avec_articles > 0 else 0 articles_par_mois_moyen = nb_articles_total / nb_mois_avec_articles if nb_mois_avec_articles > 0 else 0 articles_par_annee_moyen = nb_articles_total / nb_annees_avec_articles if nb_annees_avec_articles > 0 else 0 mots_par_jour_moyen = mots_total / nb_jours_avec_articles if nb_jours_avec_articles > 0 else 0 mots_par_mois_moyen = mots_total / nb_mois_avec_articles if nb_mois_avec_articles > 0 else 0 mots_par_annee_moyen = mots_total / nb_annees_avec_articles if nb_annees_avec_articles > 0 else 0 signes_par_jour_moyen = signes_total / nb_jours_avec_articles if nb_jours_avec_articles > 0 else 0 signes_par_mois_moyen = signes_total / nb_mois_avec_articles if nb_mois_avec_articles > 0 else 0 signes_par_annee_moyen = signes_total / nb_annees_avec_articles if nb_annees_avec_articles > 0 else 0 temps_lecture_par_jour_moyen = temps_lecture_total / nb_jours_avec_articles if nb_jours_avec_articles > 0 else 0 temps_lecture_par_mois_moyen = temps_lecture_total / nb_mois_avec_articles if nb_mois_avec_articles > 0 else 0 temps_lecture_par_annee_moyen = temps_lecture_total / nb_annees_avec_articles if nb_annees_avec_articles > 0 else 0 liens_par_jour_moyen = liens_total / nb_jours_avec_articles if nb_jours_avec_articles > 0 else 0 liens_par_mois_moyen = liens_total / nb_mois_avec_articles if nb_mois_avec_articles > 0 else 0 liens_par_annee_moyen = liens_total / nb_annees_avec_articles if nb_annees_avec_articles > 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) # Calculer les domaines combinés de tous les blogs tous_articles = [] for blog_data in blogs_data: tous_articles.extend(blog_data.get('articles', [])) domaines_liens_combines = calculer_domaines_liens(tous_articles) if tous_articles else [] # 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', nb_jours_avec_articles=nb_jours_avec_articles, nb_mois_avec_articles=nb_mois_avec_articles, nb_annees_avec_articles=nb_annees_avec_articles, articles_par_jour_moyen=articles_par_jour_moyen, articles_par_mois_moyen=articles_par_mois_moyen, articles_par_annee_moyen=articles_par_annee_moyen, mots_par_jour_moyen=mots_par_jour_moyen, mots_par_mois_moyen=mots_par_mois_moyen, mots_par_annee_moyen=mots_par_annee_moyen, signes_par_jour_moyen=signes_par_jour_moyen, signes_par_mois_moyen=signes_par_mois_moyen, signes_par_annee_moyen=signes_par_annee_moyen, temps_lecture_par_jour_moyen=temps_lecture_par_jour_moyen, temps_lecture_par_mois_moyen=temps_lecture_par_mois_moyen, temps_lecture_par_annee_moyen=temps_lecture_par_annee_moyen, liens_par_jour_moyen=liens_par_jour_moyen, liens_par_mois_moyen=liens_par_mois_moyen, liens_par_annee_moyen=liens_par_annee_moyen, sections_stats=sections_stats ) # 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 parser_sections_org(filepath): """ Parse un fichier org pour extraire les sections (entêtes) et leur contenu. Retourne une liste de dictionnaires avec : - niveau: niveau de l'entête (1, 2, 3, ...) - titre: texte de l'entête - contenu: contenu de la section (sans l'entête) - mots: nombre de mots dans la section - signes: nombre de signes dans la section """ sections = [] current_section = None current_content = [] in_properties = False with open(filepath, 'r', encoding='utf-8') as f: lines = f.readlines() for i, line in enumerate(lines): line_stripped = line.rstrip() # Ignorer les lignes de propriétés if line_stripped == ':PROPERTIES:': in_properties = True continue if in_properties: if line_stripped == ':END:': in_properties = False continue # Ignorer les lignes qui commencent par #+ if line_stripped.startswith('#+'): continue # Détecter un entête (commence par * et a un espace après) match = re.match(r'^(\*+)\s+(.+)$', line_stripped) if match: # Sauvegarder la section précédente si elle existe if current_section is not None: content_str = '\n'.join(current_content).strip() # Nettoyer le contenu des lignes vides en début/fin if content_str: mots = compter_mots(content_str, est_markdown=False) signes = compter_signes(content_str, est_markdown=False) current_section['contenu'] = content_str current_section['mots'] = mots current_section['signes'] = signes sections.append(current_section) # Nouvelle section stars = match.group(1) titre = match.group(2).strip() current_level = len(stars) current_section = { 'niveau': current_level, 'titre': titre, 'contenu': '', 'mots': 0, 'signes': 0 } current_content = [] else: # Ligne de contenu pour la section courante if current_section is not None: # Ignorer les lignes complètement vides en début, mais garder les lignes vides qui suivent du contenu if line.strip() or current_content: current_content.append(line) # Sauvegarder la dernière section if current_section is not None: content_str = '\n'.join(current_content).strip() if content_str: mots = compter_mots(content_str, est_markdown=False) signes = compter_signes(content_str, est_markdown=False) current_section['contenu'] = content_str current_section['mots'] = mots current_section['signes'] = signes sections.append(current_section) return sections def generer_statistiques_fichier(filepath, html_websites_dir, env, template, date_debut_ecriture=None, objectif_quotidien=None, objectif_mensuel=None, objectif_section=None): """ Génère les statistiques pour un seul fichier org ou markdown. Si date_debut_ecriture est None, utilise le 1er du mois courant. """ import calendar # Vérifier que le fichier existe if not os.path.exists(filepath): print(f"Erreur: Le fichier {filepath} n'existe pas") return None # Vérifier l'extension ext = os.path.splitext(filepath)[1].lower() if ext not in ['.org', '.md', '.markdown']: print(f"Erreur: Le fichier doit être en .org, .md ou .markdown (reçu: {ext})") return None print(f"Analyse du fichier {filepath}...") # Analyser le fichier article = analyser_article(filepath) if not article: print(f"Impossible d'analyser le fichier {filepath}") return None # Déterminer la date de début d'écriture aujourdhui = datetime.now() if date_debut_ecriture is None: # Par défaut : 1er du mois courant date_debut_ecriture = aujourdhui.replace(day=1) # Calculer les statistiques par jour, mois, année depuis la date de début date_article = article['date'] if date_article < date_debut_ecriture: date_debut_ecriture = date_article # Ajuster si l'article est plus ancien # Calculer le nombre de jours/mois/années depuis le début delta = aujourdhui - date_debut_ecriture nb_jours_ecriture = max(1, delta.days + 1) # Au moins 1 jour # Calculer les moyennes par jour, mois, année articles_par_jour_moyen = 1 / nb_jours_ecriture articles_par_mois_moyen = 1 / max(1, nb_jours_ecriture / 30) articles_par_annee_moyen = 1 / max(1, nb_jours_ecriture / 365) mots_par_jour_moyen = article['mots'] / nb_jours_ecriture mots_par_mois_moyen = article['mots'] / max(1, nb_jours_ecriture / 30) mots_par_annee_moyen = article['mots'] / max(1, nb_jours_ecriture / 365) signes_par_jour_moyen = article['signes'] / nb_jours_ecriture signes_par_mois_moyen = article['signes'] / max(1, nb_jours_ecriture / 30) signes_par_annee_moyen = article['signes'] / max(1, nb_jours_ecriture / 365) temps_lecture_par_jour_moyen = article['temps_lecture'] / nb_jours_ecriture temps_lecture_par_mois_moyen = article['temps_lecture'] / max(1, nb_jours_ecriture / 30) temps_lecture_par_annee_moyen = article['temps_lecture'] / max(1, nb_jours_ecriture / 365) liens_par_jour_moyen = article['liens'] / nb_jours_ecriture liens_par_mois_moyen = article['liens'] / max(1, nb_jours_ecriture / 30) liens_par_annee_moyen = article['liens'] / max(1, nb_jours_ecriture / 365) # Créer une structure de données compatible avec les templates articles = [article] stats_par_mois = calculer_statistiques_par_mois(articles) # Calculer les stats NaNoWriMo pour le mois courant (depuis date_debut_ecriture) mois_courant_cle = date_debut_ecriture.strftime('%Y-%m') stats_mois = stats_par_mois.get(mois_courant_cle, { 'articles': articles, 'signes_total': article['signes'], 'nb_articles': 1, 'mots_total': article['mots'], 'liens_total': article['liens'], 'liens_images_total': article['liens_images'], 'liens_autres_total': article['liens_autres'], 'temps_lecture_total': article['temps_lecture'] }) # Calculer NaNoWriMo en considérant que l'écriture a commencé à date_debut_ecriture stats_nanowrimo_mois = calculer_nanowrimo_mois(mois_courant_cle, stats_mois, aujourdhui, objectif_quotidien, objectif_mensuel) # Ajuster jour_actuel pour refléter les jours depuis date_debut_ecriture jours_ecoules = (aujourdhui - date_debut_ecriture).days + 1 stats_nanowrimo_mois['jour_actuel'] = min(jours_ecoules, stats_nanowrimo_mois['jours_dans_mois']) # Recalculer objectif_jusqu_aujourdhui stats_nanowrimo_mois['objectif_jusqu_aujourdhui'] = stats_nanowrimo_mois['objectif_quotidien'] * jours_ecoules # Recalculer signes_par_jour_moyen if jours_ecoules > 0: stats_nanowrimo_mois['signes_par_jour_moyen'] = article['signes'] / jours_ecoules else: stats_nanowrimo_mois['signes_par_jour_moyen'] = 0 # Recalculer reste_a_faire stats_nanowrimo_mois['reste_a_faire'] = max(0, stats_nanowrimo_mois['objectif_jusqu_aujourdhui'] - article['signes']) # Ajouter les propriétés manquantes pour compatibilité avec le template stats_nanowrimo_mois['objectif_articles'] = None # Pas d'objectif d'articles pour un fichier unique stats_nanowrimo_mois['articles_realises'] = 1 # Un seul article (le fichier) stats_nanowrimo = [stats_nanowrimo_mois] # Créer le dossier de sortie filename_base = os.path.splitext(os.path.basename(filepath))[0] output_dir = os.path.join(html_websites_dir, 'fichiers_analyses') 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) # Calculer les statistiques globales nb_articles_total = 1 mots_total = article['mots'] signes_total = article['signes'] liens_total = article['liens'] liens_images_total = article['liens_images'] liens_autres_total = article['liens_autres'] temps_lecture_total = article['temps_lecture'] # Calculer les domaines des liens domaines_liens = calculer_domaines_liens([article]) # Parser les sections si c'est un fichier org sections_stats = None if ext == '.org': sections = parser_sections_org(filepath) if sections: # Définir l'objectif par niveau # Par défaut : 500 mots pour niveau 1 et 2 objectif_par_niveau = {} if objectif_section is None: objectif_par_niveau[1] = 500 objectif_par_niveau[2] = 500 else: # Si un objectif global est fourni, l'utiliser pour tous les niveaux max_niveau = max((s['niveau'] for s in sections), default=2) for niveau in range(1, max_niveau + 1): objectif_par_niveau[niveau] = objectif_section # Calculer les stats pour chaque section sections_stats = [] for section in sections: niveau = section['niveau'] objectif = objectif_par_niveau.get(niveau, objectif_section if objectif_section else 500) mots_section = section['mots'] signes_section = section['signes'] manquants = max(0, objectif - mots_section) depasse = mots_section >= objectif sections_stats.append({ 'niveau': niveau, 'titre': section['titre'], 'mots': mots_section, 'signes': signes_section, 'objectif': objectif, 'manquants': manquants, 'depasse': depasse }) # Générer le HTML date_gen = datetime.now().strftime('%d/%m/%Y à %H:%M:%S') date_article_str = date_article.strftime('%d/%m/%Y') if date_article else 'N/A' html_content = template.render( blog_title=f"Analyse: {os.path.basename(filepath)}", 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_total, liens_moyen=liens_total, liens_images_moyen=liens_images_total, liens_autres_moyen=liens_autres_total, signes_moyen=signes_total, temps_lecture_par_article=temps_lecture_total, frequence=0, premiere_date_str=date_article_str, derniere_date_str=date_article_str, stats_par_mois=stats_par_mois, stats_nanowrimo=stats_nanowrimo, graphiques=[], date_gen=date_gen, lecture_mots_par_minute=LECTURE_MOTS_PAR_MINUTE, css_path='stats.css', fichier_source=filepath, date_article=date_article_str, date_debut_ecriture=date_debut_ecriture.strftime('%d/%m/%Y'), articles_par_jour_moyen=articles_par_jour_moyen, articles_par_mois_moyen=articles_par_mois_moyen, articles_par_annee_moyen=articles_par_annee_moyen, mots_par_jour_moyen=mots_par_jour_moyen, mots_par_mois_moyen=mots_par_mois_moyen, mots_par_annee_moyen=mots_par_annee_moyen, signes_par_jour_moyen=signes_par_jour_moyen, signes_par_mois_moyen=signes_par_mois_moyen, signes_par_annee_moyen=signes_par_annee_moyen, temps_lecture_par_jour_moyen=temps_lecture_par_jour_moyen, temps_lecture_par_mois_moyen=temps_lecture_par_mois_moyen, temps_lecture_par_annee_moyen=temps_lecture_par_annee_moyen, liens_par_jour_moyen=liens_par_jour_moyen, liens_par_mois_moyen=liens_par_mois_moyen, liens_par_annee_moyen=liens_par_annee_moyen, nb_jours_avec_articles=1, nb_mois_avec_articles=1, nb_annees_avec_articles=1, sections_stats=sections_stats, domaines_liens=domaines_liens ) # Sauvegarder le HTML html_path = os.path.join(output_dir, f'{filename_base}_stats.html') with open(html_path, 'w', encoding='utf-8') as f: f.write(html_content) print(f" ✓ Statistiques générées: {html_path}") return 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 ou un fichier unique' ) parser.add_argument( 'blogs', nargs='*', help='Noms des blogs à analyser ou chemin vers un fichier .org/.md/.markdown (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)' ) parser.add_argument( '--date-debut', type=str, help='Date de début d\'écriture au format YYYY-MM-DD (pour l\'analyse d\'un fichier unique, défaut: 1er du mois courant)' ) parser.add_argument( '--objectif-section', type=int, help='Objectif de mots par section pour les fichiers org (par défaut: 500 pour niveau 1 et 2)' ) 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') # Vérifier si un seul fichier est fourni if args.blogs and len(args.blogs) == 1: filepath = args.blogs[0] # Vérifier si c'est un fichier (et pas un dossier de blog) if os.path.isfile(filepath): # Parser la date de début si fournie date_debut_ecriture = None if args.date_debut: try: date_debut_ecriture = datetime.strptime(args.date_debut, '%Y-%m-%d') except ValueError: print(f"Erreur: Format de date invalide pour --date-debut. Utilisez YYYY-MM-DD (ex: 2024-01-15)") return # Récupérer les objectifs de signes et de section objectif_quotidien = args.objectif_signes_quotidien objectif_mensuel = args.objectif_signes_mensuel objectif_section = args.objectif_section generer_statistiques_fichier(filepath, html_websites_dir, env, template, date_debut_ecriture, objectif_quotidien, objectif_mensuel, objectif_section) print("Terminé!") return # 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()