1. Rappel sur les différences gzip/bzip2 et zip/tar
Les fichiers au format gzip ou bzip2 sont dits compressés. Seul l'algorithme de compression (et de décompression) les différencie. Bzip2 est le plus efficace en compression, mais aussi bien plus demandeur en ressources (le nouveau format xz est encore plus efficace tout en étant moins gourmand en ressources). Ces formats de compression ne concernent qu'un seul fichier. Ils ne permettent pas l'inclusion de plusieurs fichiers ou de répertoires entiers contrairement aux fichiers que je nomme « archives » dont le format peut être de type zip ou tar entre autres.
Le type de fichier tar n'est pas compressé par défaut. Toutefois, il est le plus souvent compressé à l'aide de gzip ou bzip2 (en réalité, il est très rare de trouver des archives tar non compressées). Enfin, le type de fichier zip est quant à lui généralement compressé par défaut avec un taux moyen. Le module zipfile de Python ne compresse pas par défaut et utilise l'archive zip comme un conteneur simple. Il est toutefois possible de lui demander d'effectuer la compression et il me semble que ce serait dommage de s'en priver avec les processeurs actuels.
Le module permettant de gérer le format de fichier xz, qui est basé sur la compression LZMA, est en cours d'inclusion dans Python (cf. [xz]) ainsi que dans le module tarfile qui est survolé dans cet article.
2. Utiliser gzip ou bzip2 avec Python
2.1 Exemple de lecture d'un fichier
Ces deux modules donnent accès aux fonctions des librairies écrites en C qui font le travail effectif. L'utilisation de leurs fonctionnalités de base est identique. En effet, ces deux modules fournissent des classes qui surchargent certaines fonctions d'utilisation de fichiers telles open(),(seulement disponible dans le module gzip – on utilisera BZ2File() pour ouvrir un fichier bz2) close(), read(), readline(), write(), writelines(). Ainsi, on accède à un fichier de type gzip ou bzip2 exactement comme s'il s'agissait d'un fichier « normal ». Attention, en Python 3 les modules retournent une variable de type bytes qu'il faudra « décoder » si l'on souhaite la transformer en chaîne de caractères.
Voici un exemple, simplifié à l'extrême, qui permet d'afficher le contenu de fichiers textes qu'ils soient plats ou compressés avec gzip ou bzip2. Le lecteur soucieux d'écrire un programme plus propre pourra, en s'inspirant des articles de ce hors-série, ajouter des blocs try...except pour la gestion d'erreurs systèmes, utiliser le module magic [magic] pour détecter le format du fichier (plutôt que de se baser sur son extension) et enfin gérer les éventuelles erreurs de décodage des chaînes issues des fichiers compressés :
#!/usr/bin/python3
# -*- coding: utf8 -*-
import os
import sys
import getopt
import gzip
import bz2
opts, args = getopt.getopt(sys.argv[1:], [], [])
for fichier in args:
if '.gz' in (fichier):
the_file = gzip.open(fichier, 'rb')
elif '.bz2' in (fichier):
the_file = bz2.BZ2File(fichier, 'rb')
else:
the_file = open(fichier, 'r')
for line in the_file:
if type(line) is str:
print('%s' % line, end='')
elif type(line) is bytes:
print('%s' % line.decode('utf_8', 'ignore'), end='')
the_file.close()
On pourra appeler pompeusement ce programme cat.py et l'utiliser comme suit (sur des fichiers qui ne contiennent que du texte, comme par exemple des fichiers de log) :
./cat.py *.log *.log.gz *.log.bz2
2.2 Exemple d'ajout de données dans un fichier gzip
Les remarques concernant la gestion des erreurs et des exceptions de l'exemple précédent s'appliquent ici également. Cet exemple montre comment ajouter des données (ici, il s'agit de données issues de fichiers) à une archive de type gzip. Attention, ici on ajoute bien le contenu de fichiers à la suite de notre fichier compressé. Décompresser le fichier obtenu ne permettra pas de récupérer les fichiers que nous lui avons ajoutés, mais bien un seul fichier contenant l'ensemble, les uns à la suite des autres.
J'ai mis une option (que j'ai rendue obligatoire) sur la ligne de commandes pour indiquer le nom du fichier qui sera le fichier compressé auquel on souhaite ajouter des données. En effet, se baser sur l'ordre des arguments pour cela n'est pas possible, car ils ne sont pas forcément retournés dans l'ordre dans lequel ils ont été entrés.
Le module getopt est toujours valable pour l'utilisation dans Python 3. Seul le module optparse est déprécié à partir de la version 3.2 et remplacé par le module argparse [argparse]. Ce module permet beaucoup plus de choses sur la ligne de commandes comme par exemple les options comportant un nombre d'arguments variable.
Essayer ce programme avec un fichier de type bzip2 ne fonctionnera pas ! Il conviendra de remplacer gzip.GzipFile par bz2.BZ2File pour obtenir le même résultat avec un fichier de type bzip2. En effet, ce programme a été écrit pour montrer l'utilisation de la commande with qui a été introduite dans Python 3.1 [with] pour le module gzip. Le module bzip2semble supporter cette instruction depuis Python 3.0 (elle a été introduite dans Python 2.7 pour faciliter le passage à la version 3).
Le lecteur soucieux d'une plus grande généralité pourra récrire ce programme à la manière de l'exemple précédent :
#!/usr/bin/python3
# -*- coding: utf8 -*-
import os
import sys
import getopt
import gzip
short_options = "f:"
long_options = ["file="]
archive = ''
opts, args = getopt.getopt(sys.argv[1:], short_options, long_options)
for opt, arg in opts:
if opt in ('-f', '--file'):
archive = arg
if archive != '' and len(args) > 1:
with gzip.GzipFile(archive, 'ab') as arch:
for fichier in args:
the_file = open(fichier, 'rb')
arch.writelines(the_file.readlines());
print('Added %s' % fichier)
the_file.close()
arch.close()
3. Utiliser les fichiers d'archives de type zip
Le module zipfile qui permet d'utiliser les archives zip dans Python ne permet pas l'utilisation d'archives scindées en plusieurs morceaux, mais en revanche, peut utiliser des archives dont la taille est supérieure à 4Go à condition toutefois qu'elles contiennent les extensions ZIP64. De même, ce module ne gère pas encore le chiffrement des données mais seulement le déchiffrement (qui est, aux dires de la page de documentation [zipfile], extrêmement lent).
Tout d'abord, ce module contient une fonction qui permet de vérifier si un fichier est, ou non, une archive zip : zipfile.is_zipfile('mon_fichier.zip'). Elle renvoie True quand le fichier est bien une archive zip et False sinon.
3.1 Lister les fichiers contenus dans une archive zip
Le module zipfile définit une classe ZipInfo qui stocke les données descriptives des fichiers contenus dans l'archive zip. On trouvera notamment le nom du fichier, les éventuels commentaires associés, la taille originelle du fichier, sa taille une fois compressé et la date de sa dernière modification (dans l'archive). Il existe de nombreux autres paramètres (comme un code de correction d'erreur basique CRC32) que le lecteur curieux pourra découvrir sur [zipinfo].
Les fichiers au format OpenDocument (c'est le cas de cet article par exemple) sont au format zip. Ainsi, l'exemple suivant, qui liste les fichiers contenus dans l'archive avec leurs tailles, met en évidence que la taille totale des fichiers OpenDocument est principalement due à un fichier image miniature (plus de 70% dans le cas de cet article !). C'est cette image qui est affichée par les gestionnaires de fichiers (comme Thunar) lorsqu'ils affichent un fichier de ce type.
zfile = zipfile.ZipFile('mon_article.odt', 'r')
for zinfo in zfile.infolist():
print('%s (%s --> %s)' % (zinfo.filename, zinfo.file_size, zinfo.compress_size))
zfile.close()
3.2 Extraire un fichier d'une archive zip
Voyons comment extraire cette fameuse image qui se trouve dans un dossier virtuel nommé Thumbnails contenu dans l'archive. L'image en elle-même se nommant thumbnail.png. La classe ZipFile implémente la fonction read qui permet de lire un fichier dans une archive zip (les données sont décompressées de manière transparente si nécessaire). L'exemple suivant montre comment lire ce fameux fichier image et le sauvegarder dans un nouveau fichier qui sera nommé image.png. Il faut bien prendre soin de préciser à la fonction open qu'il s'agit d'un fichier binaire que nous voulons créer en ajoutant b après le w. En effet, les données que nous lisons dans l'archive et que nous allons écrire dans ce nouveau fichier sont de type binaire.
zfile = zipfile.ZipFile('mon_article.odt', 'r')
zdata = zfile.read('Thumbnails/thumbnail.png')
fichier = open('image.png', 'wb')
fichier.write(zdata)
fichier.close()
zfile.close()
Il existe également une fonction toute faite nommée extract qui permet de réaliser une opération similaire. La fonction extractall, quant à elle, extraira l'ensemble des fichiers de l'archive. Ces fonctions recréent les arborescences, c'est-à-dire, que l'utilisation de la fonction sur l'exemple précédent : zfile.extract('Thumbnails/thumbnail.png') créera le répertoire Thumbnails et le fichier thumbnail.png à l'intérieur de celui-ci.
La documentation indique qu'il est préférable d'utiliser la fonction extractall qui est plus sécurisée que la fonction extract notamment sur la gestion des extractions possibles des fichiers dans un répertoire se trouvant dans un niveau supérieur à celui où l'archive est extraite.
3.3 Inclure un nouveau fichier dans une archive zip existante
L'inclusion d'un fichier dans une archive existante est réalisée grâce à la fonction write. Au préalable, on aura pris soin d'ouvrir l'archive en mode ajout a ou écriture w. Attention toutefois si vous ouvrez une archive existante avec le mode d'écriture, les fichiers qu'elle contient seront effacés ! Nous pouvons par exemple inclure le logo du projet Heraia [heraia] dans notre document précédent :
zfile = zipfile.ZipFile('mon_article.odt', 'a')
zfile.write('heraia.png', arcname='Logos/heraia.png')
zfile.close()
Ici, nous ajoutons dans l'archive mon_article.odt le fichier heraia.png qui se trouve dans le même dossier que le script. Le paramètre arcname permet de spécifier un chemin et un nom de fichier. Ici, le fichier est placé dans un dossier virtuel nommé Logos. Il aurait également été possible de renommer le fichier dans la même opération en spécifiant arcname='Logos/heraia_256x256.png' par exemple.
Le fichier est ajouté tel quel, sans compression, puisque c'est le comportement par défaut. Afin de pouvoir bénéficier de la compression, il faut utiliser le paramètre compress_type avec la fonction write (et ce ne sera valable que pour cette écriture bien précise, indépendamment du reste de l'archive) ou bien utiliser le paramètre compression à l'ouverture du fichier. Ces paramètres ne prennent que deux valeurs : ZIP_STORED (on ne compresse pas) ou ZIP_DEFLATED (on compresse) :
zfile = zipfile.ZipFile(archive, 'a', compression=zipfile.ZIP_DEFLATED)
ou
zfile.write('kern.log', arcname='logs/kern.log', compress_type=zipfile.ZIP_DEFLATED)
Je vous recommande, lorsque vous insérez un fichier dans une archive existante, de bien vérifier qu'il n'y soit pas déjà. En effet, s'il existe déjà, cela ajoutera bien le fichier, mais créera ainsi un doublon (cf. le bug [doublon] toujours ouvert à l'heure où j'écris ces lignes) qu'il vous sera bien difficile de différencier par la suite (cf. note ci-dessous).
Le module zipfile ne fournit pas de fonction permettant de supprimer un fichier dans une archive. Comme il n'est pas possible de remplacer un fichier d'une archive zip en incluant un nouveau fichier ayant le même nom, car il est simplement ajouté à l'archive. L'extraction avec la fonction extractall de deux fichiers comportant deux noms identiques aura pour résultat l'écrasement du premier fichier par le deuxième (celui inclus le plus récemment dans l'archive). Ce comportement n'est pas celui de l'utilitaire unzip qui posera la question à l'utilisateur de savoir que faire avec le doublon : « replace Thumbnails/thumbnail.png? [y]es, [n]o, [A]ll, [N]one, [r]ename: ».
L'ajout d'un fichier dans une nouvelle archive ne diffère en rien de celui dans une archive pré-existante. Attention, créer une archive de type zip et n'y ajouter aucun fichier, puis la fermer produira un fichier vide qui ne sera pas reconnu comme une archive zip !
3.4 Ajouter ses propres données dans les fichiers d'une archive zip
Nous savons maintenant ajouter (fonction write) et extraire (fonctions extract et extractall) des fichiers d'une archive de type zip. Nous savons même comment lire les données d'un fichier sans l'extraire (fonction read). Il serait peut-être temps de voir comment remplir des fichiers d'une archive avec nos données !
Avant toute chose, il faut avoir ouvert l'archive dans laquelle on souhaite écrire, soit en mode ajout a, soit en mode écriture w. Écrire dans un fichier de l'archive ainsi ouverte se réalise à l'aide de la fonction writestr. Elle prend pour arguments le nom du fichier dans lequel on souhaite écrire (ou un objet zipinfo) et une chaîne de caractères de type bytes (qui contient les données à écrire).
L'exemple ci-dessous crée l'archive zip nommée nombres.zip et y ajoute deux fichiers nommés premiers.txt et periodes.txt contenant respectivement des nombres premiers et les différences entre eux. L'archive est ouverte en mode ZIP_DEFLATED ce qui indique que les données seront compressées de manière transparente.
premiers = b'1\n2\n3\n5\n7\n11\n13\n'
periodes = b'1\n1\n2\n2\n4\n2\n'
zfile = zipfile.ZipFile('nombres.zip', 'w', compression=zipfile.ZIP_DEFLATED)
zfile.writestr('premiers.txt', premiers)
zfile.writestr('periodes.txt', periodes)
zfile.close()
Vérifions cela :
$ unzip nombres.zip
Archive: nombres.zip
inflating: premiers.txt
inflating: periodes.txt
$ cat premiers.txt
1
2
3
5
7
11
13
$ cat periodes.txt
1
1
2
2
2
4
2
4. Utiliser les fichiers d'archive de type tar
L'archive de type tar est une archive non compressée. Comme dans la vraie vie il est très rare d'en rencontrer une qui ne le soit pas, le module tarfile [tarfile] permet la manipulation de ces archives qu'elles soient compressées (via gzip ou bzip2 - xz est pour bientôt !) ou non.
Les interfaces entre le module zipfile et le module tarfile ne sont pas encore complètement uniformisées, mais les fonctionnalités sont quasiment identiques. En effet, il est possible de vérifier si un fichier est bien de type tar grâce à la fonction is_tarfile('monfichier.tar.gz'). Elle renvoie True si c'est le cas et False sinon. De plus elle générera une erreur IOError si le fichier n'existe pas ou ne peut être lu.
4.1 Lister les fichiers contenus dans une archive tar
Pour lister le contenu d'une archive, le module tarfile procure une classe nommée tarinfo qui remplit un rôle similaire à zipinfo. Chaque objet de type tarinfo [tarinfo] contient le nom d'un fichier ainsi que quelques attributs qui lui sont associés tels sa taille, son type, le nom et le groupe de l'utilisateur auquel il appartient (au sens Unix du terme), ses permissions, etc. La fonction getmembers, sans paramètres, renvoie la liste des objets tarinfo d'une archive de type tar. C'est-à-dire la liste des fichiers de l'archive avec tous leurs attributs :
#!/usr/bin/python3
# -*- coding: utf8 -*-
import tarfile
t = tarfile.open('exemple2.tar.gz', 'r')
for finfo in t.getmembers():
print('%s a une taille de %d octets' % (finfo.name, finfo.size), end='')
if finfo.isdir():
print(' et est un répertoire')
elif finfo.isfile():
print(' et est un fichier normal')
Ici, le type de l'objet tarinfo (variable nommée finfo) est testé grâce aux fonctions booléennes isdir et isfile. Il existe 6 autres fonctions de ce type qui permettent de savoir si un fichier donné d'une archive tar est un lien symbolique, un périphérique, un tube nommé, etc.
4.2 Extraire un fichier d'une archive tar
Extraire un fichier d'une archive tar se réalise pratiquement de la même manière que pour une archive zip. En effet, il existe les deux fonctions extract et extractall qui permettent respectivement d'extraire un fichier ou tous les fichiers de l'archive. La note sur la sécurité des deux fonctions identiques du module zipfile est également valable ici.
Il existe également la fonction extractfile qui renvoie un objet de type fichier. Avec cet objet, il est possible de réaliser la lecture de ses données et donc de les écrire dans un autre fichier (comme je l'ai fait dans l'exemple pour l'archive de type zip). Ainsi, l'exemple suivant lit le fichier nommé premiers.txt qui est présent dans l'archive exemple2.tar.gz et copie ses données dans un nouveau fichier (hors de l'archive) nommé nombres_premiers.txt la décompression ayant été effectuée de manière transparente (qu'elle soit de type gzip ou de type bzip2) :
t = tarfile.open('exemple2.tar.gz', 'r')
extr = t.extractfile('premiers.txt')
fichier = open('nombres_premiers.txt', 'wb')
for lines in extr:
fichier.write(lines)
fichier.close()
extr.close()
t.close()
4.3 Créer une archive tar et y ajouter des fichiers
La création d'une archive tar est différente de celle d'une archive zip en ceci qu'il existe la possibilité de choisir non seulement si l'archive sera ou non compressée, mais également le type de la compression (gzip ou bzip2 vus précédemment). Si la lecture peut s'effectuer de manière transparente quelque soit le type de compression, l'écriture nécessite quant à elle de spécifier clairement le type de compression. Ainsi, si le mode d'ouverture est w, l'archive sera créée sans compression. S'il s'agit de w:gz le type de compression gzip sera choisi. Enfin, s'il s'agit de w:bz2 ce sera le type bzip2 :
t = tarfile.open('premiers.tar', 'w')
tz = tarfile.open('premiers.tar.gz', 'w:gz')
tbz2 = tarfile.open('premiers.tar.bz2', 'w:bz2')
t.add('premiers.txt')
tz.add('premiers.txt')
tbz2.add('premiers.txt')
t.close()
tz.close()
tbz2.close()
Cet exemple crée trois archives respectivement non compressée, compressée de type gzip et enfin compressée de type bzip2. Le fichier premiers.txt est ajouté dans chacune de ces archives à l'aide de la fonction add. Ce qui donne les fichiers suivants :
$ ls -ls premiers.t*
12 -rw-r--r-- 1 dup dup 10240 2012-01-03 22:03 premiers.tar
4 -rw-r--r-- 1 dup dup 142 2012-01-03 22:03 premiers.tar.bz2
4 -rw-r--r-- 1 dup dup 147 2012-01-03 22:03 premiers.tar.gz
4 -rw------- 1 dup dup 16 2011-12-30 17:59 premiers.txt
La taille de 10240 octets pour l'archive premiers.tar s'explique par le format du fichier tar [tar format] qui utilise des blocs de taille fixe constitués en réalité de 20 petits blocs d'une taille de 512 octets chacun soit 10240 octets. Les blocs non utilisés sont complétés avec des zéros ce qui explique les excellents taux de compression pour les types gzip (69 fois) et bzip2 (72 fois) obtenus dans cet exemple.
La fonction add, comme la fonction write, pour les archives zip, permet d'ajouter des répertoires entiers de manière récursive, c'est-à-dire que tous les fichiers et sous-répertoires seront ajoutés à l'archive (y compris les fichiers cachés). En effet, si j'avais écrit t.add('/home/dup') dans l'exemple précédent, l'archive premiers.tar aurait contenu l'ensemble de mon répertoire personnel. Pour éviter le comportement récursif (par défaut), il est possible d'ajouter le paramètre recursive=False de la manière suivante : add('/home/dup', recursive=False).
La documentation indique qu'il n'est pas possible d'ajouter des fichiers dans une archive tar pré-existante et compressée (que ce soit par gzip ou bzip2). En revanche, il est tout à fait possible d'ajouter des fichiers dans une archive pré-existante, mais non compressée toujours grâce à la fonction add.
4.4 Ajouter ses propres données dans les fichiers d'une archive tar
Dans le module tarfile il n'existe pas de fonction équivalente à writestr du module zipfile. Pour écrire dans un fichier d'une archive tar, il convient d'utiliser la structure tarinfo et la fonction addfile. La dernière remarque du paragraphe précédent implique soit de créer une archive à partir de zéro, soit d'utiliser une archive non compressée (que l'on pourra éventuellement compresser par la suite – cf. chapitre 2). Dans l'exemple suivant, j'ai choisi de créer une archive compressée de type bzip2. La méthode présentée peut paraître compliquée, mais elle montre la capacité de la fonction addfile à travailler avec des flux. Il peut s'agir de n'importe quel flux pourvu que l'interface d'utilisation de ce flux soit semblable à celle des fichiers. Ici, j'utilise le module io qui implémente ce type d'interface : io.BytesIO.
#!/usr/bin/python3
# -*- coding: utf8 -*-
import tarfile
import io
premiers = b'1\n2\n3\n5\n7\n11\n13\n'
periodes = b'1\n1\n2\n2\n4\n2\n'
tarbz2 = tarfile.open('nombres.tar.bz2', 'w:bz2')
info = tarfile.TarInfo('premiers.txt')
info.size = len(premiers)
tarbz2.addfile(info, io.BytesIO(premiers))
tarbz2.close()
À partir de la version 3 de Python, le module io remplace les modules StingIO et cStringIO.
Conclusion
Cet article n'est qu'un survol rapide des possibilités offertes par les modules gzip, bz2, zipfile et tarfile. Toutefois, nous n'avons maintenant plus d'excuse pour ne pas compresser nos fichiers lorsque nous les créons. Cela économise de la place et, avec les processeurs actuels, ne grève pas vraiment les performances.
De même, plutôt que de générer des dizaines de fichiers les uns à côté des autres, nous devrions les générer dans une seule et même archive ! Les systèmes de fichiers seraient alors soulagés d'autant de fichiers !
La seule vraie difficulté dans tout cela est bien celle de choisir le format de compression et/ou d'archive en fonction de leurs fonctionnalités et de leurs limites !!
Références
[xz] : http://bugs.python.org/issue6715
[magic] : https://github.com/ahupp/python-magic
[with] : http://docs.python.org/dev/library/gzip.html
[argparse] : http://www.python.org/dev/peps/pep-0389
[zipfile] : http://docs.python.org/library/zipfile.html
[zipinfo] : http://docs.python.org/py3k/library/zipfile.html?highlight=zipfile#zipinfo-objects
[heraia] : http://heraia.tuxfamily.org/
[doublon] : http://bugs.python.org/issue2824
[tarfile] : http://docs.python.org/release/3.1.3/library/tarfile.html
[tarinfo] : http://docs.python.org/release/3.1.3/library/tarfile.html#tarfile.TarInfo
[tar format] : http://fr.wikipedia.org/wiki/Tar_(informatique)
Et les exemples de Doug Hellmann :
http://www.doughellmann.com/PyMOTW/zipfile/
http://www.doughellmann.com/PyMOTW/tarfile/