IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Think Julia


précédentsommairesuivant

14. Fichiers

Ce chapitre introduit la notion de programmes persistants, capables de conserver les données en stockage permanent. Il montre comment utiliser différents types de stockage permanent tels que les fichiers et les bases de données.

14-1. Persistance

La plupart des programmes que nous avons étudiés jusqu'à présent sont transitoires : ils fonctionnent pendant une courte période et produisent un résultat. Cependant, lorsqu'ils se terminent, leurs données disparaissent. Lors d'une nouvelle exécution, le programme repart ut nulla.

D'autres programmes sont persistants : ils s'exécutent sur de longues périodes (voire continuellement) et conservent au moins une partie de leurs données sur un support de stockage permanent (un disque dur, par exemple). S'ils s'arrêtent puis redémarrent, ils reprennent au point d'arrêt.

Les systèmes d'exploitation qui fonctionnent pratiquement à chaque fois qu'un ordinateur est allumé sont des exemples de programmes persistants. C'est encore plus vrai pour les serveurs web, qui fonctionnent en permanence, attendant des requêtes en provenance du réseau.

Un des moyens les plus simples pour que les programmes conservent leurs données consiste à lire et à écrire des fichiers « texte ». Jusqu'ici, nous avons vu des programmes qui lisent des fichiers texte. Dans ce chapitre, nous en étudions dont le comportement consiste à écrire dans des fichiers.

Une autre possibilité consiste à stocker l'état d'un programme dans une base de données. Nous étudions donc également la manière de tirer avantage d'une base de données simple.

14-2. Lire et écrire

Un fichier texte est une séquence de caractères enregistrée sur un support permanent comme un disque dur ou une mémoire flash. Nous avons vu comment ouvrir et lire un fichier dans la section 9.1Lecture de listes de mots.

Pour créer un fichier et y écrire, il faut l'ouvrir avec le mode "w" (writing) en deuxième paramètre :

 
Sélectionnez
1.
2.
julia> fout = open("output.txt", "w")
IOStream(<file output.txt>)

Si le fichier existe déjà, l'ouverture en mode écriture efface irréversiblement les anciennes données. Prudence, donc ! Si le fichier n'existe pas, il est nouvellement créé. La fonction open retourne un objet fichier et la fonction d'écriture "w" permet d'écrire diverses données dans le fichier.

 
Sélectionnez
1.
2.
3.
julia> line1 = "Voici quatre lions, deux rouges, deux noirs,\n";
julia> write(fout, line1)
45

La valeur numérique retournée représente le nombre de caractères écrits dans le fichier. L'objet fichier conserve une trace de l'endroit où il se trouve dans l'arborescence de l'ordinateur. Par conséquent, si elle est à nouveau appelée, la fonction write ajoute les nouvelles données à la suite des précédentes :

 
Sélectionnez
1.
2.
3.
julia> line2 = "Tels qu'ils figurent sur les armoiries d'Élouges.\n";
julia> write(fout, line2)
51

Une fois la saisie terminée, il est pertinent de fermer le fichier.

 
Sélectionnez
1.
julia> close(fout)

Si la fermeture du fichier n'a pas eu lieu, la terminaison du programme s'en chargera. L'état du fichier peut être contrôlé en l'ouvrant avec un éditeur de texte.

14-3. Formatage

L'argument en écriture doit impérativement être une chaîne de caractères. Ainsi, pour écrire d'autres valeurs, il est nécessaire de les convertir en chaînes de caractères. Le moyen le plus simple est de procéder via l'interpolation de chaînes :

 
Sélectionnez
1.
2.
3.
4.
julia> fout = open("output.txt", "w")
IOStream(<file output.txt>)
julia> write(fout, string(7370))
4

Une autre possibilité consiste à utiliser la famille de fonctions print(ln).

 
Sélectionnez
1.
2.
julia> chapelles = 2
julia> println(fout, "À Élouges, il y a $chapelles chapelles.")

La macro @printf est très puissante du fait qu'elle utilise des commandes de formatage de style C. Voir la documentation de printf.

