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 the 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.1.1"
__copyright__ = "Copyleft 2020, Mickaël Schoentgen"

import os
import re
import sys
from pathlib import Path
from typing import List

from pywikibot import Page, PatchManager, Site

# Colors
END = "\033[0m"
BLUE = "\033[33m"
YELLOW = "\033[36m"

# Confirmation question
ASK = f"Envoyer {BLUE}{{}}{END} modification(s) [{YELLOW}{{}}{END}] ? o/N/q : "

# Commit messages
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)"


def confirm(changes_count: int, msg: str) -> bool:
    """
    Display a confirmation question and return the boolean answer.
    Set the AUTOMATIC=1 envar to confirm all changes without question.
    """
    if os.getenv("AUTOMATIC") == "1":
        return True

    question = ASK.format(changes_count, msg)
    proceed = input(question).lower()

    if proceed == "q":
        # The user wants to quit
        sys.exit(0)

    return proceed in ("o", "y", "1")


def handle_changes(page: Page, new_content: str, msg: str) -> None:
    """Display changes and ask for confirmation before uploading them."""
    # Show a beautiful diff
    PatchManager(page.text, new_content, replace_invisible=True).print_hunks()

    # Save the changes if I want to :)
    changes_count = len(page.text) - len(new_content)
    if confirm(changes_count, msg):
        page.text = new_content
        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}}
    new_content = re.sub(r"({)( +)", r"\1", original_content)
    # {{foo }} -> {{foo}}
    new_content = re.sub(r"( +)(})", r"\2", new_content)
    # {{foo| bar}} -> {{foo|bar}}
    new_content = re.sub(r"(?<!\|)(\|)( +)", r"\1", new_content)
    # {{foo |bar}} -> {{foo|bar}}
    new_content = re.sub(r"( +)(\|)(?!\|)", r"\2", new_content)

    if new_content != original_content:
        msg = MSG_SPACES if len(original_content) - len(new_content) > 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)


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()
    total = len(words)
    for idx, word in enumerate(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-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.