Sauvegarder des variables python dans des fichiers et les récupérer

MALGRÉ la simplification de manipulation des fichiers en python, ce module désire apporter des facilités à l'écriture de fichiers un peu plus complexes, comme des fichiers CSV (Comma Separated Values), ou plutôt TSV, utilisant des tabulations comme séparateur.

2022.08.12

1. Argument  •  2. Emploi du module  •  3. Script auto-commenté  •  4. Versions

Argument

Pour sauvegarder ou charger des données, python n'est guère plus pratique que les bons vieux BASIC, avec un bloc ressemblant à :

fd =open("myfile, "r")                       OPEN "I", 13, "myfile"
texte =fd.read()                             INPUT #13, Var$
fd.close()                                   CLOSE 13

Le code a par la suite été quelque peu simplifié en enfermant la lecture ou l'écriture dans une structure semble-t-il inventée pour l'occasion :

with open("nomdefichier", "r") as fd :
  texte =fd.read()

…la fermeture du fichier ayant lieu lors de la sortie de la structure with / as.

La seule ligne

open("essai.txt", "w").write("Une chaîne")

fonctionne, mais il n'est pas dit qu'elle soit légale : '__warningregistry__': {'version': 0}} apparaît dans le dictionnaire des variables vars().

Le module v2f réduit le code à une seule fonction sans paramètre (pas de "r" ou "w") et sans descripteur de fichier qu'il faut ensuite fermer :

v2f.saveS("mon_fichier", "Eggs and Spam!") sauve un simple texte dans un fichier
texte =v2f.loadS("mon_fichier") lit un fichier et transmet son contenu dans une chaine

De la même manière, une liste est très simplement sauvegardée dans un fichier :

v2f.saveL("petitdej", ["Eggs", 2, "Spam", 3, "!"]) sauve une liste dans un fichier
liste =v2f.loadL("petitdej") lit un fichier et transmet son contenu dans une chaine, les éléments séparés par un retour à la ligne :

'Eggs'
2
'Spam'
3
'!'

Une liste de liste est sauvegardée dans un fichier TSV (tabulations entre les cellules) :

v2f.saveT("mon.csv", [["Eggs", 2], ["Spam", 3]]) sauve une liste dans un fichier
liste =v2f.loadL("petitdej") lit un fichier et transmet son contenu dans une liste ('→' symbolise la tabulation)

'Eggs' → 2
'Spam' → 3

Les dictionnaires sont sauvegardés comme une liste de lignes de deux cellules :

v2f.saveD("moby.dic", { 2:"", "2": {"a":1}, "a":[1,2,3]}) sauve un dictionnaire
dico =v2f.loadD("moby.dic") charge un fichier et place les données dans une variable dictionnaire

2   → ''
'2' → { 'a': 1 }
'a' → [ 1, 2, 3 ]

Les fichiers binaires sont également traités (par les objets bytes) :

v2f.saveB("my.bin", bytes([43, 10, 245, 55])) sauve un objet bytes
octets =v2f.loadB("my.bin") lit un binaire et l'insère dans un objet bytes

Ce module fait double ou triple emploi avec d'autres modules de sauvegarde de données (voir modules) mais certains sont abandonnés, n'ont jamais été très pratiques et les fichiers produits ne sont pas forcément lisibles par un être humain.

Voir les versions pour les corrections, améliorations et nouveautés.

Emploi du module

Dans un script python, il est possible d'importer ce module de deux façons :

Pour que le module v2f.py (clic droit pour le télécharger) puisse être importé par le script, il faut par exemple qu'il soit situé dans le même répertoire, ce qui peut être gênant si vos scripts ne figurent pas tous dans le même répertoire.

Si vous êtes administrateur d'un système GNU+Linux, vous pouvez placer le module, avec les droits du super-utilisateur, parmi les librairies de python :

#En mode super-utilisateur: su - [Enter] cp v2f.py /usr/lib/python3.9

Si vous êtes simple utilisateur d'un système GNU+Linux, vous devriez pouvoir le copier dans le répertoire /home/toto/.local/lib/python3.9 si votre compte est toto et que la version de python est la 3.9.

Le code est commenté avec des DocStrings. Pour faire apparaître les fonctions et leur commentaire dans une console :

>>> import v2f
>>> help(v2f)

Le script (ver0.6) commenté en bilingue

À copier-coller dans un fichier texte, ou à télécharger (clic droit) :

"""
Saving and loading in Python3 binary objects, strings, lists, tab-CSV and dictionaries
Sauvegarder et charger en Python3 des objets binaires, chaînes, listes, tab-CSV et dictionnaires
Jean-Christophe Beumier - 2022.12.01 - Ver0.6 - new : coding()
http://jchr.be/python/v2f.htm for informations (french)

This program is free software. It comes without any warranty.
You can modify it, and redistribute it, under GPLv3 conditions.
https://www.gnu.org/licenses/gpl-3.0.en.html
Ce programme est libre, utilisez-le sous votre propre responsabilité.
Vous pouvez le modifier et le distribuer, aux conditions GPLv3.
"""