14-4. Noms de fichiers et chemins

Les fichiers sont organisés en répertoires (également appelés « dossiers »). Chaque programme en cours d'exécution possède un répertoire courant, c'est-à-dire celui par défaut pour la plupart des opérations. Par exemple, lorsqu'un fichier est ouvert aux fins de lecture, Julia le cherche dans le répertoire courant.

La fonction pwd retourne le nom du répertoire courant :(36)

 
Sélectionnez
1.
2.
julia> cwd = pwd()
"/home/aquarelle/Julia/Penser_en_Julia"

cwd signifie current working directory (répertoire de travail courant). Dans cet exemple, le résultat est /home/aquarelle/Julia/Penser_en_Julia, où /home/aquarelle/ est le répertoire d'origine d'un utilisateur(37) nommé aquarelle.

Une chaîne comme /home/aquarelle/Julia/ identifie un répertoire et constitue un chemin d'accès absolu.

Un simple nom de fichier, comme output.txt, est considéré comme un chemin relatif, car il se rapporte au répertoire courant. Si le répertoire courant est /home/aquarelle/Julia/Penser_en_Julia/, le nom de fichier output.txt fera référence à /home/aquarelle/Julia/Penser_en_Julia/output.txt.

Un chemin qui commence par / ne dépend pas du répertoire courant. Il s'agit d'un chemin absolu. Pour trouver le chemin absolu d'un fichier, il convient d'utiliser abspath :

 
Sélectionnez
1.
2.
julia> abspath("output.txt")
"/home/aquarelle/Julia/Penser_en_Julia/output.txt"

Julia propose d'autres fonctions pour travailler avec les noms de fichiers et les chemins d'accès. Par exemple, ispath vérifie si un fichier ou un répertoire existe :

 
Sélectionnez
1.
2.
3.
4.
julia> ispath("/home/aquarelle/Julia/Penser_en_Julia")
true
julia> ispath("/home/aquarelle/Julia/Penser_en_Julia/mots_FR.txt")
true

isdir vérifie l'existence d'un sous-répertoire appartenant au répertoire courant (les répertoires situés au-dessus du répertoire courant, y compris ce dernier, ne sont pas testés) :

 
Sélectionnez
1.
2.
3.
4.
julia> isdir("Chimie_Julia")
false
julia> isdir("Figures")
true

De même, isfile vérifie l'existence d'un fichier.

readdir retourne un tableau des fichiers (et autres répertoires) dans le répertoire courant :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
julia> isfile("Figures/quadratic_1.pdf")
true
julia> readdir(cwd)
7-element Array{String,1}:
   "#Penser_en_Julia_v-0-24.lyx#"
   "Solutions_Exercices"
   "Figures"
   "Penser_en_Julia_v-0-24.lyx"
   "Penser_en_Julia_v-0-24.lyx~"
   "Penser_en_Julia_v-0-24.pdf"
   "Tests"
   "mots_FR.txt"
   "notre_dame_de_paris.txt"

Pour illustrer l'action de ces fonctions, la fonction walk explore un répertoire, affiche le nom de tous les fichiers et s'appelle récursivement au niveau des sous-répertoires.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
function walk(dirname)
    for name in readdir(dirname)
        path = joinpath(dirname, name)
        if isfile(path) 
            println(path)
        else
            walk(path)
        end
    end
end

La fonction interne joinpath prend un répertoire et un nom de fichier pour les réunir en un chemin complet.

Julia propose une fonction appelée walkdir (voir la page de documentation de walkdir) qui est similaire à walk tout en s'avérant polyvalente. À titre d'exercice, lisez la documentation de walkdir et utilisez cette fonction pour afficher les noms des fichiers dans un répertoire donné et ses sous-répertoires.

14-5. Levée des exceptions

Lorsque des fichiers sont manipulés en lecture et en écriture, des erreurs peuvent survenir. Tenter d'ouvrir un fichier qui n'existe pas conduit à une SystemError :

 
Sélectionnez
1.
2.
julia> fin = open("fichier_inexistant.txt") 
ERROR: SystemError: opening file "bad_file": Aucun fichier ou dossier de ce type

