I. Introduction▲
La gestion de données spécifiques et mobiles, tels des préférences, des profils… est toujours une question qui finit par se poser lorsque l'on commence le développement d'une application.
En effet, la plupart du temps, on recommande l'utilisation d'une base de données. Mais même une base de données de type SQLite3, pourtant native et composée d'un seul fichier, peut s'avérer lourde pour gérer des préférences ne comportant guère plus de 10 lignes dans un fichier texte.
Une autre solution apparaissant rapidement est l'utilisation de fichier texte (extension txt, ini, cfg…). Avantage pour eux, ils sont directement éditables à la main. Cependant, l'inconvénient d'une telle solution est le formatage et la gestion de ce genre de fichiers.
Afin de pallier cela, nous pouvons utiliser des fichiers de type INI ou json. Ces types décrivent précisément un formatage des données au sein d'un fichier texte. De plus, côté Python, les modules configparser et json permet de gérer correctement ce type de fichier.
L'ensemble des codes de cet article sont écrits en Python3.x, par conséquent leur fonctionnement en Python2.X n'est pas garanti. Pour rappel, Python 3.X est déclaré apte pour la production depuis la version 3.4 et il est fortement recommandé de ne plus utiliser que Python3.X, sauf en cas de contrainte forte (bibliothèques non portées, serveur impossible à migrer…)
II. Le module configparser▲
II-A. Le format INI▲
Il s'agit d'un simple fichier texte. Le contenu de ce fichier est constitué de sections, dont le nom est placé entre crochets, et de paramètres intégrés dans les diverses sections sous la forme « paramètre=valeur ». Chaque section est séparée par une ligne vide.
Le caractère de commentaire est « ; ».
Voici un exemple.
2.
3.
4.
5.
6.
7.
8.
;fichier INI de demonstration
[Section_01]
parametre1
=
valeur1
parametre2
=
''valeur2''
[Section_02]
parametre3
=
125
parametre4
=
127
.0
.0
.1
; equivalent a localhost
Les fichiers INI portent généralement l'extension « .ini ». Cependant, cette extension étant historiquement très connotée Windows, les développeurs préfèrent en général l'extension « .cfg ».
Que la valeur soit « 127.0.0.1 » ou « ''127.0.0.1'' », la valeur est toujours stockée sous forme de chaîne de caractères.
II-B. Python et les fichiers INI▲
Par défaut, Python, en branche 3, gère les fichiers INI comme des dictionnaires Python. De fait, la manipulation des sections et paramètres repose sur les mêmes principes.
De plus, quelle que soit l'opération que vous désirez effectuer, il vous faudra passer par un objet conteneur. En effet, il ne vous est pas possible de manipuler directement le fichier.
Le fonctionnement de ce module diffère totalement entre la branche 2 et 3 de Python. Pour rappel, il est désormais recommandé d'utiliser exclusivement la branche 3 de Python.
II-B-1. Créer un objet conteneur▲
Comme indiqué précédemment, toute opération nécessitera de créer un fichier conteneur, ce qui est très facile.
2.
import
configparser
mon_conteneur =
ConfigParser.ConfigParser
(
)
II-B-2. Charger le conteneur▲
Une fois le fichier conteneur créé, il faut le remplir. Pour cela deux solutions :
- vous créez un nouveau fichier, il suffit donc d'y créer des sections/paramètres, ce que nous verrons après ;
- vous voulez ouvrir un fichier existant, il faut donc charger le contenu de ce fichier dans le conteneur.
Dans le cas de la seconde option, il suffit d'écrire la ligne suivante :
config.read
(
'example.cfg'
)
II-B-3. Sauver le contenu du conteneur▲
Une fois le contenu de votre fichier créé ou modifié dans le conteneur, vous avez besoin de le sauvegarder dans votre fichier. Qu'il s'agisse d'une création ou non, la procédure reste la même :
2.
with
open(
'example.cfg'
, 'w'
) as
configfile:
config.write
(
configfile)
II-B-4. Créer une section▲
Pour ajouter un paramètre, on suit la même logique que pour ajouter un nouvel élément à un dictionnaire. La principale différence viendra du fait que l'on déclarera non pas une valeur, mais un dictionnaire vide.
mon_conteneur['section_01'
]=
{}
II-B-5. Créer un paramètre▲
Ici nous respecterons par contre l'ajout d'un élément de dictionnaire.
mon_conteneur['section_01'
]['parametre_01'
]=
'valeur_01'
II-B-6. Modifier le nom d'une section ou d'un paramètre▲
Dans ces deux cas, il vous faudra créer une nouvelle section ou un nouveau paramètre, avec le nouveau nom et y copier le contenu de l'autre section/paramètre, puis effacer l'ancienne section/paramètre.
2.
mon_conteneur['section_01'
]['parametre_02'
]=
mon_conteneur['section_01'
]['parametre_01'
]
del
mon_conteneur['section_01'
]['parametre_01'
]
II-B-7. Modifier la valeur d'un paramètre▲
Nous procéderons ici comme pour changer la valeur d'une clé d'un dictionnaire Python.
mon_conteneur['section_02'
]['parametre_02'
]=
'nouvelle_valeur'
II-B-8. Connaître les sections et paramètres disponibles▲
Tout comme avec un dictionnaire Python, il suffit de récupérer les clés et de convertir le tout en liste :
liste_section =
list(
mon_conteneur.keys
(
))
ou encore :
liste_section =
list(
mon_conteneur['section_02'
].keys
(
))
II-B-9. Lire le contenu d'une section ou d'un paramètre▲
Là, une simple boucle FOR suffit :
2.
for
section, dict_parameters in
mon_conteneur.items
(
):
...
ou encore :
2.
for
parameter, value in
mon_conteneur['section_02'
].items
(
):
...
Vous pouvez également simplement récupérer la valeur du dictionnaire.
2.
parametres =
mon_conteneur['section_02'
]
valeur =
mon_conteneur['section_02'
]['parametre_02'
]
III. Le module json▲
III-A. Le format JSON▲
Le format JSON a été créé entre 2002 et 2005 par Douglas Crockford.
Il sert principalement à échanger des données entre programmes. Il permet de définir des types composés de données simples, communes à pratiquement tous les langages.
Il est assimilable à un dictionnaire Python, et est de fait parfaitement lisible par les humains.
Le format JSON n'accepte pas les commentaires.
2.
3.
4.
5.
6.
7.
8.
{
"test0"
: "valeur01"
,
"test1"
:
{
"test2"
: 2
,
"test3"
: 3
}
}
Comme on peut le voir ici, il ne faut pas oublier les virgules quand on change de ligne, sauf sur la dernière ligne.
III-B. Lire un JSON en Python▲
Lire un fichier JSON depuis un fichier est relativement simple. Nous passons par une variable intermédiaire, un conteneur, au même titre que le module configparser.
2.
3.
4.
5.
6.
7.
8.
import
json
with
open(
'./json.txt'
, 'r'
) as
fichier:
data =
json.load
(
fichier)
print
(
data["test0"
])
print
(
data["test1"
])
print
(
data["test1"
]["test2"
])
Comme on peut le constater ici, nous récupérons un objet JSON sous la forme d'un dictionnaire Python.
III-C. Modifier le contenu d'un JSON▲
Comme dit précédemment, nous disposons maintenant d'un simple dictionnaire Python. La manipulation se passe alors de tout commentaire.
Redéfinir une valeur dans le conteneur, s'effectue de la même façon que pour un dictionnaire. Cependant, attention : nous n'avons modifié ici que le contenu de notre dictionnaire. Pour modifier le JSON, il faut maintenant sauvegarder le contenu de notre dictionnaire dans notre fichier JSON.
III-D. Enregistrer un JSON en Python▲
Une fois que vous vous sentez prêt à enregistrer votre conteneur, il ne vous faudra que quelques lignes.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
import
json
with
open(
'./json.txt'
, 'r'
) as
fichier:
data =
json.load
(
fichier)
print
(
data["test0"
])
print
(
data["test1"
])
print
(
data["test1"
]["test2"
])
data["test1"
]["test2"
] =
56
print
(
data["test1"
]["test2"
])
with
open(
'./json.txt'
, 'w'
) as
fichier:
json.dump
(
data, fichier, sort_keys=
True
, indent=
4
,
ensure_ascii=
False
)
Nous voyons ici que nous passons par la fonction dump. Le premier argument doit être les données (votre conteneur), le deuxième le fichier de sortie. Viennent ensuite divers arguments possibles. Nous en utilisons ici deux : « indent » permet de paramétrer le nombre d'espaces pour l'indentation et « ensure_ascii » vous permet de stipuler si vous stockez les caractères spéciaux sous forme échappée (True) ou non.
III-E. Fausse bonne idée▲
Il existe une méthode alternative qui revient, a priori au même.
Vous avez la possibilité de transformer une chaîne de caractères, contenant un dictionnaire Python, directement en dictionnaire, et vice-versa. De fait, il est possible de se passer d'une librairie dédiée. J'attire cependant votre attention sur le fait qu'en faisant cela, nombre de mécanismes et le respect de la norme seront mis à mal.
Comme le laisse penser le titre de cette section, il s'agit là d'une fausse bonne idée. Pourquoi ? Principalement parce que cela représente une faille potentielle et que du code malveillant peut être alors exécuté.
En effet, imaginons qu'une de vos valeurs soit une commande, au format texte, exécutant un 'rm -rf /'. Pour peu que vous soyez en train d'exécuter votre code en tant que root (ce qui est fortement décommandé), vous risqueriez d'avoir quelques soucis.
La librairie json est spécialement conçue pour traiter les fichiers du même nom, avec des mécanismes de sécurité. Enfin, comme toute librairie dédiée, elle est optimisée afin que les temps de traitement soient les moins longs possible.
IV. Comparatif INI / JSON▲
IV-A. Limitation▲
Avant d'aborder notre comparatif, nous allons commencer par traiter les limitations de ces formats. Cela est nécessaire, car certaines limites brideront notre comparatif.
IV-A-1. Fichier INI▲
Côté fichier INI, la principale limitation vient du nombre de niveau d'imbrication. On est limité à deux niveaux. Le premier niveau, appelé header est obligatoire.
IV-A-2. Fichier JSON▲
Côté fichier JSON, la principale limitation provient des différentes normes existantes. En effet, même si l'une d'entre elles se détache des autres, (la principale déjà évoquée, la RFC7159, respectée par Python), elles ne sont pas 100 % compatibles entre elles.
Il en résulte des bibliothèques et des parsers différents, ce qui à un moment ou a un autre risque de poser problème dans les développements ou la compatibilité entre produits.
IV-B. Performance▲
Nous allons ici nous attaquer à un comparatif de type performance. Outre les INI et JSON, nous allons en plus effectuer une comparaison avec le XML, ce dernier étant également assez répandu.
Imaginons ici une entreprise. Les données identitaires de chaque utilisateur sont conservées dans un fichier dédié. Nous y trouverons filiale, nom, prénom, numéro de poste interne, mail et adresse IP du poste.
Afin de faire simple, nous nous contenterons uniquement d'analyser le chargement des données dans Python.
IV-B-1. Au plus simple▲
Commençons par des données brutes. Toutes les informations seront placées au même niveau.
Tout d'abord les fichiers contenant les informations :
2.
3.
4.
5.
6.
[employe]
nom
=
Leguenec
prenom
=
Yann
telephone
=
3441
mail
=
yann.leguenec@societe.bzh
ip
=
192
.168
.29
.35
2.
3.
4.
5.
6.
7.
{
"nom"
:"Leguenec"
,
"prenom"
:"Yann"
,
"telephone"
:"3441"
,
"mail"
:"yann.leguenec@societe.bzh"
,
"ip"
:"192.168.29.35"
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
<?xml version="1.0" encoding="utf-8"?>
<employe>
<nom>
Leguenec
</nom>
<prenom>
Yann
</prenom>
<telephone>
3441
</telephone>
<mail>
yann.leguenec@societe.bzh
</mail>
<ip>
192.168.29.35
</ip>
</employe>
Voici maintenant les codes Python associés pour les charger :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
import
configparser
import
time
start =
time.time
(
)
mon_conteneur =
configparser.ConfigParser
(
)
for
i in
range(
10000
):
mon_conteneur.read
(
'test.cfg'
)
stop =
time.time
(
)
print
(
"Execution time:
%.4f
s"
%
(
stop-
start))
2.
3.
4.
5.
6.
7.
8.
9.
from
lxml import
etree
import
time
start =
time.time
(
)
for
i in
range(
10000
):
xml_file =
etree.parse
(
"./test.xml"
)
stop =
time.time
(
)
print
(
"Execution time:
%.4f
s"
%
(
stop-
start))
Comme vous pouvez le constater, nous effectuons une boucle de 10 000 chargements. Pourquoi ? Tout simplement, car l'opération de chargement est tellement rapide que sur une seule itération de chargement, nous ne pourrions avoir de comparatif fiable.
À ce niveau, très basique, l'avantage est au JSON. Notons que le chargement des XML et des INI est similaire.
Type |
Temps d'exécution |
INI |
3.5924s |
JSON |
2.7243s |
XML |
3.8790s |
IV-B-2. Un cran plus haut▲
Maintenant, passons au niveau supérieur.
Les codes Python restent inchangés, de même que la ligne d'appel. Voici les nouveaux fichiers de données.
2.
3.
4.
5.
6.
7.
8.
[employe]
Nom
=
Leguenec
Prenom
=
Yann
[coordonnees]
Telephone
=
3441
Mail
=
yann.leguenec@societe.bzh
Ip
=
192
.168
.29
.35
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
{
"employe"
:
{
"Nom"
:"Leguenec"
,
"Prenom"
:"Yann"
},
"coordonnees"
:
{
"Telephone"
:"3441"
,
"Mail"
:"yann.leguenec@societe.bzh"
,
"Ip"
:"192.168.29.35"
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
<?xml version="1.0" encoding="utf-8"?>
<employes>
<employe>
<nom>
Leguenec
</nom>
<prenom>
Yann
</prenom>
</employe>
<coordonnees>
<telephone>
3441
</telephone>
<mail>
yann.leguenec@societe.bzh
</mail>
<ip>
192.168.29.35
</ip>
</coordonnees>
</employes>
L'avantage est toujours au JSON, avec environ une seconde, 25 à 30 %, de rapidité supplémentaire.
Type |
Temps d'exécution |
INI |
3.6304 s |
JSON |
2.8043 s |
XML |
3.8590 s |
V. Fichier de configuration ou base de données ?▲
Une grande question classique, déjà évoquée, est pourquoi un fichier de configuration plus qu'une base de données ?
Eh bien, les deux ont leurs avantages et leurs inconvénients. Dans certains cas même, il n'y aura aucun gain à choisir l'un ou l'autre.
Fournir un simple tableau serait pratique, mais quasiment impossible. En effet, il existe tellement de cas différents…
Tout d'abord d'un point de vue performance. Il faut prendre en compte la quantité de données que vous aurez à gérer, ainsi que les performances de votre base de données. Pour quelques dizaines de paramètres, un fichier de configuration sera tout aussi pratique qu'une base de données, et même plus performant. Au-delà, la base saura probablement se démarquer.
Côté sécurité, une base de données peut se sécuriser, d'un point de vue accès et droits. Vous pouvez gérer des droits d'accès par utilisateur ou par groupe d'utilisateurs. Un simple fichier de configuration ne vous offrira nullement ces éléments de sécurité.
Enfin, côté vérification. Si le XML par exemple, offre certains mécanismes de vérification, aucun format de fichier de configuration ne saura vous offrir autant d'outils (trigger, vérifications de champs vides, type de données…) qu'une base de données.
Aussi, si nous devions résumer, si vous avez besoin de sécurité et/ou d'effectuer des vérifications, envisagez plutôt une base de données.
Sinon, tout dépendra des performances et/ou de la quantité de données à traiter.
VI. Conclusion▲
Comme nous venons de le voir ensemble, les modules configparser et json peuvent rapidement s'avérer indispensables dès lors que nous avons besoin de gérer de faibles quantités de données spécifiques.
Loin de concurrencer les bases de données, tout leur intérêt apparaît dès lors que nous disposons d'une quantité raisonnable de données à gérer.
J'espère que ce petit tutoriel vous aura permis de (re)découvrir ces modules que l'on a tendance à trop souvent oublier.