orgmode-to-gemini-blog/generate_blog_stats.py

1668 lines
67 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 nettoyer_contenu(contenu, est_markdown=False):
2025-11-02 17:51:40 +01:00
"""
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)
2025-11-02 17:51:40 +01:00
# Supprimer les lignes qui commencent par # (commentaires markdown)
contenu = re.sub(r'^\s*#.*\n', '', contenu, flags=re.MULTILINE)
return contenu.strip()
else:
2025-11-02 17:51:40 +01:00
# 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):
2025-11-02 17:51:40 +01:00
"""
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:
2025-11-02 17:51:40 +01:00
# Supprimer complètement les liens markdown [texte](url) et images ![alt](url)
contenu_clean = re.sub(r'!?\[([^\]]*)\]\([^\)]+\)', '', contenu_clean)
else:
2025-11-02 17:51:40 +01:00
# 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):
2025-11-02 17:51:40 +01:00
"""
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)
2025-11-02 17:51:40 +01:00
# 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 compter_liens(contenu, est_markdown=False):
"""
Compte le nombre de liens dans le contenu.
Pour org: format [[url]] ou [[url][texte]]
Pour markdown: format [texte](url) ou ![alt](url) pour images
Distingue les liens vers des images des autres liens.
Retourne un tuple (nb_liens_images, nb_liens_autres)
"""
nb_images = 0
nb_autres = 0
# Extensions d'images courantes
extensions_images = ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.bmp', '.ico']
if est_markdown:
# Compter les liens markdown: [texte](url) ou ![alt](url)
liens_images = re.findall(r'!\[([^\]]*)\]\(([^\)]+)\)', contenu)
liens_autres = re.findall(r'(?<!\!)\[([^\]]+)\]\(([^\)]+)\)', contenu)
for url in [l[1] for l in liens_images]:
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
for url in [l[1] for l in liens_autres]:
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
else:
# Compter les liens org-mode
liens = re.findall(r'\[\[([^\]]+)\](\[[^\]]+\])?\]', contenu)
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 (org ou markdown) et retourne ses statistiques.
"""
try:
with open(filepath, 'r', encoding='utf-8') as f:
contenu = f.read()
# Détecter le type de fichier
ext = os.path.splitext(filepath)[1].lower()
est_markdown = ext in ['.md', '.markdown']
# Extraire la date
date_pub = None
if not est_markdown:
date_pub = extraire_date_du_contenu(contenu)
else:
# Pour markdown, chercher dans le frontmatter YAML
match = re.search(r'^---\n.*?date:\s*(.+?)\n', contenu, re.DOTALL | re.MULTILINE)
if match:
try:
date_pub = datetime.strptime(match.group(1).strip(), '%Y-%m-%d')
except ValueError:
pass
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, est_markdown)
nb_signes = compter_signes(contenu, est_markdown)
nb_liens_images, nb_liens_autres = compter_liens(contenu, est_markdown)
# 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
2025-11-02 16:22:43 +01:00
# 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,
2025-11-02 16:22:43 +01:00
'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):
"""
2025-11-02 16:22:43 +01:00
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()
2025-11-02 16:22:43 +01:00
# 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 = []
2025-11-02 16:22:43 +01:00
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))
2025-11-02 16:22:43 +01:00
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')
2025-11-02 16:22:43 +01:00
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))
2025-11-02 16:22:43 +01:00
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')
2025-11-02 16:22:43 +01:00
# 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')
2025-11-02 16:22:43 +01:00
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
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
)
# 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:
2025-11-02 16:22:43 +01:00
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:
2025-11-02 16:22:43 +01:00
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
2025-11-02 16:22:43 +01:00
# 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)
2025-11-02 16:22:43 +01:00
# 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'))
2025-11-02 16:22:43 +01:00
# 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')
2025-11-02 16:22:43 +01:00
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')
2025-11-02 16:22:43 +01:00
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
2025-11-02 16:22:43 +01:00
# 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)
# 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,
2025-11-02 16:22:43 +01:00
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']
# 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
)
# 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()