Si vous ne disposez pas des droits d'accès à un fichier (en l'occurrence en écriture), sous GNU/Linux par exemple, une erreur se produit :

 
Sélectionnez
1.
2.
julia> fout = open("/etc/passwd", "w")
ERROR: SystemError: opening file "/etc/passwd": Permission non accordée

Afin d'éviter ces erreurs, il serait envisageable d'utiliser des fonctions comme ispath et isfile. Hélas, cela nécessiterait beaucoup de temps et de code pour vérifier toutes les possibilités.

La fonction try offre une solution élégante. La syntaxe est similaire à celle d'une instruction if :

 
Sélectionnez
1.
2.
3.
4.
5.
try 
    fin = open("fichier_inexistant.txt") 
catch exc
    println("Une erreur s'est produite: $exc")
end

Julia commence par exécuter la clause try. Si tout se passe normalement, le système sort de la clause try et poursuit l'exécution du programme. Si une exception se produit, Julia passe à l'exécution de la clause catch.

Le traitement d'une exception avec une clause try s'appelle la levée d'une exception. Dans cet exemple, la clause d'exception affiche un message d'erreur (peu utile). Cependant, en général, capter une exception apporte une chance réelle de résoudre le problème, d'essayer à nouveau ou, au moins, de terminer le programme sobrement.

Dans un code qui effectue des changements d'état ou qui utilise des ressources telles que des fichiers, un travail de nettoyage (fermeture de fichiers) doit généralement être réalisé lorsque le code est terminé. Les exceptions compliquent potentiellement cette tâche, car elles peuvent provoquer la sortie d'un bloc de code avant d'atteindre sa fin normale. Le mot-clé finally permet d'exécuter un code lorsqu'un bloc se termine, quelle que soit la manière dont il prend fin :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
f = open("output.txt")
try
    line = readline(f)
    println(line) 
finally
    close(f)
end

De cette sorte, la fonction close est toujours exécutée.

14-6. Bases de données

Une base de données est un fichier spécialement organisé pour conserver des données. De nombreuses bases de données sont structurées comme un dictionnaire : elles établissent une correspondance entre des clés et leurs valeurs. La plus grande différence entre une base de données et un dictionnaire vient de ce que la première réside sur un support permanent. Par conséquent, elle persiste après la fin du programme.

ThinkJulia fournit une interface au gestionnaire de base de données GDBM (GNU dbm(38)) pour la création et la mise à jour des fichiers de base de données. À titre d'exemple, créons une base de données qui contient des légendes associées à des fichiers contenant des images.

L'ouverture d'une base de données est similaire à l'ouverture de fichiers classiques :

 
Sélectionnez
1.
2.
3.
4.
julia> using ThinkJulia

julia> db = DBM("légendes", "c")
DBM(<légendes>)

Le mode "c" signifie que la base de données doit être créée si elle n'existe pas déjà. Le résultat est un objet « base de données » qui peut être utilisé (pour la plupart des opérations) à l'instar d'un dictionnaire.

Lorsqu'un nouvel élément est créé, le gestionnaire GDBM met à jour le fichier de la base de données :

 
Sélectionnez
1.
2.
julia> db["rabelais.png"] = "Portrait de François Rabelais."
"Portrait de François Rabelais."

Lorsque vous accédez à un des éléments, le gestionnaire GDBM lit le fichier :

 
Sélectionnez
1.
2.
julia> db["rabelais.png"]
"Portrait de François Rabelais."

Si vous effectuez une nouvelle affectation à une clé existante, le gestionnaire GDBM remplace l'ancienne valeur :

 
Sélectionnez
1.
2.
3.
4.
julia> db["rabelais.png"] = "Portrait de François Rabelais promenant le chat « Raminagrobis »."
"Portrait de François Rabelais promenant le chat « Raminagrobis »."
julia> db["rabelais.png"]
"Portrait de François Rabelais promenant le chat « Raminagrobis ». "

