Grâce au projet eBook Reader Dictionaries, j'ai pu découvrir pas mal de petites coquilles par-ci, par-là. J'avais commencé à corriger manuellement chaque définition, mais il y avait trop à faire finalement. Du coup, voici un script pour automatiser de petites modifications sur le Wiktionnaire.

Avant de commencer, il faudra créer un fichier user-config.py au même endroit que le futur script Python que je vais présenter. Son contenu est relativement simple :

family = "wiktionary"
mylang = "fr"
usernames[family][mylang] = "MSchoentgen"

Ensuite, il y a ces pré-requis :

  1. utiliser Python 3.6 minimum ;
  2. avoir installé le module pywikibot :
    python -m pip install -U --user pywikibot

Et voici le script update-bot.py :

"""
Automate Wiktionary content update.

The file user-config.py must be created next to that script with:

```
family = "wiktionary"
mylang = "fr"
usernames[family][mylang] = "MSchoentgen"
```

Note: you need to adapt those values, of course!

All details are here: https://www.mediawiki.org/wiki/Manual:Pywikibot/user-config.py
"""

__version__ = "0.3.0"
__copyright__ = "Copyleft 2020, Mickaël Schoentgen"

import re
import sys
from collections import defaultdict
from pathlib import Path
from typing import List

from pywikibot import OtherPageSaveError, Page, PatchManager, Site, bot_choice

config = {}
usernames = defaultdict(dict)
exec(
    (Path(__file__).parent / "user-config.py").read_text(),
    {"usernames": usernames},
    config,
)
lang = config["mylang"]

# Commit messages
if lang == "fr":
    MSG_SPACE = "Suppression d'un espace superflu"
    MSG_SPACES = "Suppression des espaces superflus"
    MSG_UNI_200E = r"Suppression du caractère unicode \u200e (LEFT-TO-RIGHT MARK)"
else:
    MSG_SPACE = "Deletion of a superfluous space"
    MSG_SPACES = "Deletion of superfluous spaces"
    MSG_UNI_200E = r"Deletion of the unicode \u200e character (LEFT-TO-RIGHT MARK)"


def handle_changes(page: Page, new_content: str, msg: str, skip_confirmation: bool = False) -> None:
    """Display changes and ask for confirmation before uploading them."""
    patch = PatchManager(page.text, new_content, by_letter=True, replace_invisible=True)

    if skip_confirmation:
        patch.print_hunks()
    else:
        try:
            patch.review_hunks()
        except bot_choice.QuitKeyboardInterrupt:
            # Press CTRL-C to abort
            sys.exit(0)

        # None of the changes were accepted, then there is nothing to update
        if all(h.reviewed == h.NOT_APPR for h in patch.hunks):
            return

    page.text = new_content if skip_confirmation else "".join(patch.apply())
    try:
        page.save(msg)
    except OtherPageSaveError as exc:
        # Try to handle several several errors (it should work when retrying)
        #   - abusefilter-disallowed: This action has been automatically identified as harmful, ...
        print("ERROR", exc)
    print("")


def sanitize_space(page: Page) -> None:
    """Remove superfluous spaces in templates."""
    original_content = page.text

    # {{ foo}} -> {{foo}}
    # [[ foo]] -> [[foo]]
    new_content = re.sub(r"([{\[])( +)", r"\1", original_content)
    # {{foo }} -> {{foo}}
    # [[foo ]] -> [[foo]]
    new_content = re.sub(r"(?: +)([}\]])", r"\1", new_content)
    # {{foo| bar}} -> {{foo|bar}}
    # [[foo| bar]] -> [[foo|bar]]
    new_content = re.sub(r"(?<!\|\[)(\|)( +)", r"\1", new_content)
    # {{foo |bar}} -> {{foo|bar}}
    # [[foo |bar]] -> [[foo|bar]]
    new_content = re.sub(r"(?: +)(\|)(?!\|\])", r"\1", new_content)
    # bla   bla -> bla bla
    new_content = re.sub(r" {2,}", " ", new_content)
    # bla bla bla     -> bla bla bla
    new_content = "\n".join([l.strip() for l in new_content.splitlines()])

    if new_content != original_content:
        diff = abs(len(original_content) - len(new_content))
        msg = MSG_SPACES if diff > 1 else MSG_SPACE
        handle_changes(page, new_content, msg)