def coding() :
  """
  These codes are used to force a specific text-coding
  Ces codes sont utilisés pour forcer l'encodage d'un texte
  """
  print("""
  utf-8 (default) : universal / universel (Unicode set)
  iso-8859-1 – latin1 – latin-1 : Western Europa / Europe occidentale
  iso-8859-15 – latin9 : slightly modified latin-1 légèrement modifié
  cp1252 : Windows enhanced latin-1 (utilis° des car. 160-191 chars are used)
  iso-8859-2 – latin-2 : Eastern Europa / Europe de l'est
  iso-8859-3 – latin-3 : Southern Europa / Europe du sud
  iso-8859-4 – latin-4 : Northern Europa / Europe du nord
  iso-8859-5 : Cyrillic / cyrillique
  iso-8859-6 : Arabic / arabe
  iso-8859-7 : Monotonic Greek / grec monotonique
  iso-8859-8 : Hebrew / hébreu
  iso-8859-9 – latin-5 : Turkish, Kurdish / turc, kurde
  iso-8859-10 – latin-6 : Northern Europa / Europe du nord
  iso-8859-11 : thai
  iso-8859-13 – latin-7 : Baltic / baltique
  iso-8859-14 – latin-8 : Celtic / celtique
  iso-8859-16 – latin-10 : Southern-Eastern Europa / Europe du sud-est
  """)

### Blobs section (binary objects)

def saveB(fichier, blob) :
  """
     saveB(my_file, my_blob)
  Save a Blob (binary object, bytes from 0 to 255) in a file
  Sauvegarde un Blob (valeurs de 0 à 255) dans un fichier
  """
  with open(fichier, "wb") as fd :
    fd.write(blob)

def loadB(fichier) :
  """
     my_blob =loadB(filename)
  Load a Blob (binary file, bytes values from 0 to 255) from a file
  Charge un Blob (fichier binaire, octets de valeurs de 0 à 255) d'un fichier
  """
  with open(fichier, "rb") as fd :
    return fd.read()

### Section Strings

def saveS(fichier, chaine, mode="utf-8") :
  """
     saveS(my_file, my_string)
  Saves a String into a human readable file (utf-8 or your prefered encoding)
  Sauvegarde une chaîne dans un fichier lisible par un être humain, UTF-8 par
   défaut ou selon l'encodage désiré
  """
  with open(fichier, "w", encoding=mode) as fd :
    return fd.write(chaine)

def loadS(fichier, mode="") :
  """
     my_string =loadS(filename)
  Loads a String from a human readable file. You can mention an encoding,
   otherwise it's utf-8 or cp1252 (an improved iso-8859-1 / latin-1 )

  Charge une chaîne d'un fichier texte. Il est possible de préciser un encodage,
   sinon c'est UTF-8 qui est tenté, et puis CP1252 (extension du iso-8859-1 / latin-1)
  """
  if mode :
    with open(fichier, "r", encoding=mode) as fd :
      return fd.read()
  else :
    try :
      with open(fichier, "r", encoding="utf-8") as fd :
        return fd.read()
    except :
      try :
        with open(fichier, "r", encoding="cp1252") as fd :
          return fd.read()
      except :
        return "Pas d'encodage rencontré. Désolé."


def addS(fichier, chaine) :
  """
     addS(filename, my_string)
  Adds a String to a file ; creation if inexistant
  Ajoute une chaîne à un fichier, le crée au besoin
  """
  with open(fichier, "a") as fd :
    fd.write(chaine)

### Section Listes

def saveL(fichier, liste) :
  """
     saveL(my_file, my_list)
  Saves a chain-list in a string form, with line separators ; strings are 'quoted'
  Sauve une liste sous forme de chaîne avec sauts de ligne ; chaînes avec guillemets
  """
  for i in range(len(liste)) :
    liste[i] =repr(liste[i])
  saveS(fichier, "\n".join(liste))

def loadL(fichier) :
  """
     my_list =loadL(filename)
  Loads a String file into a list with end-of-line separator
  Charge un fichier chaîne en liste (séparateur: fin de ligne)
  """
  liste =loadS(fichier).split("\n")
  for i in range(len(liste)-1, -1, -1) : # permitting pop()
    item =liste[i].strip()
    if item =="" :
      liste.pop(i) ; continue
    liste[i] =eval(item)
  return liste

### Section Tables