Certaines fonctions ayant un dictionnaire comme argument, comme key et value, sont inopérantes avec les objets de la base de données. Toutefois, l'itération avec une boucle for fonctionne :

 
Sélectionnez
1.
2.
3.
for (key, value) in db
    println(key, ": ", value)
end

Comme pour les autres fichiers, in fine, il est nécessaire de fermer la base de données :

 
Sélectionnez
1.
julia> close(db)

14-7. Sérialisation

Une des limites du gestionnaire GDBM provient du fait que les clés et les valeurs doivent être des chaînes de caractères ou des tableaux d'octets. Si un autre type est utilisé, Julia retourne une erreur.

Cependant, les fonctions serialize et deserialize contribuent à contourner cette limitation. Elles traduisent presque tout type d'objet en un tableau d'octets (IOBuffer(39)) adapté au stockage dans une base de données. Ensuite, elles retranscrivent les tableaux d'octets en objets :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
julia> using Serialization
julia> io = IOBuffer();

julia> t = [1, 2, 3];

julia> serialize(io, t)
24 
julia> print(take!(io)) 
UInt8[0x37, 0x4a, 0x4c, 0x07, 0x04, 0x00, 0x00, 0x00, 0x15, 0x00, 0x08, 0xe2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]

Le format n'est pas évident pour nous ; il est essentiellement censé être facile à interpréter pour Julia.

deserialize reconstitue l'objet :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
julia> io = IOBuffer();

julia> t1 = [1, 2, 3];

julia> serialize(io, t1) 
24 
julia> s = take!(io);

julia> t2 = deserialize(IOBuffer(s));

julia> print(t2) 
[1, 2, 3]

serialize et deserialize écrivent et lisent un objet IOBuffer qui représente un flux d'entrées/sorties en mémoire. La fonction take! récupère le contenu du tampon d'entrées/sorties sous la forme d'un tableau d'octets et remet le tampon dans son état initial.

Bien que le nouvel objet ait la même valeur que l'ancien, il ne s'agit pas du même objet (du moins en général) :

 
Sélectionnez
1.
2.
3.
4.
julia> t1 == t2
true
julia> t1 ≡ t2 
false

En d'autres termes, la sérialisation puis la désérialisation ont le même effet que la copie de l'objet.

Vous pouvez l'utiliser pour enregistrer des structures de données autres que des chaînes de caractères dans une base de données.

En fait, le stockage de structures de données autres que des chaînes de caractères dans une base de données est si courant qu'il a été encapsulé dans un paquet appelé JLD2 (voir la documentation de JLD2).

14-8. Objets de commande

La plupart des systèmes d'exploitation fournissent une interface en ligne de commande, également appelée « shell »(40). Les shells fournissent entre autres des commandes pour naviguer dans le système de fichiers et aussi, pour lancer des applications. Par exemple, les systèmes kitxmlcodeinlinelatexdvp{\rm\small UNIX}finkitxmlcodeinlinelatexdvp permettent de changer de répertoire avec cd, d'afficher le contenu d'un répertoire avec ls, et de lancer un navigateur web en saisissant une commande comme firefox (par exemple) dans une console.

Tout programme pouvant être lancé depuis le shell l'est également depuis Julia à l'aide d'un objet de commande :

 
Sélectionnez
1.
2.
julia> cmd = `echo Julia est un langage très puissant.`
`echo Julia est un langage très puissant.`

Les guillemets inversés sont utilisés pour délimiter la commande. La fonction run exécute cette dernière :

 
Sélectionnez
1.
2.
julia> run(cmd)
Julia est un langage très puissant.

La partie « Julia est un langage très puissant. » représente la sortie de la commande echo, envoyée à STDOUT(41). La fonction run elle-même retourne un objet processus et émet une ErrorException si la commande externe ne s'exécute pas correctement.

Si vous souhaitez lire la sortie de la commande externe, il convient d'utiliser read au lieu de run :

 
Sélectionnez
1.
2.
julia> a = read(cmd, String)
"Julia est un langage très puissant.\n"

