Journal Analyse de texte

Posté par  . Licence CC By‑SA.
Étiquettes :
13
15
déc.
2015

Mon but est d’analyser la fréquence de syllabes ou de mots depuis différents textes. Les fréquences d’apparition de chaque mot/syllabe étant cumulées dans une base de données.

À chaque fois que j’ai posté du code ici je n’ai reçu que des critiques constructives alors je vous soumets celui-là :)

Le code fait ce que je lui demande mais il est très lent sur de gros textes. Je pense que le problème se situe dans mes interactions avec la base de données. J’ai quelques une idée pour remédier à ce problème, vous verrez plus bas, vous me donnerez peut-être votre avis.

#!/usr/bin/env python3

"""Reader.py: Read text for strings frequency."""
__author__ = "M4rotte"
__copyright__ = "Copyright 2015, Institut Marotte pour un Mouling de Qualitäy"
__license__ = "GPL"
__version__ = "0.1"

import sys                                  # SYS module (used for argument management)
import re                                   # Regular expressions
import sqlite3                              # SQLite
import time                                 # Time is something you've never have enough
from collections import deque               # Read buffer

MAXLEN = 24
KEEPMAXLEN = 3 # Over this length we only keep strings which don't match following regexp to avoid too many splitted words
reKEEP = re.compile(r'^( |[A-ZÉÀÔ])(.*)( |\n|\.|,|\;)$')
buf = deque(['' for s in range(MAXLEN)])
counter = 0
string = ''
strings = {}
strings_ok = {}
dbfile = './reader.sqlite'

### Read input and populate `strings{}`
try:
  while True:
    c = sys.stdin.read(MAXLEN-1)
    if c:
      for i in range (0,len(c)):
        buf.popleft()
        buf.append(c[i])
      #print(buf)
      for i in range (MAXLEN-len(c)-1, MAXLEN):
        string = ''
        for j in range (i, MAXLEN):
          try:
            #print("(i,j):"+str(i)+","+str(j))
            if buf[i]:
              string += buf[j]
              s = string.replace ("\n"," ")
              string = s.replace("  "," ")
            if len(string) > 1:
              strings[string] += 1
              #print ("ANOTHER:"+string)
          except KeyError:
            strings[string] = 1
            #print ("NEW:"+string)            
      #print(strings)
      counter += 1
    else:
      break

except KeyboardInterrupt as e:
  sys.stderr.write (repr(e))
  sys.stderr.flush()
  sys.stdout.flush()
  exit(1)

sys.stderr.write (str(counter) + "\t×"+ str(MAXLEN) + " bytes read.\n")
sys.stderr.write (str(len(strings)) + "\traw strings.\n")

### Remove unwanted strings
for s in strings:
  if len(s) < KEEPMAXLEN:
    strings_ok[s] = strings[s]
  elif reKEEP.match(s):
    strings_ok[s] = strings[s]
    #print(s) 

### Free some memory
buf.clear()
strings.clear()

sys.stderr.write (str(len(strings_ok)) + "\tOK strings.\n")

# Open database
connection = sqlite3.connect(dbfile)
cursor = connection.cursor()
# Create SQLite table if not exists, to store shits…
cursor.execute('create table if not exists strings (string text primary key, freq int)')  

# Store `strings_ok{}` into database
inserted = 0
updated = 0
for s in strings_ok:
  try:
    cursor.execute("insert into strings values (?, ?)",(s, strings_ok[s]))
    inserted += 1
  except sqlite3.IntegrityError:
    updated += 1
    cursor.execute("update strings set freq=freq+? where string like ?",(strings_ok[s],s))
  if ((inserted+updated) % 100 == 0):
    prog = (inserted+updated)/len(strings_ok)*100
    sys.stderr.write ("Database update: "+str(round(prog,1))+"%\r")
    sys.stderr.flush()
print("Database update:\tOK.      ")

cursor.execute("select count(*) from strings")
count = cursor.fetchone()[0]

sys.stderr.write (str(inserted)+"\tstrings inserted in database.\n")
sys.stderr.write (str(updated)+"\tstrings updated in database.\n")
sys.stderr.write (str(count)+"\trecords in database.\n")

connection.commit()
connection.close()

Je pense que la grosse erreur se situe ici :

cursor.execute("update strings set freq=freq+? where string like ?",(strings_ok[s],s))

ce doit être une requête coûteuse à cause de la clause where

