Les modules de configuration Python

configparser et json

GALODE Alexandre

Lorsque l'on doit gérer des données dans un logiciel, on utilise généralement une base de données en arrière-plan.Cependant, comment faire pour gérer facilement des données relatives à un profil, données qui peuvent éventuellement être communiquées d'un PC à l'autre ?

C'est la réponse à cette question que je vous invite à découvrir, à travers l'utilisation des modules configparser et json.

Commentez

- Titre : Les modules de configuration Python

- Auteur : GALODE Alexandre

- Parution : 30 mars 2016

- Mise à jour : 31 mars 2016

- Public visé :

- Licence : Le contenu de cet article est rédigé par Alexandre GALODE et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Introduction

- II. Le module configparser

- II-A. Le format INI

- II-B. Python et les fichiers INI

- II-B-1. Créer un objet conteneur

- II-B-2. Charger le conteneur

- II-B-3. Sauver le contenu du conteneur

- II-B-4. Créer une section

- II-B-5. Créer un paramètre

- II-B-6. Modifier le nom d'une section ou d'un paramètre

- II-B-7. Modifier la valeur d'un paramètre

- II-B-8. Connaître les sections et paramètres disponibles

- II-B-9. Lire le contenu d'une section ou d'un paramètre

- III. Le module json

- III-A. Le format JSON

- III-B. Lire un JSON en Python

- III-C. Modifier le contenu d'un JSON

- III-D. Enregistrer un JSON en Python

- III-E. Fausse bonne idée

- IV. Comparatif INI / JSON

- IV-A. Limitation

- IV-A-1. Fichier INI

- IV-A-2. Fichier JSON

- IV-B. Performance

- IV-B-1. Au plus simple

- IV-B-2. Un cran plus haut

- V. Fichier de configuration ou base de données ?

- VI. Conclusion

- VII. Remerciements

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.

1. ;fichier INI de demonstration

2. [Section_01]

3. parametre1=valeur1

4. parametre2=''valeur2''

6. [Section_02]

7. parametre3=125

8. 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.

1. import configparser

2. 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 :

1. 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 :

1. with open('example.cfg', 'w') as configfile:

2. 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.

1. 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.

1. 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.

1. mon_conteneur['section_01']['parametre_02']=mon_conteneur['section_01']['parametre_01']

2. 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.

1. 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 :

1. liste_section = list(mon_conteneur.keys())

ou encore :

1. 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 :

1. for section, dict_parameters in mon_conteneur.items():

2. ...

ou encore :

1. for parameter, value in mon_conteneur['section_02'].items():

2. ...

Vous pouvez également simplement récupérer la valeur du dictionnaire.

1. parametres = mon_conteneur['section_02']

2. 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.

1. {

2. "test0": "valeur01",

3. "test1":

4. {

5. "test2": 2,

6. "test3": 3

7. }

8. }

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.

1. import json

3. with open('./json.txt', 'r') as fichier:

4. data = json.load(fichier)

6. print(data["test0"])

7. print(data["test1"])

8. 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.

1. import json

3. with open('./json.txt', 'r') as fichier:

4. data = json.load(fichier)

6. assert type(data) is dict

8. print(data["test0"])

9. print(data["test1"])

10. print(data["test1"]["test2"])

11. data["test1"]["test2"] = 56

12. print(data["test1"]["test2"])

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.

1. import json

3. with open('./json.txt', 'r') as fichier:

4. data = json.load(fichier)

6. print(data["test0"])

7. print(data["test1"])

8. print(data["test1"]["test2"])

9. data["test1"]["test2"] = 56

10. print(data["test1"]["test2"])

12. with open('./json.txt', 'w') as fichier:

13. json.dump(data, fichier, sort_keys=True, indent=4,

14. 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.

1. s="{'Hello':1, 'World':2}"

2. print(s)

3. print(type(s))

4. d=eval(s)

5. print(d)

6. print(type(d))

7. print(d['Hello'])

8. s2=str(d)

9. print(s2)

10. print(type(s2))

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 :

1. [employe]

2. nom=Leguenec

3. prenom=Yann

4. telephone=3441

5. mail=yann.leguenec@societe.bzh

6. ip=192.168.29.35

1. {

2. "nom":"Leguenec",

3. "prenom":"Yann",

4. "telephone":"3441",

5. "mail":"yann.leguenec@societe.bzh",

6. "ip":"192.168.29.35"

7. }

1. <?xml version="1.0" encoding="utf-8"?>

2. <employe>

3. <nom>

4. Leguenec

5. </nom>

6. <prenom>

7. Yann

8. </prenom>

9. <telephone>

10. 3441

11. </telephone>

12. <mail>

13. yann.leguenec@societe.bzh

14. </mail>

15. <ip>

16. 192.168.29.35

17. </ip>

18. </employe>

Voici maintenant les codes Python associés pour les charger :

ini_demo.py

1. import configparser

2. import time

4. start = time.time()

6. mon_conteneur = configparser.ConfigParser()

7. for i in range(10000):

8. mon_conteneur.read('test.cfg')

9. stop = time.time()

11. print("Execution time: %.4fs" % (stop-start))

json_demo.py

1. import json

2. import time

4. start = time.time()

6. for i in range(10000):

7. with open('./test.json', 'r') as fichier:

8. data = json.load(fichier)

9. stop = time.time()

11. print("Execution time: %.4fs" % (stop-start))

xml_demo.py

1. from lxml import etree

2. import time

4. start = time.time()

5. for i in range(10000):

6. xml_file = etree.parse("./test.xml")

7. stop = time.time()

9. print("Execution time: %.4fs" % (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.

1. [employe]

2. Nom=Leguenec

3. Prenom=Yann

5. [coordonnees]

6. Telephone=3441

7. Mail=yann.leguenec@societe.bzh

8. Ip=192.168.29.35

1. {

2. "employe":

3. {

4. "Nom":"Leguenec",

5. "Prenom":"Yann"

6. },

7. "coordonnees":

8. {

9. "Telephone":"3441",

10. "Mail":"yann.leguenec@societe.bzh",

11. "Ip":"192.168.29.35"

12. }

13. }

1. <?xml version="1.0" encoding="utf-8"?>

2. <employes>

3. <employe>

4. <nom>

5. Leguenec

6. </nom>

7. <prenom>

8. Yann

9. </prenom>

10. </employe>

11. <coordonnees>

12. <telephone>

13. 3441

14. </telephone>

15. <mail>

16. yann.leguenec@societe.bzh

17. </mail>

18. <ip>

19. 192.168.29.35

20. </ip>

21. </coordonnees>

22. </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.

VII - Remerciements

- Vincent.mbg

- Stendhal666

- jacques_jean