Solution de l’exercice d’hier 10


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.

10 thoughts on “Solution de l’exercice d’hier

  • zanguu

    Sans vouloir faire mon chieur (mais un peu quand même), pourquoi ces deux lignes :
    texte = re.sub('[^%s]' % string.ascii_lowercase, ' ', texte.lower())

    for i, e in enumerate(texte.split()):

    alors que ceci me semble plus “opti” :
    for i, e in enumerate(re.findall('[\w]+', texte.lower())):
    vu qu’on ne passe qu’on parse qu’une fois le texte.
    Surtout que .encode('ascii', 'ignore') à viré tout ce qui pouvais interférer avec la regex.

    Et j’ajouterai que tu m’as menti!

    @zanguu : “c’est” est une contraction de deux mot : “cela est”, donc c’est bien deux mots.

    mais quand je fais tourner ton code j’ai :

    – quil: 39
    – jabattrai: 65
    – leternel: 98
    – cest: 175, 199, 208, 234

    ce que j’ai personnellement géré grace à :
    u''.join(c for c in unicodedata.normalize('NFKD', texte) if not unicodedata.combining(c ) et qui m’autorise à oublier le .encode('ascii', 'ignore')

    PS: wouhou je suis dans l’article… et je casse les couilles après !

  • Sam Post author

    Arf, j’ai merdé. Amen. J’aurais du les remplacer par un espace.

    Findall est une meilleure solution en effet, par contre pas avec \w qui garde les underscores. Il faut une regex plus perfectionnée.

  • zanguu

    Ah pas faux, j’oublie toujours ce détail.
    Étant donné qu’on a fait un .lower() on peut utiliser ‘[a-z\d]+’.
    Mais vu que ton texte ne comportait pas de ‘_’ j’y ai pas pensé.

  • Sam Post author

    @zanguu: oh le vilain, j’ai refait tourné le code, et j’obtiens bien “c” et “est” séparément.

    Ce qui est logique puisque exte = re.sub('[^%s]' % string.ascii_lowercase, ' ', texte.lower()) remplace l’apostrophe par un espace. Vil ! Perfide ! Enculé !

  • zanguu

    Mais, monsieur le juge, je jure c’est la véritée.
    Chez moi ton normalize me donne ça :

    « La marche des vertueux est semée d’obstacles qui sont les entreprises égoïstes

    (c’est con le ‘i’ de ‘égoïstes’ avec trois points ne passe pas au c/c)
    et le encode ascii le transforme en :

    La marche des vertueux est semee dobstacles qui sont les entreprises egoistes

    du coup ton sub ne remplace que la ponctuation et le ‘-‘ de ‘Tout-Puissant’ vu que le reste existe plus.

    PS: mon python est le suivant (si ça joue): Python 2.7.3 (default, Sep 26 2013, 20:08:41) [GCC 4.6.3] on linux2

    Et je voudrais finir ma défense avec la citation d’un homme célèbre:

    Si vous ne savez pas ce que contient une variable, vous ne comprenez pas le programme

  • zanguu

    Trouvé…
    J’ai copié le texte depuis la news et collé dans sublime text.
    Sauf que dans le texte les apostrophes sont des
    >>> unicodedata.name('’'.decode('utf8'))
    'RIGHT SINGLE QUOTATION MARK'

    et quand je tape une apostrophe dans ST j’ai
    >>> unicodedata.name('\''.decode('utf8'))
    'APOSTROPHE'

    Comme quoi y’a pas que l’encoding qui fait chier dans les fichiers de texte…

Leave a comment

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.