def saveT(fichier, liste, mode="") :
  """
     saveT(my_file, my_list)
  Saves a CSV table, TSV (Tab Separated Values)
  Sauvegarde une table CSV, TSV (valeurs séparées par des tabulations)
     saveT(my_file, my_list, "-")
  Doesn't quote the strings
  N'entoure pas les chaînes de guillemets
     saveT(my_file, my_list, ",")
  Transforms the number dots to comma (french decimal notation)
  Transforme les point décimaux en virgule (export pour LibreOffice Calc)
  """
  for i in range(len(liste)) :
    rangee =liste[i]
    for j in range(len(rangee)) :
      if "-" not in mode or type(rangee[j]) !=type("") :
        rangee[j] =repr(rangee[j]) # string transformation
        if "," in mode :
          rangee[j] =rangee[j].replace(".", ",")
    liste[i] ="\t".join(rangee)
  saveS(fichier, "\n".join(liste))

def loadT(fichier) :
  """
     my_table =loadT(filename)
  Loads a TSV (Tab-Separated-Value) file into a list (rows) of lists (cells)
  Charge un fichier TSV Valeurs séparées par des Tab (liste de listes)
  """
  chaine =loadS(fichier)
  liste =chaine.split("\n")
  for i in range(len(liste)-1, -1, -1) :
    ligne =liste[i].strip()
    if ligne =="" :
      liste.pop(i) ; continue
    cells =liste[i].split("\t")
    for j in range(len(cells)) :
      cells[j] =eval(cells[j])
    liste[i] =cells
  return liste

### Section Dictionnaires

def saveD(fichier, dico) :
  """
     saveL(my_file, my_dic)
  Saves a dictionary into lines 'key + tab + value'
  Sauve un dictionnaire sous forme de lignes "clé + tab + valeur"
  """
  liste =[]
  for i in dico :
    liste +=["%s\t%s" %(repr(i), repr(dico[i]))]
  chaine ="\n".join(liste)
  saveS(fichier, chaine)


def loadD(fichier) :
  """
     my_dic =loadD(filename)
  Reads a lines-file 'keys + tab + values' to fill a dictionary
  Charge un fichier de lignes "clé + tab + valeur" pour constituer un dictionnaire
  """
  chaine =loadS(fichier)
  liste =chaine.split("\n")
  dico ={}
  for i in range(len(liste)) :
    morceaux =liste[i].split("\t")
    if morceaux ==[''] : continue
    if len(morceaux) ==1 :
      print("!!! %s : Clé sans valeur" %morceaux[0])
      continue
    if len(morceaux) > 2 :
      print("!!! Trop de valeurs sur la ligne :", morceaux)
      continue
    cle, val =morceaux
    dico[eval(cle)] =eval(val) #
  return dico

### Imports with différents cell-separators

def guess(chaine) :
  """
     v2f.guess(my_string)
  Tries to guess the character used to separate the fields of a CSV
  Tente de déterminer le caractère utilisé pour séparer les champs d'un CSV
  """
  cs =[]
  cs +=[[chaine.count("\t"), "\t"]]
  cs +=[[chaine.count(";"), ";"]]
  cs +=[[chaine.count(","), ","]]
  cs =sorted(cs, reverse =True)
  return cs[0][1]

def impT(fichier) :
  """
     v2f.impT(filename)
  Loads a CSV (Comma/Semi-comma/Tabulation-Separated File) into a list, guessing cellseps
  Charge un fichier CSV dans une table, en devinant les séparateurs de cellules
  """
  chaine =loadS(fichier)
  cellsep =guess(chaine)
  liste =chaine.split("\n")
  for i in range(len(liste)) :
    cells =liste[i].split(cellsep)
    for j in range(len(cells)) :
      cell =cells[j]
      try :
        cell =eval(cell)
      except :
        pass
      cells[j] =cell
    liste[i] =cells
  return liste

def impD(fichier) :
  """
     v2f.impD(filename)
  Loads a file into a python dictionnary, guessing cellseps
  Charge un fichier dans un dictionnaire, en devinant les séparateurs de cellules
  """
  chaine =loadS(fichier)
  cellsep =guess(chaine)
  liste =chaine.split("\n")
  dico ={}
  for i in range(len(liste)) :
    chaine =liste[i]
    if chaine.strip() =="" : continue
    cells =chaine.split(cellsep)
    lon =len(cells)
    if lon !=2 :
      if lon ==1 : print("Ligne à un seul elément:", chaine)
      if lon > 2 : print("Ligne à plusieurs valeurs:", chaine)
      continue
    cle =cells[0] ; val =cells[1]
    try :
      cle =eval(cle)
    except :
      pass
    try :
      val =eval(val)
    except :
      pass
    if dico.get(cle) :
      print("Clé ~%s~ déjà employée, l'ancienne valeur ~%s~ écrasée par ~%s~" %(cle, dico[cle], val))
    dico[cle] =val
  return dico