def sanitize_unicode(page: Page) -> None:
    """Remove useless unicode characters."""
    original_content = page.text
    new_content = original_content.replace("\u200e", "")

    if new_content != original_content:
        handle_changes(page, new_content, MSG_UNI_200E, skip_confirmation=True)


def main(words: List[str]) -> int:
    """Entry point."""
    if not words:
        # Load words from raw logs of the GitHub Action
        file = Path("raw.log")
        if file.is_file():
            content = file.read_text(encoding="utf-8")
            words = re.findall(r"Wikicode of '([^']+)' \(", content)
            words.extend(re.findall(r'Wikicode of "([^"]+)" \(', content))

    if not words:
        # Load words from a text file, one word by line
        file = Path("words.txt")
        if file.is_file():
            content = file.read_text(encoding="utf-8")
            words = [w.strip() for w in content.splitlines()]

    site = Site(code=f"{config['family']}:{lang}", user=usernames[config["family"]][lang])
    words = set(words)
    total = len(words)
    for idx, word in enumerate(sorted(words), 1):
        print(f">>> [{idx}/{total}] {word!r}")
        page = Page(site, title=word)
        # Note: page.text is updated in-place when there are changes
        sanitize_unicode(page)
        sanitize_space(page)

    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

Comment ça fonctionne ?

$ python update-bot.py
$ python update-bot.py WORD
$ python update-bot.py WORD WORD WORD ...

Si aucun mot n'est passé en argument, alors le script vérifiera si le fichier "raw.log" existe, puis "words.txt". Dès qu'il trouvera un de ces fichiers, il l'utilisera.

Le premier fichier, raw.log, ne devrait pas aider grand monde, il s'agit des logs exportés depuis cette action GitHub.
Par contre le second fichier, words.txt, peut être intéressant : il s'agit d'une liste de mots (un mot par ligne).


Le script contient ces fonctions qui s'occupent de faire le ménage :

  1. sanitize_space() supprime les espaces superflus dans les modèles ({{ foo | bar|baz }}{{foo|bar|baz}}) ;
  2. sanitize_unicode() supprime les caractères unicode LEFT-TO-RIGHT (U+200E).

Dès qu'une définition est modifiée, et seulement si elle l'est, une différence des modifications avant/après est affichée et une confirmation est demandée avant d'envoyer la mise à jour sur le Wiktionnaire.

Vous pouvez tout à fait ajouter d'autres fonctions ; et pensez aussi à personnaliser les message de commit en début de script. ♠


Historique

  • 2020-12-27 : correction de l'oubli de l'import de OtherPageSaveError ; adaptation des commentaires dans sanitize_space(), retour en arrière concernant la regexp des espaces ('(\s+)''( +)') ; suppression des espaces multiples ; suppression des espaces en début et fin de lignes ; affichage d'une revue intéractive pour les modifications qui seront envoyées (au lieu de simplement afficher le diff) ; envoi automatique des changements liés à sanitize_unicode() et suppression de l'utilisation de la variable d'environnement AUTOMATIC=1.
  • 2020-09-19 : adaptation des messages suivant la langue définie dans le fichier user-config.py.
  • 2020-09-18 : amélioration des regex pour supprimer les espaces ('( +)''(\s+)') ; prise en charge des modèles [[foo]].
  • 2020-06-24 : amélioration de la regex pour trouver les mots dans le fichier raw.log ('(.+)''([^']+)') ; prise en charge des mots contenant une simple quote dans le fichier raw.log.
  • 2020-06-19 : gestion des exceptions OtherPageSaveError dans handle_changes() et ajout de la possibilité d'automatiser les réponses en passant la variable d'environnement AUTOMATIC=1.