La plupart des systèmes kitxmlcodeinlinelatexdvp{\rm\small UNIX}finkitxmlcodeinlinelatexdvp fournissent une commande appelée md5sum ou md5 qui lit le contenu d'un fichier et calcule une « somme de contrôle ». Vous pouvez en apprendre davantage sur la page MD5 de Wikipédia. Cette commande fournit un moyen efficace de vérifier si deux fichiers présentent le même contenu. La probabilité que des contenus différents donnent la même somme de contrôle est extrêmement faible.

Vous pouvez utiliser une commande pour lancer md5 à partir de Julia et obtenir le résultat :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
julia> filename = "output.txt" 
"output.txt"
julia> cmd = `md5 $filename`
` md5 output.txt` 
julia> res = read(cmd, String)
"MD5 (output.txt) = d41d8cd98f00b204e9800998ecf8427e\n"

14-9. Modules

Supposons que vous ayez un fichier nommé wc.jl contenant les 9 lignes de code suivant :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
function linecount(filename) 
    count = 0
    for line in eachline(filename) 
        count += 1 
    end 
    count 
end

print(linecount("wc.jl"))

À l'exécution, ce programme se lit lui-même et affiche le nombre de lignes du fichier, soit 9. Il est permis de l'inclure dans le REPL comme ceci :

 
Sélectionnez
1.
2.
julia> include("wc.jl")
9

Julia introduit des modules permettant de créer des espaces de travail variables distincts, c'est-à-dire avec de nouvelles portées générales.

Un module commence par le mot-clé module et se termine par end. Les conflits de noms sont évités entre vos propres définitions de haut niveau et celles trouvées dans le code de quelqu'un d'autre. import permet de contrôler quels noms d'autres modules sont visibles et export spécifie les noms de fichiers qui sont publics parmi les vôtres, c'est-à-dire ceux qui peuvent être utilisés en dehors du module sans être préfixés par le nom du module.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
module LineCount
    export linecount
    function linecount(filename)
        count = 0
        for line in eachline(filename)
            count += 1
        end
        count
    end
end

L'objet LineCount fournit la commande linecount :

 
Sélectionnez
1.
2.
3.
julia> using LineCount
julia> linecount("wc.jl") 
11

14-9-1. Exercice 14-1

Saisissez cet exemple dans un fichier nommé wc.jl, incluez-le dans le REPL et entrez using LineCount.

Si vous réimportez un module, Julia ne réagira pas et ne relira pas le fichier, même en cas de modification. Si vous voulez recharger un module, vous devez redémarrer le REPL. Il existe un paquet Revise qui permet de prolonger la durée des sessions (voir Revise.jl).

14-10. Débogage

Lorsque des fichiers sont utilisés en lecture et en écriture, des problèmes d'espacement sont susceptibles d'apparaître. Ces erreurs sont souvent difficiles à déboguer, car les espaces, les tabulations et les nouvelles lignes sont normalement invisibles :

 
Sélectionnez
1.
2.
3.
4.
5.
julia> s = "1 2\t 3\n 4";

julia> println(s)
1 2     3
 4

Les fonctions internes repr ou dump sont des auxiliaires pratiques. Elles prennent tout objet en argument et retournent une représentation de l'objet sous forme de chaîne.

 
Sélectionnez
1.
2.
3.
4.
julia> repr(s)
"\"1 2\t 3\n 4\""
julia> dump(s) 
String "1 2\t 3\n 4"

Cela peut être utile pour le débogage.

Un autre problème susceptible d'être rencontré tient au fait que divers systèmes utilisent différents caractères pour indiquer la fin d'une ligne. Certains utilisent une nouvelle ligne, représentée par \n, d'autres un caractère de retour, représenté \r, d'autres encore les deux. Si des fichiers sont échangés entre différents systèmes, des incohérences de ce type peuvent survenir.

Pour la plupart des systèmes, il existe des applications permettant la conversion d'un format à un autre. Vous pouvez les trouver (et en apprendre plus sur cette question) en consultant la page Wikipédia traitant la fin de ligne. Bien entendu, vous pouvez en écrire une vous-même.