def what(fichier) :
  """  what("myfile")
  Attempts to determine the kind of file (binary/text, separators...)
  Tente de déterminer le type de fichier (binaire/texte, séparateurs...)
  """
  octets =bytes(loadB(fichier))
  lsr =octets.count(13) ; lsn =octets.count(10)
  coct =[0] *256
  lon =len(octets) ; lon0 =min(lon, 100000)
  for i in range(lon0) :
    coct[octets[i]] +=1

  if sum(coct[0:9]) > 0 and sum(coct[14:26]) > 0 :
    print("\n%s is a binary file; %s 'null bytes' on %s (%s%%)" %(fichier, coct[0], lon0, round(coct[0] *100 /lon0, 2)))
  else :
    print("\n«%s» is probably a text file" %fichier)
    if lsr ==0 and lsn ==0 :
      print("No line separator")
    else :
      if lsr ==lsn :
        print("%s line separator '\\r\\n' (bytes 13 + 10) Atari or Microsoft" %lsn) ; lsep ="\\r\\n"
      elif lsr ==0 :
        print("%s linesep '\\n' (octet 10) UNIX, Linux or McOSX" %lsn) ; lsep ="\\n"
      else :
        print("%s linesep '\\r' (octet 13) Amiga or Mc Classic" %lsr) ; lsep ="\\r"
      nspc =coct[32]; ntab =coct[9] ; npvi =coct[59] ; nvrg =coct[44]
      lignes =str(octets).split(lsep)
      if nspc ==0 :
        print("No space found")
      else :
        print("%s spaces on %s bytes (%s%%)" %(nspc, lon0, round(nspc *100 /lon0, 2)))
      if ntab ==0 :
        print("No tab")
      else :
        ltab =[]
        for i in lignes :
          ltab += [i.count("\\t")]
        mintab =min(ltab) ; maxtab =max(ltab)
        if mintab ==maxtab :
          print("%s tab(s) ; %s tab(s) a line" %(ntab, mintab))
        else :
          print("%s tab(s) ; de %s à %s tab(s) a line" %(ntab, mintab, maxtab))
      if npvi ==0 :
        print("No semicol")
      else :
        lpvi =[]
        for i in lignes :
          lpvi += [i.count(";")]
        minpvi =min(lpvi) ; maxpvi =max(lpvi)
        if minpvi ==maxpvi :
          print("%s semicol(s) ; %s semicol(s) a line" %(npvi, minpvi))
        else :
          print("%s semicol(s) ; from %s to %s semicol(s) a line" %(npvi, minpvi, maxpvi))
      if nvrg ==0 :
        print("No comma")
      else :
        lvrg =[]
        for i in lignes :
          lvrg += [i.count(",")]
        minvrg =min(lvrg) ; maxvrg =max(lvrg)
        if minvrg ==maxvrg :
          print("%s comma(s) ; %s comma(s) a line" %(nvrg, minvrg))
        else :
          print("%s comma(s) ; from %s to %s comma(s) a line" %(nvrg, minvrg, maxvrg))
  print("\nBytes:", end ="")
  for i in range(16) :
    print(" _%X  " %i, end ="")
  print()
  for i in range(16) :
    print("%2X_ " %i, end ="")
    for j in range(16) :
       print("%5d" %coct[i *16 +j], end ="")
    print()

Versions

v0.1 - 2022.08.12

Écriture des premières fonctions d'un module appelé v2f (variables to files) sous GPL v2.

v0.2 - 2022.08.14

Ajout des fonctions qui tient compte du séparateur de cellules (tab, virgule, point-virgule) :

Élimination des routines pour déterminer les séquences de fin de paragraphe :

v0.3 - 2022.08.16

Afin de faciliter l'export, un mode (facultatif) permet d'éviter que les chaînes soient entourées de guillemets (les nombres entre guillemets sauvegardés avec "-" seront récupérés sous forme de nombre) et le point décimal de python peut être remplacé par une virgule. Ces deux modes sont compatibles.

saveT(filename, listoflists, "-") n'entoure pas les chaînes de guillemets
saveT(filename, listoflists, ",") transforme les point décimaux en virgules
saveT(filename, listoflists, "-,") ou mode=",-" fait les deux, ce qui est intéressant pour un export vers LibreOffice Calc français

v0.4 - 2022.08.18

Ajout d'un mode d'encodage des chaînes (facultatif) :

Expérimental, une fonction de détermination de fichier :

v0.5 - 2022.10.29

Corrections de bugs critiques pour saveD() et loadD() ; amélioration de what() . Le projet m"était complètement sorti de la tête.

v0.6 - 2022.12.01

Passage en licence GPL-v3 ; suppression de la fonction 'utilisateur' strip() pour celle native ; ajout de la fonction coding() qui détaille les encodages iso-8859, cp1252 et utf-8.

Quoi d'autre ?