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.

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.5) autocommenté en bilingue

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

"""
Saving and loading binary objects, strings, lists, tab-CSV and dictionaries
Sauvegarder et charger des objets binaires, chaînes, listes, tab-CSV et dictionnaires
Jean-Christophe Beumier - 2022.10.29 - Ver0.5 - some fixes: loadD(), saveD(), what()
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 GPL v2 conditions.
Ce programme est libre, utilisez-le sous votre propre responsabilité.
Vous pouvez le modifier et le distribuer, aux conditions GPL v2.
"""

### Line separator according to Operating System

### Killing spaces

def strip(chaine) :
  """
    v2f.strip(my_string)
  Remove spaces before and after a string ; who ever knows?
  Enlève les espaces entourant une chaîne ; sait-on jamais?
  """
  while len(chaine) and (chaine[0] ==" ") :
    chaine =chaine[1:]
  while len(chaine) and (chaine[-1] ==" ") :
    chaine =chaine[:-1]
  return chaine

### Section Blobs

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 a 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 (ISO-8859-1 / latin-1 étendu)
  """
  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
  Charge un fichier chaîne en liste
  """
  liste =loadS(fichier).split("\n")
  for i in range(len(liste)-1, -1, -1) : # permitting pop()
    item =strip(liste[i])
    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 =strip(liste[i])
    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 strip(chaine) =="" : 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 déterminent le séparateur de cellules (tab, virgule, poit-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.08.18

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

Quoi d'autre ?