mirror of
https://forge.chapril.org/tykayn/orgmode-to-gemini-blog
synced 2025-11-19 23:00:35 +01:00
1738 lines
69 KiB
Python
1738 lines
69 KiB
Python
#!/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 
|
|
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 
|
|
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 
|
|
liens_images = re.findall(r'!\[([^\]]*)\]\(([^\)]+)\)', contenu)
|
|
liens_autres = re.findall(r'(?<!\!)\[([^\]]+)\]\(([^\)]+)\)', contenu)
|
|
|
|
for url in [l[1] for l in liens_images]:
|
|
urls.append(url)
|
|
|
|
for url in [l[1] for l in liens_autres]:
|
|
urls.append(url)
|
|
else:
|
|
# Extraire les liens org-mode: [[url]] ou [[url][texte]]
|
|
liens = re.findall(r'\[\[([^\]]+)\](\[[^\]]+\])?\]', contenu)
|
|
|
|
for lien_match in liens:
|
|
url = lien_match[0]
|
|
urls.append(url)
|
|
|
|
return urls
|
|
|
|
|
|
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  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']
|
|
|
|
urls = extraire_liens(contenu, est_markdown)
|
|
|
|
for url in urls:
|
|
# 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 calculer_domaines_liens(articles):
|
|
"""
|
|
Calcule les domaines des liens web et retourne un Counter trié par occurrence décroissante.
|
|
Retourne une liste de tuples (domaine, nombre) triée par nombre décroissant.
|
|
"""
|
|
domaines_counter = Counter()
|
|
|
|
for article in articles:
|
|
urls = extraire_liens(article.get('contenu', ''), article.get('est_markdown', False))
|
|
|
|
for url in urls:
|
|
domain = extraire_domaine(url)
|
|
if domain:
|
|
domaines_counter[domain] += 1
|
|
|
|
# Trier par occurrence décroissante
|
|
domaines_tries = sorted(domaines_counter.items(), key=lambda x: x[1], reverse=True)
|
|
|
|
return domaines_tries
|
|
|
|
|
|
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,
|
|
'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()
|