MediaWiki:Gadget-translation editor.js/Statistiques/code

Définition, traduction, prononciation, anagramme et synonyme sur le dictionnaire libre Wiktionnaire.

Ces codes ont servi à générer les statistiques présentes dans MediaWiki:Gadget-translation editor.js/Statistiques.

Le principe pour générer les stats est simple :

  1. On compte d’un côté toutes les traductions dans les résumés d’édition du type (Traductions : +langue : [[traduction]] ; +langue2 : [[traduction2]] (assisté))
  2. Puis on analyse tous les diffs contenant des ajouts de nombreuses traductions, avec des résumés tronqués du type : (Traductions : +langue : [[traduction]] ; +langue2 : [[traduction 2]] ; +langue3 : [[traduction 3]] ; +lang…). Dans un tel cas, analyser le résumé d’édition n’est pas suffisant pour savoir combien de traductions ont été ajoutées et dans quelles langues. On analyse donc les diffs.

La première partie a été réalisée via un code écrit en C++ (parce qu’inspirée d'un script de Pamputt), tandis que la 2e a été implémentée en Python, via le framework Pywikibot.

Partie 1

#include <iostream>
#include <fstream>
#include <string>
#include <map>

using namespace std;

// Dump à télécharger à : https://dumps.wikimedia.org/frwiktionary/latest/ :
// frwiktionary-latest-pages-meta-history.xml.7z , puis à décompresser
string input_folder = "C:\\Users\\automatik\\Documents\\Wiktionnaire\\Stats translation editor\\";
string input_file_history = input_folder + "frwiktionary-latest-pages-meta-history.xml";

// Chemins des fichiers contenant les résultats
string output_folder = "C:\\Users\\automatik\\Documents\\Wiktionnaire\\Stats translation editor\\";
string output_file_res = output_folder + "stats-summary.txt"; // Chiffres-clés
string output_file_langs = output_folder + "stats-langs.txt"; // Stats par langue
string output_file_dates = output_folder + "stats-months.txt"; // Stats par mois
string output_file_contribs = output_folder + "stats-trads-contributors.txt"; // Stats par contributeur
string output_file_ips = output_folder + "stats-trads-ips.txt"; // Stats par IP
string output_file_diffs = output_folder + "stats-trads-diffs-to-check.txt"; // Diffs à analyser en Partie 2

string from_date = "2014-07"; // Mois à partir duquel on dénombre les ajouts (mois inclus)
string to_date = "2024-01"; // Mois jusqu’auquel on dénombre les ajouts (mois exclus)

/**** STATS ****/
std::map<string, int> stats_langs = {}; // exemple (en début d’analyse) : { "italien": 3, "allemand" : 8 }
std::map<string, int> stats_dates = {}; // ex : { "2014-08": 6169, "2014-09" : 6203 }
std::map<string, int> stats_contribs = {}; // ex : { "Un utilisateur": 213, "Un deuxième utilisateur": 65 }
std::map<string, int> stats_ips = {}; // ex : { "184.252.23.16": 11, "174.145.64.87": 75 }
// Maps pour stocker temporairement des résultats (pour être sûr que les trads analysées n'ont pas été révoquées)
// Ces maps sont transférées vers les "map" finaux ci-dessus une fois qu'on est sûr que les traductions concernées n'ont pas été révoquées
// Note : pour s'assurer que les ajouts ne sont pas révoqués, on lit tous les résumés d'édition qui succèdent à un ajout de traduction
// jusqu'à ce qu'on tombe sur une révocation ou autre chose qu'un ajout de traduction (via l'outil). Si l'ajout correspond à la dernière version
// de l'article, on le considère comme non révoqué
std::map<string, int> stats_langs_tmp = {};
std::map<string, int> stats_dates_tmp = {};
std::map<string, int> stats_contribs_tmp = {};
std::map<string, int> stats_ips_tmp = {};

unsigned int counter_pages = 0; // Nombre de pages parcourues dans le dump (simplement pour afficher la progression de la tâche)
unsigned int counter_additions = 0; // Nombre d'ajouts avec l'outil de traductions (ajouts révoqués ou non)
unsigned int counter_pages_with_trans = 0; // Nombre de pages avec au moins un ajout de traduction non révoqué
unsigned int counter_trans = 0; // Nombre de traductions ajoutées et non révoquées
unsigned int total_count_contribs = 0, total_count_ips = 0; // Nombre de traductions non révoquées ajoutées par des contributeurs enregistrés / non enregistrés
unsigned int total_count_contribs_tmp = 0, total_count_ips_tmp = 0; // Dénombrements temporaires transférés si traductions pas révoquées

unsigned int counter_additions_contributors = 0; // Nombre d’ajouts de traduction(s) par des utilisateurs… inscrits
unsigned int counter_additions_ips = 0;          //         "           "           "           "        … non inscrits
unsigned int revoked_contributor_additions = 0; // Nombre de traductions révoquées… de contributeurs inscrits
unsigned int revoked_ip_additions = 0;      //      "          "        "     … de contributeurs non inscrits

string remember; // diffs à mettre de côté car résumé d'édition tronqué
string remember_tmp; // Variable temporaire (transférée si traductions correspondantes non révoquées)
int cnt_rem = 0; // Nombre de diffs à analyser
int cnt_rem_tmp = 0;
/**************/

// Transfert d’une map (temporaire) à une autre (dénombrant les résultats finaux)
void transfer_between_maps(map<string, int> &map_source, map<string, int> &map_dest) {
    for(auto const &item : map_source) {
        if (map_dest[item.first]) {
            map_dest[item.first] += item.second;
        } else {
            map_dest[item.first] = item.second;
        }
    }
}
// Vide les variables temporaires après un transfert
void empty_all_tmp_vars() {
    stats_langs_tmp = {};
    stats_dates_tmp = {};
    stats_contribs_tmp = {};
    stats_ips_tmp = {};
    total_count_contribs_tmp = 0;
    total_count_ips_tmp = 0;
    cnt_rem_tmp = 0;
    remember_tmp = "";
}
// Transfert de toutes les variables temporaires vers leurs versions persistantes
void transfer_all_tmp_vars() {
    transfer_between_maps(stats_langs_tmp, stats_langs);
    transfer_between_maps(stats_dates_tmp, stats_dates);
    transfer_between_maps(stats_contribs_tmp, stats_contribs);
    transfer_between_maps(stats_ips_tmp, stats_ips);
    total_count_contribs += total_count_contribs_tmp;
    total_count_ips += total_count_ips_tmp;
    counter_trans += (total_count_contribs_tmp + total_count_ips_tmp);
    remember += remember_tmp;
    cnt_rem += cnt_rem_tmp;
    empty_all_tmp_vars();
}

// Fonction effectuant le traitement de la Partie 1
void count_trads_added_with_te()
{
    /**** FICHIERS D'ENTREE ET DE SORTIE ****/
    ifstream infile(input_file_history, ifstream::in);
    if (!infile) {
        cout << "Le dump n'est pas situe a " << input_file_history << endl;
        return;
    }
    ofstream out_res(output_file_res, ofstream::out);
    if (!out_res) {
        cout << "Probleme avec le fichier de sortie global " << endl;
        return;
    }
    ofstream out_langs(output_file_langs, ofstream::out);
    if (!out_langs) {
        cout << "Probleme avec le fichier de sortie sur les langues" << endl;
        return;
    }
    ofstream out_dates(output_file_dates, ofstream::out);
    if (!out_dates) {
        cout << "Probleme avec le fichier de sortie sur les dates" << endl;
        return;
    }
    ofstream out_contribs(output_file_contribs, ofstream::out);
    if (!out_contribs) {
        cout << "Probleme avec le fichier de sortie sur les contributeurs" << endl;
        return;
    }
    ofstream out_ips(output_file_ips, ofstream::out);
    if (!out_ips) {
        cout << "Probleme avec le fichier de sortie sur les ips" << endl;
        return;
    }
    ofstream out_diff(output_file_diffs, ofstream::out);
    if (!out_diff) {
        cout << "Probleme avec le fichier de sortie de diffs" << endl;
        return;
    }
    /***************************************/

    /*** PARAMETRES ***/
    // Pour s’assurer que la révision en cours est bien dans la période d’analyse désirée (et définie plus haut)
    int from_year = stoi(from_date.substr(0, 4));
    int from_month = stoi(from_date.substr(5, 2));
    int to_year = stoi(to_date.substr(0, 4));
    int to_month = stoi(to_date.substr(5, 2));
    /******************/

    /**** REVISION EN COURS ****/
    bool pending_rev = false;
    bool additions_to_count = false; // Ajouts à décompter ? Variable vidée après s'être assurée que les ajouts décomptés n'ont pas été annulés
    unsigned int ip_additions_to_count = 0;      // Nombre d'ajouts d'IPs à décompter comme révoqués si tel est le cas
    unsigned int contrib_additions_to_count = 0; //      "      "   de contributeurs    "      "         "      "
    bool id_read = false;
    string title, rev_id, prev_rev_id; // Informations utiles pour les ajouts dont on veut analyser les diffs
    int ns = -1;
    string tstamp; // Timestamp de la révision en cours d'analyse
    int year, month;
    string contributor; // Nom du contributeur ou adresse IP
    bool is_ip = false; // Si le contributeur est enregistré ou non
    string comment = ""; // résumé d’édition en cours d’analyse
    string lang; // langue de la traduction en cours d’analyse (langues des traductions mentionnées dans le résumé d'édition)

    string line; // Ligne du dump actuellement en cours d’analyse
    size_t pos1, pos2, pos3;

    bool entry_with_trans = false;
    /***************************/

    while (getline(infile, line)) {
        if (line.find("<title>") != string::npos) {
            pos1 = line.find("<title>");
            pos2 = line.find("</title>");
            title= line.substr(pos1+7, pos2-pos1-7);
            counter_pages++;
        }
        // Si pas main, on passe
        if (line.find("<ns>") != string::npos) {
            pos1 = line.find("<ns>");
            pos2 = line.find("</ns>");
            ns = stoi(line.substr(pos1+4, pos2-pos1-4));
        }
        if (ns != 0) {
            continue;
        }
        if (line.find("<revision>") != string::npos) {
            pending_rev = true;
            id_read = false;
        }
        if (pending_rev) {
            if (!id_read && line.find("<id>") != string::npos) {
                pos1 = line.find("<id>");
                pos2 = line.find("</id>");
                prev_rev_id = rev_id;
                rev_id = line.substr(pos1+4, pos2-pos1-4);
                id_read = true;
            }
            // On verifie que l'édition est faite dans la période choisie
            if (line.find("<timestamp>") != string::npos) {
                pos1 = line.find("<timestamp>");
                pos2 = line.find("</timestamp>");
                tstamp = line.substr(pos1+11, 7);
                year = stoi( tstamp.substr(0, 4) );
                month = stoi( tstamp.substr(5, 2) );
                if (year < from_year || (year == from_year && month < from_month) ||
                    year > to_year || (year == to_year && month >= to_month)
                    ) {
                    pending_rev = false;
                    if (additions_to_count) {
                        transfer_all_tmp_vars();
                        additions_to_count = false;
                    }
                    continue;
                }
            }
            // Pour un utilisateur on aura dans le fichier XML <username></username>, tandis que pour une IP on aura <ip></ip>
            if (line.find("<username>") != string::npos) {
                pos1 = line.find("<username>");
                pos2 = line.find("</username>");
                contributor = line.substr(pos1+10, pos2-pos1-10);
                is_ip = false;
            }
            if (line.find("<ip>") != string::npos) {
                pos1 = line.find("<ip>");
                pos2 = line.find("</ip>");
                contributor = line.substr(pos1+4, pos2-pos1-4);
                is_ip = true;
            }

            // On analyse tous les commentaires des pages de l’espace principal à la recherche de
            // résumés d’édition correspondant à des ajouts traductions avec Translation editor
            if (line.find("<comment>") != string::npos) {
                pos1 = line.find("<comment>");
                pos2 = line.find("</comment>");
                comment = line.substr(pos1+9, pos2-pos1-9);
                if (comment.substr(0, 15) == "Traductions : +") { // cas 1 : traductions ajoutées avec l'outil : on met de côté
                                                                // avant de s’assurer qu'elles n'ont pas été révoquées
                    additions_to_count = true;
                    counter_additions++;
                    if (is_ip) {
                        ip_additions_to_count++;
                        counter_additions_ips++;
                    } else {
                        contrib_additions_to_count++;
                        counter_additions_contributors++;
                    }
                    // Si le résumé d’édition est tronqué, on garde les infos de côté pour analyser + tard les diffs
                    if (comment.find("(assist") == string::npos) {
                        remember_tmp += "title=" + title + ";prev_rev_id=" + prev_rev_id + ";rev_id=" + rev_id +
                                    ";contrib=" + contributor + ";is_ip=" + (is_ip ? "true" : "false") +
                                    ";date=" + tstamp + "\n";
                        cnt_rem_tmp++;
                        continue;
                    }
                    entry_with_trans = true;
                    pos3 = 11;
                    do {
                        pos3 += 4;
                        lang = comment.substr(pos3, comment.find(" :", pos3) - pos3);
                        if (lang == "en-tête") continue; // On ignore les modifications d’en-tête
                        if (stats_langs_tmp.count(lang)) {
                            stats_langs_tmp[lang]++;
                        } else {
                            stats_langs_tmp[lang] = 1;
                        }
                        if (stats_dates_tmp.count(tstamp)) {
                            stats_dates_tmp[tstamp]++;
                        } else {
                            stats_dates_tmp[tstamp] = 1;
                        }
                        if (!is_ip) {
                            if (stats_contribs_tmp.count(contributor)) {
                                stats_contribs_tmp[contributor]++;
                            } else {
                                stats_contribs_tmp[contributor] = 1;
                            }
                            total_count_contribs_tmp++;
                        } else {
                            if (stats_ips_tmp.count(contributor)) {
                                stats_ips_tmp[contributor]++;
                            } else {
                                stats_ips_tmp[contributor] = 1;
                            }
                            total_count_ips_tmp++;
                        }
                        pos3 = comment.find(" ; +", pos3);
                    } while (pos3 != string::npos); // On répète la boucle pour toutes les traductions présentes dans le résumé d’édition
                } else if ((comment.substr(0, 15) == "R\u00E9vocation des"
                            || comment.substr(0, 14) == "Annulation des")
                            && additions_to_count
                           ) { // cas 2 : révocation d'ajout(s) de traduction : on ignore
                    revoked_contributor_additions += contrib_additions_to_count;
                    revoked_ip_additions += ip_additions_to_count;
                    empty_all_tmp_vars();
                    additions_to_count = false;
                    ip_additions_to_count = 0;
                    contrib_additions_to_count = 0;
                    entry_with_trans = false;
                } else if (additions_to_count) { // cas 3 : non-révocation après ajout de traductions :
                                                 // on compte les trads précédemment analysées
                    transfer_all_tmp_vars();
                    additions_to_count = false;
                    ip_additions_to_count = 0;
                    contrib_additions_to_count = 0;
                }
            }
            if (line.find("</revision>") != string::npos) {
                pending_rev = false;
            }
        }
        if (line.find("</title>") != string::npos) {
            if (additions_to_count) { // cas ou la derniere modif d'une page est un ajout de traduction (non révoqué) :
                                    // on prend alors en compte les ajouts de trads
                transfer_all_tmp_vars();
                additions_to_count = false;
                ip_additions_to_count = 0;
                contrib_additions_to_count = 0;
            }
            if (entry_with_trans) {
                counter_pages_with_trans++;
                // Tous les 100 pages modifiées avec Translation editor, on affiche des stats récapitulatives
                if (counter_pages_with_trans % 100 == 0) {
                    cout << counter_pages_with_trans << " pages modifiees sur " <<
                        counter_pages << " scannees - " << counter_trans << " trads ajoutees avec TE (et "
                                        << revoked_contributor_additions + revoked_ip_additions << "/"
                                        << counter_additions << " ajouts revoques) contribs: "
                                        << counter_additions_contributors << " ; IPs: " << counter_additions_ips << endl;
                }
                // Tous les 10 000 pages modifiées avec Translation editor, on affiche les stats accumulés jusqu’alors
                // (pour le suivi et le repérage anticipé d’aberrations dans les résultats)
                if (counter_pages_with_trans % 10000 == 0) {
                    for (const auto &p : stats_langs) {
                        std::cout << p.first << " : " << p.second << '\n';
                    }
                    for (const auto &p : stats_dates) {
                        std::cout << p.first << " : " << p.second << '\n';
                    }
                    for (const auto &p : stats_contribs) {
                        std::cout << p.first << " : " << p.second << '\n';
                    }
                    std::cout << "Total contributeurs : " << total_count_contribs << '\n';
                    std::cout << "Total IPs : " << total_count_ips << '\n';
                    cout << cnt_rem << " diffs a analyser en +" << endl;
                }
            }
            entry_with_trans = false;
        }
    }

    cout << cnt_rem << " diffs a analyser en +" << endl;
    cout << counter_pages_with_trans << " pages modifiees avec translation_editor (" << counter_trans << " traductions)" << endl;

    infile.close();

    out_res  << "* Traductions ajoutées : " << counter_trans << endl;
    out_res << "* Ajouts avec l'outil : " << counter_additions << " (contributeurs : " << counter_additions_contributors
            << " ; IPs : " << counter_additions_ips << ")" << endl;
    out_res << "* Pages modifiées avec translation_editor : " << counter_pages_with_trans << endl;
    out_res << "* Traductions ajoutées par des utilisateurs inscrits : " << total_count_contribs << endl;
    out_res << "* Traductions ajoutées par des utilisateurs non inscrits : " << total_count_ips << endl;
    out_res << "* Ajouts de contributeurs révoqués : " << revoked_contributor_additions << endl;
    out_res << "* Ajouts d'IPs révoqués : " << revoked_ip_additions << endl;

    // Ecriture des resultats dans le fichier de sortie
    for (const auto &p : stats_langs) {
        out_langs << p.first << " : " << p.second << endl;
    }
    for (const auto &p : stats_dates) {
        out_dates << p.first << " : " << p.second << endl;
    }
    for (const auto &p : stats_contribs) {
        out_contribs << p.first << " : " << p.second << endl;
    }
    for (const auto &p : stats_ips) {
        out_ips << p.first << " : " << p.second << endl;
    }

    out_res.close();
    out_langs.close();
    out_dates.close();
    out_contribs.close();
    out_ips.close();

    // Entrees restantes à traiter en analysant les diffs => dans un fichier a part
    out_diff << remember;
    out_diff.close();
}

int main()
{
    count_trads_added_with_te();

    cout << "End of processing!" << endl;
    return 0;
}
// Traductions décomptées sur le dump du 1/12/2023 : 494138

Ce script a pris ~30 minutes à s’exécuter (Windows 10, processeur quadricore (2.3GHz), 8 Go de RAM).

Partie 2

#!/usr/bin/env python3
import os
import re
import time
import pywikibot
import ast
from subprocess import call

# Copie de [[MediaWiki:Gadget-translation editor.js/langues.json]] (la liste étant préfixée de languages =)
from languages_list import languages

site = pywikibot.Site(code='fr', fam='wiktionary')

# Dossier qui contient les résultats générés précédemment et notamment le fichier
# stats-trads-diffs-to-check.txt qui contient les infos sur les diffs à analyser.
folder = os.path.dirname(__file__)
# Fichiers de sortie
file_langs = 'stats-langs-after-diffs.txt'
file_dates = 'stats-months-after-diffs.txt'
file_contributors = 'stats-trads-contributors-after-diffs.txt'
file_ips = 'stats-trads-ips-after-diffs.txt'
  
def first_n_items_dict(d, n, first_index):
  '''Returns a list of n top-valued items from index first_index'''
  return sorted(d.items(), key=lambda x: x[1], reverse=True)[first_index:first_index+n]
  
def dict_from_file(file, folder=folder):
  with open(os.path.join(folder, file), encoding='utf-8') as f:
    dict_results = ast.literal_eval(f.read())
  return dict_results
  
def generate_graph(results, file_graph='graph_template.txt', cat='langs', label='lang'):
  '''
  @param results: an object containing the results
  @type results: str representing a file,
                or dict or list of tuples
  '''
  input_file = os.path.join(folder, file_graph)
  output_file = os.path.join(folder, 'graph-{}.txt'.format(cat))
  if isinstance(results, str):
    with open(os.path.join(folder, results)) as f:
      results = ast.literal_eval(f.read())
  with open(input_file, 'r', encoding='utf-8') as f:
    content = f.read()
    specific_results = ''
    if isinstance(results, dict):
      for k in results:
        specific_results += '        {{"{}": "{}", "amount": {} }},\n'.format(label, k, str(results[k]))
    elif isinstance(results, list):
      for item in results:
        specific_results += '        {{"{}": "{}", "amount": {} }},\n'.format(label, item[0], str(item[1]))
    else:
      print('results objects should be either a string, a list or a dict, but it is a {}'.format(type(results))); return
    specific_results = specific_results[:-2] # removing last ",\n"
    content = content.replace('__TO_REPLACE__', specific_results)
    content = content.replace('__LABEL__', label)
    oblique_labels = '__OBLIQUE_LABELS__'
    if cat == 'dates':
        content = content.replace(oblique_labels, ', "properties": { "labels": {"angle": {"value": -45}, "dx": {"value": -20} } } ')
    else:
      content = content.replace(oblique_labels, '')
  with open(output_file, 'w', encoding='utf-8') as f:
    f.write(content)
  print('Graph saved to {}'.format(output_file))
    
def generate_langs_graph(nbLangs=15, first_index=0, filename_var='langs'):
    d = dict_from_file(file_langs)
    data = first_n_items_dict(d, nbLangs, first_index)
    generate_graph(data, file_graph='graph_template.txt', cat=filename_var, label='lang')
    
def generate_dates_graph():
    generate_graph(file_dates, file_graph='graph_template.txt', cat='dates', label='date')
    
def results_to_wikitable(dict, header1='Langues', header2='Traductions ajoutées'):
  wikitext = '{{| class="wikitable sortable mw-collapsible"\n! {} !! {}'.format(header1, header2)
  for k in dict:
    wikitext += '\n|-\n| {} || {}'.format(k, str(dict[k]))
  wikitext += '\n|}'
  return wikitext
  
def raw_results_to_wikitable(filename, dest=None, header1='Langues', header2='Traductions ajoutées'):
  if dest is None:
    raise Exception('A destination file must be provided')
  input_file = os.path.join(folder, filename)
  output_file = os.path.join(folder, dest)
  with open(input_file, encoding='utf-8') as f:
    for line in f:
      dict_results = ast.literal_eval(line)
      break
  with open(output_file, 'w+', encoding='utf-8') as f:
    f.write(results_to_wikitable(dict_results, header1, header2))
  print('Wikitable saved to {}'.format(output_file))

def count_trads_per_diff():
  '''
  Génère des statistiques d'ajout de trads lorsque les résumés d'édition sont tronqués
  et necessitent une analyse des diffs
  '''
  stats_langs = {}
  stats_dates = {}
  stats_contribs = {}
  stats_ips = {}
  
  cpt = 0
  cpt_trads = 0
  count_contributors = 0
  count_ips = 0

  with open(os.path.join(folder, "stats-langs.txt"), encoding="utf-8") as f:
    for line in f:
      match = re.search("(.+) : (\d+)", line)
      if match is None:
        continue
      stats_langs[match[1]] = int(match[2])
  
  with open(os.path.join(folder, "stats-months.txt"), encoding="utf-8") as f:
    for line in f:
      match = re.search("(.+) : (\d+)", line)
      stats_dates[match[1]] = int(match[2])
  
  with open(os.path.join(folder, "stats-trads-contributors.txt"), encoding="utf-8") as f:
    for line in f:
      match = re.search("(.+) : (\d+)", line)
      if match is None:
        continue
      stats_contribs[match[1]] = int(match[2])
      
  with open(os.path.join(folder, "stats-trads-ips.txt"), encoding="utf-8") as f:
    for line in f:
      match = re.search("(.+) : (\d+)", line)
      if match is None:
        continue
      stats_ips[match[1]] = int(match[2])
      
  with open(os.path.join(folder, "stats-trads-diffs-to-check.txt"), encoding="utf-8") as f:
    for line in f:
      # line = "title=lire;prev_rev_id=18770633;rev_id=18908602;contrib=Test;is_ip=false;date=2015-01"
      # title = line.split(';')[0].split('=')[1]
      from_rev = line.split(';')[1].split('=')[1]
      to_rev = line.split(';')[2].split('=')[1]
      contrib = line.split(';')[3].split('=')[1]
      if line.split(';')[4].split('=')[1] == 'true':
        is_ip = True
      else:
        is_ip = False
      date = line.split(';')[5].split('=')[1].strip()
      diff_html = site.compare(old=int(from_rev), diff=int(to_rev))
      
      # The Mediawiki diff algorithm sometimes shows existing translations as removed lines
      # (cf. eg. https://fr.wiktionary.org/w/index.php?title=boto&diff=prev&oldid=25504237),
      # and then shows them back in the added lines.
      # Hence we collect all language codes in the added translations (A), and in the removed translations (B),
      # and we do the diff, by substracting B from A (as multisets).
      # This works as translation_editor is only used to add translations.
      # Note: the previously used algorithm ([[Special:Permalink/33473930]]) was almost twice as efficient,
      # and had an error margin of just about ~100 entries out of 77,500, that is a ~0.13 % error margin.
      trads_removed = re.findall('<td class="diff-deletedline diff-side-deleted"><div>(.+)</div></td>', diff_html)
      trads_added = re.findall('<td class="diff-addedline diff-side-added"><div>(.+)</div></td>', diff_html)
      codes_added = []
      codes_removed = []
      for t in trads_added:
        codes_added += re.findall('{{trad(?:<ins class=\"diffchange diffchange-inline\">)?[+-]{0,2}(?:</ins>)?\|(?:<ins class="diffchange diffchange-inline">)?([^|<]+)(?:</ins>)?\|', t)
      for t in trads_removed:
        codes_removed += re.findall('{{trad(?:<del class=\"diffchange diffchange-inline\">)?[+-]{0,2}(?:</del>)?\|(?:<del class="diffchange diffchange-inline">)?([^|<]+)(?:</del>)?\|', t)
      # Now that we collected the codes for all removed translations, we remove one instance
      # of each code in the added translations.
      for code in codes_removed:
        codes_added.remove(code)
      for code in codes_added:
        if code in languages:
          lang_name = languages[code]
        elif code in languages['redirects']:
          lang_name = languages[languages['redirects'][code]]
        else:
          # Les codes langue non répertoriés dans la liste des langues sont ignorés
          # 1 cas le 3/12/2023 : le code zh-tc supprimé depuis
          print('CODE ' + code + ' NOT FOUND (and translation ignored) - ' + line.strip())
          continue
        if lang_name:
          if lang_name in stats_langs:
            stats_langs[lang_name] += 1
          else:
            stats_langs[lang_name] = 1
        if is_ip:
          if contrib in stats_ips:
            stats_ips[contrib] += 1
          else:
            stats_ips[contrib] = 1
          count_ips += 1
        else:
          if contrib in stats_contribs:
            stats_contribs[contrib] += 1
          else:
            stats_contribs[contrib] = 1
          count_contributors += 1
        if date in stats_dates:
          stats_dates[date] += 1
        else:
          stats_dates[date] = 1
        cpt_trads += 1
      cpt += 1
      if cpt % 100 == 0:
        print(str(cpt) + " diffs traites (" + str(cpt_trads) + " traductions)")
  
  with open(os.path.join(folder, "stats-trads-res-after-diffs.txt"), "w+", encoding="utf-8") as f:  
    res = "Résultats des stats sur les résumés d'édition tronqués (ajout de traductions par lots) :\n"
    res += "Traductions ajoutées : " + str(cpt_trads) + "\n"
    res += "Traductions ajoutées par des utilisateurs inscrits : " + str(count_contributors) + "\n"
    res += "Traductions ajoutées par des utilisateurs non inscrits : " + str(count_ips) + "\n"
    f.write(res)
    
  with open(os.path.join(folder, "stats-langs-after-diffs.txt"), "w+", encoding="utf-8") as f:
    f.write(str(stats_langs))
  
  with open(os.path.join(folder, "stats-months-after-diffs.txt"), "w+", encoding="utf-8") as f:
    f.write(str(stats_dates))
  
  with open(os.path.join(folder, "stats-trads-contributors-after-diffs.txt"), "w+", encoding="utf-8") as f:
    f.write(str(stats_contribs))
      
  with open(os.path.join(folder, "stats-trads-ips-after-diffs.txt"), "w+", encoding="utf-8") as f:
    f.write(str(stats_ips))
    
if __name__ == '__main__':
    start_time = time.time()
    count_trads_per_diff()
    print("--- %s seconds ---" % (time.time() - start_time))

    # Création des fichiers de stats
    raw_results_to_wikitable(file_dates, dest='wikitable-dates.txt', header1='Mois', header2='Traductions ajoutées')
    raw_results_to_wikitable(file_langs, dest='wikitable-langs.txt', header1='Langue', header2='Traductions ajoutées')
    raw_results_to_wikitable(file_ips, dest='wikitable-ips.txt', header1='Utilisateur non enregistré', header2='Traductions ajoutées')
    raw_results_to_wikitable(file_contributors, dest='wikitable-contributors.txt', header1='Utilisateur enregistré', header2='Traductions ajoutées')
    
    # Disabling graphs generation, as the Graph extension is disabled on Wikimedia wikis
    # as of 1/2024 due to security issues.
    # generate_langs_graph()
    # generate_langs_graph(15, 15, filename_var='langs2')
    # generate_dates_graph()

Ce script a pris ~20 minutes à s’exécuter (Windows 10, processeur quadricore (2.3GHz), 8 Go de RAM)

Il fait référence au fichier graph_template.txt suivant pour la génération des graphiques :

{{#tag:graph|
{
  "version": 4,
  "width": 1000,
  "height": 200,
  "padding": {"top": 20, "left": 65, "bottom": 60, "right": 10},
 
  "data": [
    {
      "name": "table",
      "values": [
__TO_REPLACE__
      ]
    }
  ],
 
  "signals": [
    {
      "name": "tooltip",
      "init": {},
      "streams": [
        {"type": "rect:mouseover", "expr": "datum"},
        {"type": "rect:mouseout", "expr": "{}"}
      ]
    }
  ],
 
  "predicates": [
    {
      "name": "tooltip", "type": "==",
      "operands": [{"signal": "tooltip._id"}, {"arg": "id"}]
    }
  ],
 
  "scales": [
    { "name": "xscale", "type": "ordinal", "range": "width",
      "domain": {"data": "table", "field": "__LABEL__"} },
    { "name": "yscale", "type": "linear", "range": "height",
      "domain": {"data": "table", "field": "amount"} }
  ],
 
  "axes": [
    { "type": "x", "scale": "xscale"__OBLIQUE_LABELS__},
    { "type": "y", "scale": "yscale" }
  ],
 
  "marks": [
    {
      "type": "rect",
      "from": {"data":"table"},
      "properties": {
        "enter": {
          "x": {"scale": "xscale", "field": "__LABEL__"},
          "width": {"scale": "xscale", "band": true, "offset": -1},
          "y": {"scale": "yscale", "field": "amount"},
          "y2": {"field": {"group": "height"} }
        },
        "update": { "fill": {"value": "steelblue"} },
        "hover": { "fill": {"value": "red"} }
      }
    },
    {
      "type": "text",
      "properties": {
        "enter": {
          "align": {"value": "center"},
          "fill": {"value": "#333"}
        },
        "update": {
          "x": {"scale": "xscale", "signal": "tooltip.__LABEL__"},
          "dx": {"scale": "xscale", "band": true, "mult": 0.5},
          "y": {"scale": "yscale", "signal": "tooltip.amount", "offset": -5},
          "text": {"signal": "tooltip.amount"},
          "fillOpacity": {
            "rule": [
              {
                "predicate": {"name": "tooltip", "id": {"value": null} },
                "value": 0
              },
              {"value": 1}
            ]
          }
        }
      }
    }
  ]
}
| mode=interactive }}