14-11. Glossaire

persistant terme qui se rapporte à un programme qui fonctionne « indéfiniment » et qui conserve au moins une partie de ses données sur un support permanent.

fichier texte séquence de caractères enregistrée en permanence sur un disque dur (par exemple).

répertoire collection de fichiers nommés (parfois appelée « dossier »).

chemin d'accès chaîne de caractères qui identifie un fichier.

chemin relatif chemin d'accès qui part du répertoire courant.

chemin absolu chemin d'accès qui part du répertoire racine (ou root, symbolisé par / sur les systèmes kitxmlcodeinlinelatexdvp{\rm\small UNIX}finkitxmlcodeinlinelatexdvp, kitxmlcodeinlinelatexdvp{\rm\small GNU/LINUX}finkitxmlcodeinlinelatexdvp et BSD).

catch commande pour éviter qu'une exception ne mette fin à un programme en utilisant les déclarations try ... catch ... finally.

base de données fichier dont le contenu est organisé comme un dictionnaire avec des clés correspondant à des valeurs.

shell programme qui permet aux utilisateurs de saisir des commandes et de les exécuter en lançant d'autres programmes (comme bash, csh, sh).

objet de commande objet qui représente une commande shell, permettant à un programme Julia d'exécuter des commandes et d'en lire le résultat.

14-12. Exercices

14-12-1. Exercice 14-2

Écrivez une fonction appelée sed qui prend comme arguments une chaîne de caractères modèle, une chaîne de remplacement et deux noms de fichiers. Cette fonction doit lire le premier fichier et écrire le contenu dans le second, en le créant si nécessaire. Si la chaîne modèle apparaît quelque part dans le fichier, elle doit être substituée par la chaîne de remplacement.

Si une erreur se produit lors de l'ouverture, de la lecture, de l'écriture ou de la fermeture d'un fichier, votre programme doit capter l'exception, afficher un message d'erreur, puis quitter le flux de programmation.

14-12-2. Exercice 14-3

Si vous avez résolu l'exercice 12.10.2Exercice 12-3, vous savez comment créer un dictionnaire qui établit une correspondance entre une chaîne de lettres triée et la série de mots pouvant être orthographiés avec ces lettres. Par exemple, "spot" correspond au tableau ["post", "pots", "stop", "tops"].

Écrivez un module qui importe anagramsets et qui fournit deux nouvelles fonctions : (i) storeanagrams qui doit enregistrer le dictionnaire des anagrammes en utilisant JLD2, (ii) readanagrams qui doit rechercher un mot et retourner un tableau de ses anagrammes.

14-12-3. Exercice 14-4

Dans une grande collection de fichiers MP3, il peut y avoir plusieurs copies d'une même chanson stockées dans différents répertoires ou avec des noms de fichiers différents. L'objectif de cet exercice est de rechercher les doubles.

  • Écrivez un programme qui effectue une recherche dans un répertoire ainsi que tous ses sous-répertoires (donc, de manière récursive) et qui retourne un tableau de chemins complets pour tous les fichiers ayant un suffixe donné (comme .mp3).

  • Pour reconnaître les doubles, vous pouvez utiliser md5sum ou md5 pour calculer une « somme de contrôle » associée à chaque fichier. Si deux fichiers ont la même somme de contrôle, ils ont plus que probablement le même contenu.

  • Pour effectuer une double vérification, vous pouvez utiliser la commande kitxmlcodeinlinelatexdvp{\rm\small UNIX}finkitxmlcodeinlinelatexdvp diff.


précédentsommairesuivant
pwd — print working directory — est une commande des systèmes kitxmlcodeinlinelatexdvp{\rm\small UNIX}finkitxmlcodeinlinelatexdvp, GNU/Linux et BSD.
Ce répertoire est souvent noté $HOME.
database manager
mémoire tampon d'entrées/sorties
Un shell est l'interface entre un utilisateur et le noyau.
STandard OUTput

Licence Creative Commons
Le contenu de cet article est rédigé par Thierry Lepoint 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 © 2021 Developpez.com.