orgmode-to-gemini-blog/generate_blog_stats.py

930 lines
35 KiB
Python
Raw Normal View History

#!/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()