Donc mon idée serait de reporter l’incrément des compteurs au niveau du code Python, ce qui ne nécessite qu’un select, puis de remplacer l’update par un insert or replace

  • # Erratum

    Posté par  . Évalué à 2.

    Le premier commentaire est erroné :

    s/~which don't match~/which match/

  • # En memoire

    Posté par  . Évalué à 3. Dernière modification le 15 décembre 2015 à 04:51.

    Pourquoi ne pas stocker l'information en memoire (et ne la mettre dans la base de donnee qu'a la fin). J'imagine effectivement que le goulet doit se situer dans la mise a jour de la base de donnee.

    A priori, ta structure de donnee comptant les syllabes/mots ne devraient pas augmenter lineairement avec la taille de tes textes (si ma memoire est bonne, la taille doit augmenter en logarithme, distribution de Zipf, toussa). Un bete dict devrait faire l'affaire, mais python a des structures plus subtiles pour compter je crois.

    Ca devrait etre assez simple a tester (et tu devrais pouvoir chronometrer le temps passer sur la mise a jour de ta table egalement—meme juste en utilisant time.time()).

    [edit: vas y efface, c'est ce que tu fais deja j'ai mal lu ton code]

    • [^] # Re: En memoire

      Posté par  . Évalué à 2.

      [edit: vas y efface, c'est ce que tu fais deja j'ai mal lu ton code]

      Euh… bah non :)

      Utiliser time() je sais faire mais je ne l’ai pas fait ici, je lance simplement le script précédé de la commande time. Vu la sortie d’erreur je sais à peu près combien de temps prend chaque étape. Je remarque nettement que ça va à une vitesse correct quand la base est vide (donc que les INSERT aboutissent).

      A priori, ta structure de donnee comptant les syllabes/mots ne devraient pas augmenter lineairement avec la taille de tes textes

      Oui. Plus les textes passeront plus il aura de mises à jour et moins d’insertions.

      Pourquoi ne pas stocker l'information en memoire (et ne la mettre dans la base de donnee qu'a la fin)

      C’est ce que je vais faire.

      • [^] # Re: En memoire

        Posté par  . Évalué à 1.

        Attention avec les dict ça bouffe très vite énormément de mémoire. Si tu n'as pas besoin des clefs et que tu peux te contenter des list fais le.

        A titre d'exemple une liste de 16 000 000 dict de 7 éléments pèse plus de 32go en RAM, en remplaçant simplement les dict par des list on passe a 500Mo-800Mo de RAM.

        De plus tu utilise sqlite, je ne connais pas les performance de ce moteur pour les update mais peut être que choisir mysql(ou mariadb) avec le bon moteur sur la table tu dois pouvoir réduire la charge.

        • [^] # Re: En memoire

          Posté par  . Évalué à 2.

          Attention avec les dict ça bouffe très vite énormément de mémoire.

          Oui… quand on parcourt toutes les combinaisons possibles rien qu’entre caractères lus ça va vite très vite.

          Pour la mémoire j’ai abandonné le deque pour stocker mon tampon, le problème étant qu’un deque n’est pas adressable à l’aide de slices ([:4])… contrairement à un string. J’ai bien profité du conseil concernant collections.Counter(), c’est vraiment le type de variable dont j’avais besoin.

          J’ai un peu changé d’approche pour conserver les « strings » (sentences), je me base sur des expressions régulières, c’est plus propre. Dès que j’ai un truc correct je le postici.

          De plus tu utilise sqlite, je ne connais pas les performance de ce moteur pour les update mais peut être que choisir mysql(ou mariadb)

          Alors là attention, que ça puisse fonctionner plus ou ou moins vite sous MariaDB c’est clairement pas un angle d’étude pour le moment… J’utilise sqlite parce que c’est parfaitement adapté à la modélisation (et c’est par ailleurs l’un des meilleurs moteur opensource de base relationnelle). SQLite ça marche partout. sqlite passe le test ACID : https://fr.wikipedia.org/wiki/Propri%C3%A9t%C3%A9s_ACID je n’en suis pas si sûr pour MarioleDB…

          • [^] # Re: En memoire

            Posté par  . Évalué à 3.

            SQLite ça marche partout. sqlite passe le test ACID : https://fr.wikipedia.org/wiki/Propri%C3%A9t%C3%A9s_ACID je n’en suis pas si sûr pour MarioleDB…

            Tu n'as pas besoin des propriété ACID. Il n'y a qu'un utilisateur à la fois sur ta db, non ?

            Mais sinon, je te rejoins là-dessus pourquoi Maria DB serait mieux. Peut-être un redis qui travaille au maximum en mémoire avec un système clé / valeur.

            • [^] # Re: En memoire

              Posté par  . Évalué à 2.

              Tu n'as pas besoin des propriété ACID. Il n'y a qu'un utilisateur à la fois sur ta db, non ?

              Pour l’instant.

              Entendons nous bien, ce n’est pas pour ce caractère que j’ai pris SQLite. En plus des raisons déjà évoquées c’est que MariaDB je connais pas mal alors que SQLite je découvre un peu.

              Peut-être un redis qui travaille au maximum en mémoire avec un système clé / valeur.

              Ou encore BerkeleyDB. Mais je ne veux pas me limiter à un système clé-valeur pour les autres fonctionnalités que j’ai en tête. Je compte utiliser les tables virtuelles avec recherche de texte (FTS), par exemple.

              • [^] # Re: En memoire

                Posté par  . Évalué à 2.

                Ou encore BerkeleyDB. Mais je ne veux pas me limiter à un système clé-valeur pour les autres fonctionnalités que j’ai en tête. Je compte utiliser les tables virtuelles avec recherche de texte (FTS), par exemple.

                Ah, tiens une db clé/valeur comme librairies embarquée. C'est intéressant.

                Si tu veux aller plus loin dans l'analyse de texte une db clé valeur n'est peut-être pas la meilleur piste.

                Moi, pour faire une application un peu complexe, j'utiliserais Solr et surtout la bibliothèque Lucène, mais je ne sais pas si il y a un binding python. Je pense que ça fait tout :-)

                Et c'est cette librairie qui est utilisée dans partie recherche textuelle pour des applications comme cassandra. Et je la soupçonne d'être partout où il y a de la recherche de texte qui marche bien. Github peut-être.

    • [^] # Re: En memoire

      Posté par  . Évalué à 4.

      A priori, ta structure de donnee comptant les syllabes/mots ne devraient pas augmenter lineairement avec la taille de tes textes (si ma memoire est bonne, la taille doit augmenter en logarithme, distribution de Zipf, toussa). Un bete dict devrait faire l'affaire, mais python a des structures plus subtiles pour compter je crois.

      Dict se base sur une table de hashage.

      Il y a aussi le Trie (pas tree!) qui travaille avec la même interface mais utilise une autre structure de données qui est peut-être plus efficace en terme d'espace utilisé.

      C'est un arbre basé sur les lettres du mot. Par exemple si j'insère valide, valeur et vanne ça donne ça

      v                 v                      v
      a                 a                      a
      l      ->         l            ->     l     n
      i               e   i               e   i     n
      d              u     d             u     d     e
      e             r       e           r       e
      

      On comprends que si les mots de sont pas trop long c'est assez rapide de les retrouver. Et la structure semble assez économe en espace mémoire.

      Voilà le lien vers une implémentation python: https://pypi.python.org/pypi/patricia-trie/8

  • # une seule exécution SQL

    Posté par  (site web personnel, Mastodon) . Évalué à 10.

    Il est toujours plus lent d'exécuter plusieurs requêtes SQL en plusieurs fois.

    Trois pistes :
    - utilise "insert or update" au lieu de "insert"
    - bufferise tes requêtes SQL dans une chaîne de caractères et exécute les toutes en une seule fois avec la fonction executemany()
    - utilise des noms de variables moins génériques que string ou buf qui degradent la lisibilité du code et compliquent donc la démarche de t'aider.

    Enfin… C'est juste un avis de moule qui se dit que plus est de fous moins il y a de riz… Et accessoirement que même qqun qui ne connait pas SQLite pourra mieux t'aider si la lecture de ton code est immédiate.

  • # Commentaire supprimé

    Posté par  . Évalué à 6.

    Ce commentaire a été supprimé par l’équipe de modération.

    • [^] # Re: nltk

      Posté par  . Évalué à 2.

      Gère-t-elle le français et le découpage en syllabes (si c’est vraiment ce dont Marotte a besoin) ?

      « Le fascisme c’est la gangrène, à Santiago comme à Paris. » — Renaud, Hexagone

      • [^] # Re: nltk

        Posté par  (site web personnel) . Évalué à 3.

        Pour le traitement d'un texte, si considérer le texte comme un sac de mots te suffit (et donc perdre les informations liées à la progression du texte), tu peux le découper (tokenize) en une liste de mots et ensuite compter les occurrences des mots. Pour les syllabes, c'est plus difficile en fait, ça dépend si tu ne souhaites que les syllabes à l'intérieur des mots ou si les liaisons t'intéressent. Si les liaisons t'intéressent, il te faut conserver la progression du texte et réfléchir aux liaisons possibles (mais bon, entre celles possibles et réalisées, il y a des différences importantes selon les régions de France où tu te trouves et les classes socio-culturelles… y'a pas mal d'articles sur le sujet aussi.) ; si c'est simplement de règles pseudo-grammaticales/normatives de découpage syllabique qui t'intéresse à l'intérieur d'un mot, ça doit se faire en une expression régulière à peine complexe :)

        Sinon, NLTK gère le français et les fréquences distributionnelles, il peut même te sortir des graphiques assez rapidement. Voir ça par exemple : http://www.nltk.org/api/nltk.html#nltk.probability.FreqDist

        • [^] # Re: nltk

          Posté par  . Évalué à 3.

          si considérer le texte comme un sac de mots te suffit

          Comme un sac de caractères même… :)

          Je me pencherai sur NLTK un de ces jours ça a l’air bien mais là je veux une approche plus « bête », basée seulement sur les fréquences d’apparition.

    • [^] # Re: nltk

      Posté par  . Évalué à 2.

      Très intéressant, merci.

  • # Je ne ferais pas ça comme ça.

    Posté par  . Évalué à 4.

    Mon but est d’analyser la fréquence de syllabes ou de mots depuis différents textes.

    Les syllabes ? Les mots ? Les deux ? Dans la même base ???

    Je pense que le problème se situe dans mes interactions avec la base de données.

    As-tu au moins essayé de mettre une trace entre l’analyse du texte et la mise à jour de la base de données pour en être sûr ?

    Pour l’analyse du texte, je ne dis pas que ça impacte les performances (normalement, ça les améliorerait, mais avec le coup du remplacement de chaîne relancé à chaque caractère, c’est moins sûr), mais tu fais un truc genre prise de tête en C avec des MAXLEN, des buf… ce n’est même pas évident de voir ce que ça fait exactement et si c’est ce qui est attendu, alors que tu utilises un langage de script avec un type chaîne dynamique, des expressions régulières…
    Si tu veux la performance pure, ce n’est pas le meilleur langage. Si tu utilises un langage de haut niveau, autant en profiter pour faire quelque chose de plus compréhensible.

    Personnellement, je lirais l’entrée par ligne, je splitterais sur les espaces et signes de ponctuation (ou tout ce qui n’est pas une lettre), comme ça, j’aurais directement les mots et ensuite seulement, pour chaque mot, j’itérerais sur la lettre de départ et la longueur pour avoir les groupes de lettres.
    Là, il ne s’agirait que des groupes de lettres et pas vraiment des syllabes, mais je n’ai pas l’impression que ton code fasse mieux. Le découpage en véritables syllabes d’un mot (est-ce réellement ce dont tu as besoin ?) est un problème nettement moins trivial.

    Si je devais faire ça en C, je ne ferais pas comme toi non plus. Je lirais aussi par ligne (même avec le C, on peut !), voire je chargerais tout le texte en mémoire (mon premier ordinateur avait 8 Ko de mémoire, mais on n’en est plus là), j’itérerais sur le caractère de la ligne (ou du texte complet dans le deuxième cas), si ce n’est pas une lettre, je viderais le mot courant, sinon, je lui ajouterai la lettre et pour les groupes de lettres, j’itérerais sur la longueur jusqu’au caractère courant du mot. Ainsi, pas de remplacements de chaînes ni de popleft…

    « Le fascisme c’est la gangrène, à Santiago comme à Paris. » — Renaud, Hexagone

    • [^] # Re: Je ne ferais pas ça comme ça.

      Posté par  . Évalué à 3.

      As-tu au moins essayé de mettre une trace entre l’analyse du texte et la mise à jour de la base de données pour en être sûr ?

      Je le vois simplement grâce au sys.stderr.write(…) qui est fait entre chaque étape, la première étape (chargement) et la deuxième (suppression des chaînes qui coupent des mots en deux, sauf les petites…) s’exécutent en un temps acceptable.

      Personnellement, je lirais l’entrée par ligne, je splitterais sur les espaces et signes de ponctuation (ou tout ce qui n’est pas une lettre), comme ça, j’aurais directement les mots et ensuite seulement, pour chaque mot, j’itérerais sur la lettre de départ et la longueur pour avoir les groupes de lettres.

      En faisant ça si j’ai une première ligne qui contient "voici une première ligne" et une seconde qui contient "et là une deuxième" je n’aurais pas d’entrée créée contenant le dernier mot de la première et le premier de la deuxième ("ligne et"). Je n’aurais pas non plus 'e e', 'e et', etc… (je remplace systématiquement "\n" par "n"…)

      J’utilise donc un tampon de MAXLEN caractères qui se décale de MAXLEN-1 caractères à chaque fois afin que le dernier caractère du précédent tampon se retrouve premier du tampon suivant. Pour ça un deque me semblait plus adapté qu’un string

      je chargerais tout le texte en mémoire (mon premier ordinateur avait 8 Ko de mémoire

      Comme tu peux le voir le programme lit l’entrée standard, pour l’instant il attend EOF pour traiter les données mais à terme je veux qu’il puisse tourner en continue, en faisant un màj de la base tous les N tampons/caractères.

      • [^] # Re: Je ne ferais pas ça comme ça.

        Posté par  . Évalué à 3. Dernière modification le 15 décembre 2015 à 15:13.

        je n’aurais pas d’entrée créée contenant le dernier mot de la première et le premier de la deuxième ("ligne et"). Je n’aurais pas non plus 'e e', 'e et', etc

        je comprend pas bien ce que tu veux faire, au début parles de compter l’occurrence de chaque mot et syllabe mais en fait tu sembles vouloir compter n'importe quel groupe de lettres… C'est quoi le but?

        Tu n'aurais pas un petit exemple de texte avec le résultat obtenu?

        • [^] # Re: Je ne ferais pas ça comme ça.

          Posté par  . Évalué à 3.

          en fait tu sembles vouloir compter n'importe quel groupe de lettres…

          Oui c’est ça. N’importe quel groupe de caractères en fait.

          C'est quoi le but?

          Y’en a pas vraiment… je fais des essais on va dire…

          • [^] # Re: Je ne ferais pas ça comme ça.

            Posté par  . Évalué à 2.

            Je vais enfoncer une porte ouverte mais tu pourrai regarder du coté des algos de compression par dictionnaire. Il y a sûrement des idées à récupérer.

          • [^] # Re: Je ne ferais pas ça comme ça.

            Posté par  . Évalué à 1.

            Merci pour ta réponse.

            a priori, ça m'aurait semblé plus intéressant de compter le nombres d'apparitions de mots dans divers textes (ne serait-ce que pour vérifier la loi de zipf ;-)), que de compter la fréquence dans les textes étudiés d'une suite de caractères telles que "a ma".

            • [^] # Re: Je ne ferais pas ça comme ça.

              Posté par  . Évalué à 2.

              En incluant les suites telles que "a ma" je peux toujours par la suite filtrer avec une expression régulière pour n’avoir que les mots.

              • [^] # Re: Je ne ferais pas ça comme ça.

                Posté par  . Évalué à 3.

                Tu peux facilement exclure les suites de caractères qui contiennent autre chose que des lettres, mais je ne vois pas comment tu pourrais savoir qu’une suite de caractères qui n’en contient pas correspond à un mot complet.

                Si tu as « car » dans ta base, comment pourrais-tu savoir facilement si ça correspond au mot « car », à un monceau de « caractère », de « carrément » ou aux trois, avec plus ou moins d’occurrence chacun ?

                Il y a moyen de faire un traitement inversé du genre s’il y a deux fois la suite « caractère », il faut la soustraire deux fois du nombre d’occurrences de la suite « car » pour arriver finalement au nombre d’occurrences du mot « car », mais le mieux pour faire des traitements sur les mots entiers et de stocker à part les mots entiers…

                « Le fascisme c’est la gangrène, à Santiago comme à Paris. » — Renaud, Hexagone

                • [^] # Re: Je ne ferais pas ça comme ça.

                  Posté par  (site web personnel) . Évalué à 2.

                  Il y a moyen de faire un traitement inversé du genre s’il y a deux fois la suite « caractère »

                  je pense que c'est un cas d'utilisation auquel il n'avait pas encore pensé :-)

                  sympa de lui suggérer :p (mais ça ne va pas améliorer les perfs /o)

  • # On dirait du C

    Posté par  . Évalué à 10.

    for i in range (0,len(c)):
        buf.popleft()
        buf.append(c[i])

    ça s'écrit plutôt comme ça:

    for d in c:
        buf.popleft()
        buf.append(d)

    vu qu'on est en python, pas besoin de reprendre un idiome du C. De manière générale, dès que tu as "range(len(x))", tu sais que tu as quelque chose de moche en python (si tu as vraiment besoin de l'indice, tu peux toujours utiliser enumerate). Et par habitude, "c" c'est plutôt un caractère, du coup le mettre pour représenter une chaîne, ça choque l'œil.

    s = string.replace ("\n"," ")
    string = s.replace("  "," ")

    ça s'écrit:

    string = string.replace ("\n"," ").replace("  "," ")

    Pas besoin de tampon.

    while True:
        counter += 1

    il y a directement une fonction pour ça:

    from itertools import count
    for counter in count(start=0):

    Pour ce qui est de compter, tu as collections.Counter qui te permet d'éviter de vérifier si une clé est déjà dans ton dictionnaire (et d'incrémenter si c'est le cas où de mettre à 1 sinon): avec Counter, tu fais simplement compteur[clé] += 1 et ça marche toujours: si la clé n'est pas présente, elle est considérée valoir 0.

    Et enfin, parce que vraiment, on dirait du C ce code, tout le foin avec le buffer etc, ça me parait entièrement inutile. Tu peux parfaitement lire ligne par ligne l'entrée standard:

    import fileinput
    for line in fileinput.input():

    De manière générale, si tu crois que tu as besoin de gérer la mémoire toi-même en Python pour une tâche aussi simple, c'est que tu n'utilises pas les bons outils.

    • [^] # Re: On dirait du C

      Posté par  . Évalué à 3.

      Et pour l'affichage d'un message/erreur aussi :

      https://docs.python.org/3.1/whatsnew/3.0.html

      Message

      Python 2: print "The answer is", 2*2
      Python 3: print("The answer is", 2*2)

      Erreur

      Python 2: print >>sys.stderr, "fatal error"
      Python 3: print("fatal error", file=sys.stderr)

    • [^] # Re: On dirait du C

      Posté par  . Évalué à 2.

      Merci pour tous ces conseils avisés ! Surtout pour le collections.Counter je pense que ça va me servir…

      import fileinput
      for line in fileinput.input():

      Ma ligne peut avoir une longueur variable. J’aurais toujours besoin de la traiter en morceau de N caractères car je ne vais pas itérer sur tous les caractères de la ligne si je n’ai pas besoin de conserver des chaînes de plus de N caractères. J’ai également besoin d’avoir la fréquence de chaînes telles que "titi\ntoto" je devrais donc conserver un certain nombre de caractères de la ligne précédente à chaque fois…

      J’ai trouvé plus simple d’avoir une sorte de « fenêtre » (buf) qui va « glisser » sur mon entrée… Par exemple si je prends un MAXLEN de 2 et que mon entrée est "abcd\n" j’aurais tour à tour :

      • ab
      • bc
      • cd
      • d\n

      Avec MAXLEN à 3 j’aurais, en plus :

      • abc
      • bcd
      • cd\n

      si tu crois que tu as besoin de gérer la mémoire toi-même en Python

      Non, j’ai bien conscience que ce n’est pas à moi de gérer la mémoire.

      • [^] # Re: On dirait du C

        Posté par  . Évalué à 4. Dernière modification le 15 décembre 2015 à 21:05.

        Piqué sur http://stackoverflow.com/questions/6822725/rolling-or-sliding-window-iterator-in-python

        from itertools import islice
        
        line="abcd\nefg"
        
        def window(seq, n=2):
            "Returns a sliding window (of width n) over data from the iterable"
            "   s -> (s0,s1,...s[n-1]), (s1,s2,...,sn), ...                   "
            it = iter(seq)
            result = tuple(islice(it, n))
            if len(result) == n:
                yield result    
            for elem in it:
                result = result[1:] + (elem,)
                yield result
        
        for chunk in window(line, 2):
            print(chunk, ''.join(chunk))
        for chunk in window(line, 3):
            print(chunk, ''.join(chunk))
            from itertools import islice
        

        La sortie :

        ('a', 'b') ab
        ('b', 'c') bc
        ('c', 'd') cd
        ('d', '\n') d
        
        ('\n', 'e') 
        e
        ('e', 'f') ef
        ('f', 'g') fg
        ('a', 'b', 'c') abc
        ('b', 'c', 'd') bcd
        ('c', 'd', '\n') cd
        
        ('d', '\n', 'e') d
        e
        ('\n', 'e', 'f') 
        ef
        ('e', 'f', 'g') efg
        
  • # I like =

    Posté par  . Évalué à 4.

    Je remarque que tu utilises la clause like dans dans tes requêtes SQL.
    Je ne connais pas SQLite et son optimisation des requêtes, mais il me semble qu'il est préférable d'utiliser = comme comparateur si ton but est de vérifier l'égalité.

    Sauf erreur de ma part (j'ai lu entre les lignes), tu n'utilises pas de wildcard dans ton code.
    Si ce n'est déjà fait, il serait peut être aussi intéressant d'utiliser des indexes pour améliorer les performances des requêtes.

    • [^] # Re: I like =

      Posté par  . Évalué à 2.

      Et oublie le passage sur les indexes..

      • [^] # Re: I like =

        Posté par  . Évalué à 3.

        Je confirme pour le like. Ici, cela t'empêche d'utiliser l'index et donc tu te tapes un full scan de ta table à chaque update. Cela est très coûteux.
        Après, je ne connais pas très bien sqlite, je viens d'apprendre qu'il n'y a pas de "INSERT … ON DUPLICATE KEY UPDATE …" ce qui t'aurais bien aidé ici.

        Mais il y a une question que tu peux te poser. Aurais tu plus d'insert ou d'update ?
        Ton code exécute 2 requêtes pour un update et une seule pour un insert. Si tu prévois que l'update sera plus récurent, mieux vaut faire le contraire : update d'abord, et si le nombre de rows modifiés est 0, tu fais l'insert.
        Par exemple :

            cursor.execute("update strings set freq=freq+? where string = ?",(strings_ok[s],s))
            if (cursor.rowcount == 0):
              cursor.execute("insert into strings values (?, ?)",(s, strings_ok[s]))
              inserted += 1
            else:
              updated += 1

        Sinon, il y aurait une solution a une requête :

        cursor.execute("insert or replace into strings values (?, ifnull((SELECT freq from strings where string = ?),0)+?)",(s, s, strings_ok[s]))

        Mais elle ne semble pas performante.

        • [^] # Re: I like =

          Posté par  . Évalué à 2.

          Je viens de tester avec = à la place de like les performances sont bien meilleures effectivement :)

          Merci à vous deux. Le temps d’exécution est passé de 15 min à 40 s (environ)…

          Pour le nombre de chaque requête je ne sais pas encore, je garde en tête ta remarque très judicieuse sur l’ordre d’exécution.

      • [^] # Re: I like =

        Posté par  . Évalué à 3.

        Et oublie le passage sur les indexes..

        Oui, parce que un index a dû être créé automatiquement sur la colonne string car elle est définie comme primary key.

  • # occurrence des mots

    Posté par  . Évalué à 9.

    moi j'utilise ça :

    cat texte.txt | tr " " "\n" | sort | uniq -c | sort -n >> resultat.txt
    c'est pas très précis, mais c'est simple et ça suffit pour mes besoins (savoir si je répète trop certains mots)

    « Le pouvoir des Tripodes dépendait de la résignation des hommes à l'esclavage. » -- John Christopher

    • [^] # Commentaire supprimé

      Posté par  . Évalué à 3.

      Ce commentaire a été supprimé par l’équipe de modération.

      • [^] # Re: occurrence des mots

        Posté par  . Évalué à 3.

        C’est volontaire de vouloir prendre les signes de ponctuation…

        Pour moi 'mice' et 'mice.' ne sont pas deux chaînes identiques. De la deuxième on peut présager que le mot « mice » a des chances de terminer une phrase. À part le remplacement des retour à la ligne par des espaces puis la réduction des espaces consécutives à une seule espace je travaille en mode suite de caractères.

  • # un pipe de six commandes shell et s'est plié

    Posté par  . Évalué à 6. Dernière modification le 15 décembre 2015 à 13:44.

    Dans le livre Classic Schell Scripting de chez O'reilly [»], il y a un exemple d'analyse fréquentielle des mots d'un texte pour montrer la puissance des pipes. En six commandes et c'est plié.

    #! /bin/sh
    # Read a text stream on standard input, and output a list of
    # the n (default: 25) most frequently occurring words and
    # their frequency counts, in order of descending counts, on
    # standard output.
    #
    # Usage:
    #       wf [n]
    
    tr -cs A-Za-z\' '\n' |         # Replace nonletters with newlines
      tr A-Z a-z |                 # Map uppercase to lowercase
        sort |                     # Sort the words in ascending order
          uniq -c |                # Eliminate duplicates, showing their counts
            sort -k1,1nr -k2 |     # Sort by descending count, and then by ascending word
              sed ${1:-25}q        # Print only the first n (default: 25) lines; see Chapter 3
    • [^] # Commentaire supprimé

      Posté par  . Évalué à 7. Dernière modification le 15 décembre 2015 à 21:00.

      Ce commentaire a été supprimé par l’équipe de modération.

      • [^] # Re: un pipe de six commandes shell et s'est plié

        Posté par  . Évalué à 1. Dernière modification le 16 décembre 2015 à 09:13.

        1. supporter plus que l'ascii
        2. gérer les mots composés
        3. avoir une véritable fréquence \frac{c_m}{n} avec un mot m rencontré c_m fois et un texte de n mots

        Quelques suggestions rapides à tes remarques :
        1. en convertissant les encodages avec la commande iconv
        2. en accédant aux dictionnaires (comme /usr/share/dict/words ou un dictionnaire Hunspell) et en s'assurant que le tiret est bien absent de la variable IFS
        3. en comptant le total des mots du fichier avec wc -l sur la liste des mots avant le dé-doublonnage par uniq -c
        4. …

        … Je ne vois rien de rédhibitoire à résoudre ce problème en Shell.

        Là, on est juste trop loin de son domaine de prédilection.

        Le traitement de données textuelles est justement le domaine de prédilection du Shell.

  • # Compliqué

    Posté par  . Évalué à 5.

    Je ne suis pas expert en python, mais tu semble faire des choses assez compliquées pour rien. Notamment quand tu fais ça :

    s = string.replace ("\n"," ")
    string = s.replace("  "," ")

    Alors que tu ajoute des éléments à string 1 par 1. Tu peux faire :

    if buf[i] != "\n" :
       string += buf[j]
    elif string[-1] != " " :
       string += " "

    Pour ce qui est de tes accès à la base, tu peux :

    • lire toute ta base d'un coup (avec un "where string in (…)")
    • faire tes incréments en mémoire
    • faire des "insert or replace" (avec un executemany)

    Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

  • # Tout faire en base

    Posté par  (site web personnel) . Évalué à 3.

    Bonjour,

    C'est plutôt non intuitif, mais tu pourrais tout faire en SQL pur… Surtout si tu cherches de la performance.
    Tu charges chaque mot dans une table, avec un traitement de type sql_loader et ensuite tu manipules tout en base de données avec des GROUP BY et des DISTINCT et des COUNT pour avoir respectivement les mots, une seule ligne dans une table même si ton mot est répété, et le nombre d'occurences… Et tu as toutes les primitives en SQL pour manipuler les chaînes de caractères…

    Deuxième chose contre intuitive : sur Oracle et dans certains cas sur POSTGres, un DROP TABLE et CREATE AS SELECT c'est beaucoup plus rapide que des INSERT et des UPDATES.

    SQL c'est ensembliste, et comme tu te fiches de l'ordre des mots, et bien tu pourrais avoir des perfs proches du C (parce que l'implémentation de l'exécuteur de requête et de parcours de bases de données, c'est du bas niveau).

    J'ai fait ça des années, et d'ailleurs avec quelques astuces mêmes pour des algorithmes plutôt procéduraux, les résultats sont bluffants.

    • [^] # Re: Tout faire en base

      Posté par  . Évalué à 3.

      C'est probablement vrai ce que tu dit (encore que debugger du sql pur ou du python, c'est pas vraiement la meme chose), mais en l'occurence il fait du sqlite, donc je doute qu'il ait des gains comparable a ce qu'il aurait sur un vrai sgbd des familles.

      Linuxfr, le portail francais du logiciel libre et du neo nazisme.

      • [^] # Re: Tout faire en base

        Posté par  . Évalué à 3.

        SQLite est un SGBD. Je vois pas sur quoi tu te base pour dire qu'il est moins performant qu'un autre….

        • [^] # Re: Tout faire en base

          Posté par  . Évalué à 2.

          Ce qu’il dit, je pense, c’est que les gains apportés par le fait de faire le plus possible de chose en SQL (et je dois dire qu’on peut vraiment faire un tas de trucs insoupçonnés dans les différentes variantes SQL…), par le moteur SQL, ça prends plus de sens pour un SGBD client/serveur… ça évite des « aller-retours »…

  • # Version 0.3

    Posté par  . Évalué à 2. Dernière modification le 21 décembre 2015 à 23:36.

    Merci encore pour toutes vos suggestions. Il faut souligner qu’elles étaient toutes plutôt pertinentes… J’ai pu appliquer beaucoup de vos conseils même si j’ai dû abandonner certains (je pense notamment au islice du module itertools, qui est exactement ce que je cherchais à faire…) parce que je suis parti sur d’autres types de variables et une nouvelle approche du problème.

    D’une point de vue fonctionnel je désire ne plus saucissoner l’input à l’arrache mais essayer de découper sur les phrases en me basant sur les règles typographiques (une phrase finit par un point, '«' ouvre un dialogue (ou pas…), etc…). Ça fait nettement moins d’enregistrements en base !

    D’un point de vue technique j’ai utilisé une classe afin que le code principal soit plus clair.

    Donc voici le code, j’ai viré les commentaires inutiles et il est de toute façon plus concis.

    #!/usr/bin/env python3
    
    """Reader.py: Read text for sentences frequencies."""
    __author__ = "M4rotte"
    __copyright__ = "Copyright 2015, Institut Marotte for Mouling"
    __license__ = "GPL"
    __version__ = "0.3"
    
    import sys
    import re
    import sqlite3
    from collections import Counter
    
    class Reader:
      def __init__(self):
        self.sb          = ''
        self.characters  = Counter()
        self.sentences   = Counter()
        self.start      = re.compile(r'^(\n|)[\-–—«ÉÀÇÔÎÖÏA-Za-z]$')
        self.stop       = re.compile(r'(.|\n)+[.!?…:]( |)$')
        self.nb_insert  = 0
        self.nb_update  = 0
        self.discard    = re.compile(r'^(([0-9]+)([\\]+)|^//).')
    
    
      def read(self,f):
        """Read input and populate `sentences`"""
        while True:
          c = f.read(1)
          if c == '\xa0':
            c = ' '
          self.sb += c
          self.sb = self.sb.replace('  ',' ')
          if not c:
            break
          if self.start.match(self.sb):
            continue
          if self.stop.match(self.sb):
            self.sentences[self.sb] += 1
            self.sb = ''
            continue
          if self.sb == ' ' or self.sb == '\n' or self.sb == '«' or self.sb == '»':
            self.sb = ''  
    
      def store(self,dbfile):
        """ Update the database from `sentences` """
        connection = sqlite3.connect(dbfile)
        cursor = connection.cursor()
        cursor.execute('create table if not exists sentences (sentence text primary key unique, freq int)')
        ## For futur use :) # cursor.execute('create virtual table if not exists fts_strings using fts4(string text, freq int, tokenize=unicode61 "remove_diacritics=0" "tokenchars= ")')
    
    
        # Store `sentences` into database
        for s in self.sentences.most_common():
          if not self.discard.match(s[0]):
            try:
              cursor.execute("insert into sentences values (?, ?)",(s[0], s[1]))
              self.nb_insert += 1
            except sqlite3.IntegrityError:
              self.nb_update += 1
              cursor.execute("update sentences set freq=freq+? where sentence = ?",(s[1],s[0]))
        sys.stderr.write('Database update: '+str(self.nb_insert)+' inserts '+str(self.nb_update)+' updates. \n')
        connection.commit()
        connection.commit()
    
    try:
      reader = Reader()
      reader.read(sys.stdin)
      reader.store('reader.sqlite')
    
    
    except KeyboardInterrupt as error:
      print ('\n'+repr(error),file=sys.stderr)
      sys.stderr.flush()
      sys.stdout.flush()
      exit(1)

    D’un point de vu performance je trouve que ça tourne pas mal (à partir d’une base inexistante) :

    $ time for f in *.txt; do ./Reader.py < "$f"; done;
    Database update: 7537 inserts 0 updates. 
    Database update: 7569 inserts 76 updates. 
    Database update: 7402 inserts 35 updates. 
    Database update: 2496 inserts 2 updates. 
    Database update: 11022 inserts 72 updates. 
    Database update: 11052 inserts 69 updates. 
    Database update: 5825 inserts 44 updates. 
    Database update: 7148 inserts 64 updates. 
    Database update: 10743 inserts 108 updates.
    

    Les fichiers sont des classiques de la littérature issus de sites comme http://www.ebooksgratuits.com/ (principalement), convertis from EPUB par un script choppé sur github. Les fichiers ayant été ensuite légèrement toilettés à la main est au grep avant traitement… Ça représente 6,6M de texte UTF-8.

    Voici le résultat de quelques requêtes effectuées sur la base suite au traitement de ces fichiers :

    sqlite> select * from sentences order by freq desc limit 10;
    M.|213
    Ah !|184
    « Ah !|181
    - Oh !|112
    - Ah !|98
    Oh !|91
    « Oh !|85
    – Ah !|67
    Eh bien !|54
    - Tiens !|38
    
    sqlite> select * from sentences where sentence like '%?' order by freq desc limit 10;
    Hein ?|33
    - Hein ?|27
    – Pourquoi ?|18
    « Eh bien ?|13
    pourquoi ?|12
    quoi ?|11
    « Pourquoi ?|11
    – Pourquoi cela ?|10
    – Comment cela ?|9
    « Comment ?|9
    
    sqlite> select * from sentences where sentence like '%moule%' order by freq desc limit 10;
    « Il n’y a plus de ténors, disait-il, le moule en est brisé.|1
    Mme Arnoux fit exhiber les moules pour les ouvrages plus difficiles.|1
    

    J’ai déjà noté que je devrais appliquer mon expression régulière "discard" dans la fonction read() plutôt qu’au niveau store()…

    • [^] # Re: Version 0.3

      Posté par  . Évalué à 2.

      J’ai zappé le time :/

      real    1m30.632s
      user    1m25.840s
      sys 0m0.640s
      
    • [^] # Re: Version 0.3

      Posté par  . Évalué à 2. Dernière modification le 22 décembre 2015 à 00:06.

      if self.sb == ' ' or self.sb == '\n' or self.sb == '«' or self.sb == '»':

      Je devrais je pense créer un re.reset pour celle là…

      • [^] # Re: Version 0.3

        Posté par  . Évalué à 5. Dernière modification le 22 décembre 2015 à 11:19.

        ou tester l'appartenance à une liste de caractères

        if self.sb in [' ', '\n', '«', '»']:
          self.sb = ' '

        si un jour tu veux rajouter d'autres caractères dans le test, ce sera plus rapide et plus lisible. ;-)

        Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

    • [^] # Re: Version 0.3

      Posté par  . Évalué à 2.

      En modifiant un peu le code voilà ce que ça donne sur un extrait de bouchot standard :

      self.start      = re.compile(r'^(\n|)[\-–—<«ÉÀÇÔÎÖÏA-Za-z]$')
      if self.sb == ' ' or self.sb == '\n' or self.sb == '«' or self.sb == '»' or self.sb == '<' or self.sb == '>':
        self.sb = ''
      
      sqlite> select * from sentences order by freq desc limit 40;
      ..|1704
      <a href="http:|818
      <a href="https:|361
      youtube.|359
      com/watch?|356
      imgur.|259
      [:|211
      pr0gramm.|193
      !!|158
      wikipedia.|154
      lemonde.|115
      twimg.|114
      moul.|92
      lefigaro.|66
      20minutes.|62
      )
      22:|55
      o
      20:|52
      programme.|48
      /
      20:|48
      /
      21:|46
      )
      20:|46
      yahoo.|45
      o
      21:|43
      o
      17:|42
      news.|42
      co.|42
      google.|42
      ??|41
      nouvelobs.|41
      -C'est quoi ça ?|39
      )
      16:|35
      liberation.|35
      o
      16:|35
      /
      22:|34
      /
      17:|33
      o
      22:|33
      pan !|32
      php?|32
      gouv.|32
      02 [:|31
      

      Ça donne des informations intéressantes sur les domaines des liens POSTé :)

    • [^] # Re: Version 0.3

      Posté par  . Évalué à 5.

      Tu fais toujours pour chaque caractère :

      self.sb = self.sb.replace('  ',' ')

      Tu pourrais le faire une fois pour toute à la fin.

      Pour le stockage je persiste à dire que tu peux faire ça bien plus efficacement en effectuant toute les lectures en une fois puis en faisant tous les ajouts/mise à jour ensuite (faire 1+N requêtes plutôt que N+N/2).

      Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

    • [^] # Re: Version 0.3

      Posté par  . Évalué à 3. Dernière modification le 24 décembre 2015 à 12:23.

      Je comprend pas les résultats que tu obtiens, si j'ai bien compris tu as pris plusieurs classiques de la littérature française et les mots ou groupes de mots les plus courants sont:

      M.|213
      Ah !|184
      « Ah !|181
      - Oh !|112
      - Ah !|98

      ça ne me paraît pas possible! ou il y a un bug, ou je n'ai pas compris ce que tu faisais. Je m'attendais à ce que les mots les plus courants soient "le", "la", "et" etc. Que "M." arrive en tête, suivi de "Ah" ça me semble être n'importe quoi.

      • [^] # Re: Version 0.3

        Posté par  (site web personnel) . Évalué à 5.

        Ça ne semble donner que ce qui se termine par une ponctuation (point, point-virgule, point d'exclamation, etc.).

        • [^] # Re: Version 0.3

          Posté par  . Évalué à 2.

          Oui, le but c’est d’avoir la fréquence des phrases.

          J’ai finalement modifié le script pour qu’il ne considère que c’est la fin d’un phrase seulement si ça termine par un point (.?!…:) suivi d’une espace ou d’un retour à la ligne. Ceci pour éviter de découper une URL (ou encore un sigle écrit avec des points, tel que U.R.S.S).

          Ce n’est pas vraiment adapté à la tribune car il n’y a pas forcément un point à la fin de chaque post… Je pourrais ajouter un point à la fin de chaque post avant de les passer en entrée du script si nécessaire mais je m’oriente plus vers l’analyse de textes respectant la typographie française (littérature, article de jounrnal…)

          • [^] # Re: Version 0.3

            Posté par  . Évalué à 2.

            Oui, le but c’est d’avoir la fréquence des phrases.

            J’ai finalement modifié le script pour qu’il ne considère que c’est la fin d’un phrase seulement si ça termine par un point (.?!…:) suivi d’une espace ou d’un retour à la ligne. Ceci pour éviter de découper une URL (ou encore un sigle écrit avec des points, tel que U.R.S.S).

            Ce n’est pas vraiment adapté à la tribune car il n’y a pas forcément un point à la fin de chaque post… Je pourrais ajouter un point à la fin de chaque post avant de les passer en entrée du script si nécessaire mais je m’oriente plus vers l’analyse de textes respectant la typographie française (littérature, article de jounrnal…)

            Qu'est-ce que tu attends de cette information? En général les phrases ne sont jamais répétées. Sauf si elles contiennent très peu de mots. Ou si ce sont des citations.

            Si tu veux identifier des expressions qui se répètent, là il te faudrait plutôt utiliser une fonction qui calcul le degré de cooccurence entre deux ou plusieurs mots dans des phrases.

  • # Ça avance

    Posté par  . Évalué à 2.

    À la base j’aurais pas dû poster ça dans un journal mais dans un forum. Le fait que ce soit un journal m’incite à continuer à poster mon code…

    Voici donc la dernière version de Reader.py, script dont le but est de prendre n’importe quel fichier texte en entrée, le découper en « phrases » et stocker la fréquence d’apparition de ces « phrases » dans une base SQLite.

    #!/usr/bin/env python3
    
    """Reader.py: Read text for sentences frequencies."""
    __author__ = "M4rotte"
    __copyright__ = "Copyright 2015, Institut Marotte for Mouling"
    __license__ = "GPL"
    __version__ = "0.4"
    
    import sys
    import re
    import sqlite3
    from collections import Counter
    
    class Reader:
      def __init__(self):
        self.sb             = ''
        self.characters     = Counter()
        self.sentences      = Counter()
        self.signature      = ''
        self.sig96          = ''
        self.start          = re.compile(r'^(\n|)[\-–—«ÉÀÇÔÎÖÏA-Za-z]$')       # If string buffer (self.sb) match, just read next character.
        self.stop           = re.compile(r'(.|\n)+[.!?…:]( |\n)$')             # If self.sb match, consider self.sb valid sentence, store it in self.sentences and reset self.sb.
        self.discard        = re.compile(r'^(([0-9]+)([\\:]+)|##)(.*)(\n|)')   # If self.sb match, just drop it.
        self.correct_tiret  = re.compile(r'^( |)-')                            # If match (it's obviously part of a dialog), replace the tiret with the designed Unicode character.
        self.cancel_stop    = re.compile(r'(.*|)( |\n)(M|Me)(\.)( |\n|)$');    # If match (means it matched self.stop first…), just don't "stop" and read next character.
        self.nb_insert      = 0   # Number of inserts off the last read()
        self.nb_update      = 0   # Number of updates off the last read() 
        self.nb_record      = 0   # Number of records in the updated database
    
      def read(self,f):
        """Read input and populate `sentences`"""
        while True:
          c = f.read(1)
          if not c:
            break
          if c == '\xa0':
            c = ' '
          self.characters[c] += 1  
          self.sb += c
          if self.sb in [' ','\n','»']:
            self.sb = ''  
            continue
          if self.discard.match(self.sb):
            self.sb = ''
            continue
          if self.start.match(self.sb):
            continue
          if self.stop.match(self.sb) and not self.cancel_stop.match(self.sb):
            if self.correct_tiret.match(self.sb):
              self.sb = self.sb.replace('-','–') 
            self.sentences[self.sb.strip()] += 1
            self.sb = ''
            continue
        for c in self.characters.most_common():
          self.signature += c[0]
        self.sig96 = self.signature[:96].replace('\n',' ')  
    
      def store(self,dbfile='reader.sqlite'):
        """ Update the database from `sentences` """
        connection = sqlite3.connect(dbfile)
        cursor = connection.cursor()
        cursor.execute('create table if not exists sentences (sentence text primary key unique, freq int)')
        ## For futur use :) # cursor.execute('create virtual table if not exists fts_strings using fts4(string text, freq int, tokenize=unicode61 "remove_diacritics=0" "tokenchars= ")')
    
    
        # Store `sentences` into database
        for s in self.sentences.most_common():
          try:
            cursor.execute("insert into sentences values (?, ?)",(s[0], s[1]))
            self.nb_insert += 1
          except sqlite3.IntegrityError:
            self.nb_update += 1
            cursor.execute("update sentences set freq=freq+? where sentence = ?",(s[1],s[0]))
        self.nb_record = cursor.execute('select count(*) from sentences;')
        sys.stderr.write('Database update: '+str(self.nb_insert)+' inserts '+str(self.nb_update)+' updates '+str(self.nb_record.fetchone()[0])+' records. SIG96 : '+str(self.sig96)+'\n')
        connection.commit()
    
    try:
      reader = Reader()
      reader.read(sys.stdin)
      reader.store()
    
    
    except KeyboardInterrupt as error:
      print ('\n'+repr(error),file=sys.stderr)
      sys.stderr.flush()
      sys.stdout.flush()
      exit(1)

    La sortie en partant d’une base inexistante et en la renseignant à partir de classiques de la littérature française, posts du bouchot et contenu issu de Wikipédia (histoire d’avoir des entrées assez hétérogènes) :

    $ for f in *.txt; do ./Reader.py < "$f"; done; sqlite3 ../linuxfr.org.sqlite 'select message from posts;' | ./Reader.py ; sqlite3 ../Wikipedia.sqlite 'select fact from facts;' | ./Reader.py 
    Database update: 7498 inserts 0 updates 7498 records. SIG128 :  eaistnrulodcmp v,é’fqh.gbàx-jyè;–ALê?«»zkC!SEJMKDI:WPçO…îôâQVNT#œû_ùwBUïRG1FYÀ2()É307H5486][9ÔX
    Database update: 7533 inserts 74 updates 15031 records. SIG128 :  eaistnrulodcpm v,é’qf.hbgàjx-yè–;AêL?«»kCzE!SM:JIPDKWîçVôNOTâQ_…û#ùœBw0RïU1ÀF2G()3YHÉ5][847°69ü
    Database update: 7367 inserts 33 updates 22398 records. SIG128 :  easitnrulodcmp ,év’f.qgbhàèxj-êyMIL;EPCS«»Az!RâçJ:DFVûîTô…?ùOGQ–UœNïÀB1É8H{Ç45/kë#0w397XÔ2Z)6—]
    Database update: 2496 inserts 2 updates 24894 records. SIG128 :  esatinrulodcpmé, v'qfgbh.àxèCy;jê-LAG\:IRzôSBçâTHPîDûVMEù12Q3O«4»N5ï0U687œ?9FÉJ)(_!…ëÀW
    Database update: 10914 inserts 71 updates 35808 records. SIG96 :  easitnrulodc mp,év’.bfhgqxà»!èj«;-yMAêIECLFDz?P:çSRâOVJîTôQUBùN—…ûHG()ïk1#8X3Ç2w4590Yë6óW7Z\áñú
    Database update: 1359 inserts 15 updates 37167 records. SIG96 :  esitanrulodpmc '.é,v-qbfghjàxJèIEC!:ML?TyAêçPzQOS»«BVHûRîâùXôÇDNU2ï1F3()69À05Éë8Z°;7
    Database update: 10707 inserts 62 updates 47874 records. SIG96 :  eaistnrulodmpc év,’q.fhbgàjMx-èJêyL;CIz–!ERSVAP?Dâç:ûôQî…FBTNOœUùÀ#XHGÉï1*()0»«8Kw637k°924YW5ÊZ
    Database update: 5734 inserts 79 updates 53608 records. SIG96 :  eastinrulodcmp év'.,fqgbh-àjTxèyIêL!çJEC«»A?kMHPSzDOB…Nô*Qûâ:RUîV{ùXœ_ÀÇwGF;1ÉW/()2ï][YZ07438}Î
    Database update: 7114 inserts 64 updates 60722 records. SIG96 :  easintrulodm,cp vé’qf.hgbàjxè—-yêM:L!zEACJ?IP;çD»…«FôSVùTâOûNBQîR–œUHGÀ1kÉw2X907K5ü84ï#ëäÇY3WÈÊ
    Database update: 10690 inserts 123 updates 71412 records. SIG96 :  eastinrulodc,mp vé'.fghbq-àjxè!yMEêLC;N…zIPçADSâF?VBT:OôJûùîGURHQZœ_À»«Çk*{Éw1Xïë4Ô05W2Ê73Y
    Database update: 24771 inserts 29 updates 96183 records. SIG96 :  eastinruol:cpdm1 20fh/vé'5b3.qg4-,j76<>98"wyx?à[]=ç͌!kèêzCA’_;&SET)LPMOINJD(FBRU\VHG%ô…+W̚҉Qû█«
    Database update: 67825 inserts 14 updates 164008 records. SIG96 :  eainrstoludcmép,. ghvbf1q'()y9L02ACxè-MSjPàBk†8DRJ3IGçF5746T:EzNê°HV’OKUwÉ«W»Xôûî;ïZâYQœ%áù/í"ë
    

    On peut voir que l’intégration du contenu Wikipédia amène à certains biais (et c’est pas Me Soleil qui s’en plaindra…) mais que « Ah ! » et « Oh ! » restent des valeurs sûres :

    $ sqlite3 reader.sqlite "select * from sentences  order by freq desc limit 30;"
    Signe du zodiaque :|358
    M.|224
    Ah !|194
    « Ah !|181
    – Ah !|180
    Mgr Paul Guérin, Vie des saints ; t.|162
    – Oh !|148
    Pas de journée internationale répertoriée pour cette date.|134
    Oh !|100
    « Oh !|87
    Les noms de plusieurs voies, places, sites ou édifices, de pays ou régions francophones, contiennent cette date sous diverses graphies :|66
    – Eh bien !|59
    France :|59
    Eh bien !|56
    États-Unis :|52
    Décembre :|49
    Bahaïsme :|47
    Février :|46
    Juin :|43
    – Tiens !|40
    Octobre :|40
    Hein ?|39
    –C'est quoi ça ?|39
    Mai :|39
    Fêtes religieuses romaines :|39
    Mars :|38
    « Non !|37
    dit Frédéric.|36
    Japon :|36
    — Ah !|35
    

    Vous pouvez reprendre une activité normale.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.