Du code, du communisme

Solution de l’exercice d’hier

Il faut bien noter que ce n’est qu’une solution parmi d’autres :

import re
import sys
import string
import unicodedata
mots = {}
texte = open(sys.argv[1]).read().decode('utf8').replace(u'œ', 'oe')
texte = unicodedata.normalize('NFKD', texte).encode('ascii', 'ignore')
texte = re.sub('[^%s]' % string.ascii_lowercase, ' ', texte.lower())
for i, e in enumerate(texte.split()):
    mots.setdefault(e, []).append(i)
mots = sorted(mots.items(), key=lambda x: (len(x[1]), sorted(x[1])))
for mot, positions in mots:
    print('- %s: %s' % (mot, ', '.join(map(str, positions))))

On ignore cordialement toute gestion d’erreur, donc le code peut se permettre d’être court. Et .replace(u'œ', 'oe') n’est pas très générique 🙂
Dans les propositions de code des commentaires, il faut noter :

  • Une utilisation fort maline du defaultdict par bob.
  • Le signalement d’unidecode par zanguu qui aurait géré 'œ' sans problème. Mais ça rajoute une dépendance.

Décorticage :

import re
import sys
import string
import unicodedata
# On va tocker les mots dans ce dico
mots = {}
# Je récupère en vrac le contenu du fichier. Comme on a pas de gestion des
# erreurs, je récupère cash pistache le chemin de la ligne de commande
# et je suppose un encoding en UTF8. Le résultat obtenu est un objet
# unicode de tout le texte du fichier, sans le caractère 'œ'.
texte = open(sys.argv[1]).read().decode('utf8').replace(u'œ', 'oe')
# Astuce pour normaliser les caractères spéciaux. Ne marche que pour
# l'alphabet latin malheureusement. Donc le script est limité. Unidecode
# permettrait d'avoir un script plus générique.
texte = unicodedata.normalize('NFKD', texte).encode('ascii', 'ignore')
# string.ascii_lowercase contient toutes les lettres ASCII en minuscule,
# ce qui permet de faire un remplacement, via regex, de
# [^abcdefghijklmnopqrstuvwxyz]', c'est à dire tout ce qui n'est pas
# une lettre ASCII minuscule.
texte = re.sub('[^%s]' % string.ascii_lowercase, ' ', texte.lower())
# Je récupère tous les "mots", split() sans paramètre coupe en effet toute
# combinaison de caractères non imprimables. enumerate() me permet d'avoir
# la position de chaque mot. setdefault() me permet d'ignorer les clés qui
# n'existent pas encore dans le dico. J'aurais pu utiliser un defaultdict, mais
# comme on a qu'une seule ligne ici, c'est plus court.
# J'obtiens donc un dico {mot1: [positon1, position2, ...], mot2: ...}
for i, e in enumerate(texte.split()):
    mots.setdefault(e, []).append(i)
# On récupère le contenu du dico sous forme de liste de tuples
# [(mot, positions)...], et on l'ordonne selon le nombre d'apparitions
# (len(x[1])), ou a défaut par ordre naturel des apparitions sorted(x[1]).
# Pour rappel, key attend une fonction qui prend chaque élement, et retourne
# une clé. La clé est utilisée pour ordonner les éléments : chaque élément
# voit sa clé comparée à celle des autres, et ordonnée par ordre naturel.
# Y a un article sur ça : http://sametmax.com/ordonner-en-python/
# En gros, une entrée ('salut', 4, 18) aura pour clé (2, (4, 18)),
# ce que Python peut comparer facilement.
# Je réalise en rédigeant ces lignes que mon sorted est inutile, puisque
# le processus est incrémental et déjà ordonné. Je le laisse comme référence.
mots = sorted(mots.items(), key=lambda x: (len(x[1]), sorted(x[1])))
# Et on affiche tout ça, non sans caster les positions du type int vers str
# pour éviter un crash
for mot, positions in mots:
    print('- %s: %s' % (mot, ', '.join(map(str, positions))))

Enoncé de l’exercice.
Télécharger le code de l’article.