Il est vrai qu'on utilise le plus souvent les fichiers texte, mais il peut arriver qu'on souhaite gérer sur disque des données avec les objectifs suivants:
On peut de plus faire des recherches très rapide (par dichotomie) dans un tel fichier, même très grand (je l'ai fait avec 10 millions de valeurs) si les données du fichier sont déjà triées, ou si on a créé avant, un fichier d'index qui permet de retrouver triées les données initialement non triées.
Cette page donne les bases pour faire cela!
Dans un premier temps, pour simplifier, on se limitera à l'enregistrement de chaines de caractères. Mais on peut enregistrer n'importe quoi dans un tel fichier, y compris, bien sûr, des données binaires.
Même si on utilisera par la suite le mode d'ouverture des fichiers 'rb+' (mode de “mise à jour” = lecture+écriture), on ne peut pas créer un nouveau fichier avec lui. Il faut utiliser le mode “wb” tout simplement. Pour simplifier le code, on ne gère pas ici le déclenchement d'exceptions dues aux erreurs (droit insuffisant pour créer le fichier par exemple).
# création du fichier fichier = "datas.fad" f = open(fichier, 'wb') f.close()
Bien entendu, l'extension du fichier (j'ai mis “.fad” pour “fichier en accès direct”) peut être n'importe quoi. Cependant, il ne serait pas prudent d'utiliser une extension connue comme “.txt” alors qu'on n'a pas l'intention de créer un fichier texte.
Un fichier en accès direct est un fichier dans lequel tous les enregistrements tiennent la même place. C'est ce qui permet de lire un enregistrement sur disque sans lire tous les précédents, parce qu'on peut facilement calculer l'emplacement exact où la lecture doit être faite. Encore faut-il définir cette place, et la conserver d'une session à l'autre.
On appellera ici cette place prise “lge” (=longueur enregistrement).
Quand on définit cette longueur, on cherche un compromis entre:
C'est assez facile pour l'enregistrement de chaines de caractères, à une exception près: attention aux encodages unicode qui “consomment” plus de place en octets qu'il n'y a de caractères imprimables! Il serait d'ailleurs sage qu'il y ait dans le code en même temps une vérification/alerte de dépassement et un tronquage pour éviter qu'une chaine trop longue ne recouvre le début de l'enregistrement suivant.
Pour conserver cette longueur d'une session à l'autre, quand il n'y a qu'un seul programme qui accède au fichier, on peut la conserver dans le programme en la codant “en dur” (exemple: lge = 25).
On peut aussi la stocker dans l'enregistrement “zéro” du fichier de données, ce qui permettra de la récupérer avant chaque mise à jour. Il faut, bien sûr, que la place soit suffisante: vous ne stockerez pas ainsi n=1000000 avec une longueur d'enregistrement de 5 octets. Cela nécessitera cependant que dans le code du programme, on n'oublie pas que la 1ère valeur des données se trouve dans l'enregistrement 1 et pas 0.
On peut enfin stocker la longueur de l'enregistrement dans un autre fichier de même nom mais d'extension différente (—.lge par exemple), ce qui nécessitera qu'on lise ce fichier avant chaque session de mise à jour. Ce fichier peut être un simple fichier texte. Dans ce cas, il n'est pas interdit d'en profiter pour noter dans ce même fichier d'autres infos utiles d'exploitation comme par exemple le nombre de données du fichier connu à la dernière session, les caractéristiques de la dernière session (date, login, …), l'existence et le nom d'un éventuel fichier index, etc…
Par la suite, on se contentera de l'inscription “en dur” dans le texte du programme.
Une fois qu'on a créé le fichier, et même s'il est vide, on va l'ouvrir en mode “mise à jour” pour faire ce qu'on veut avec: lecture/écriture de toutes les données ou seulement de certaines d'entre elles.
Le nouveau fichier est vide, et je veux y écrire toutes les valeurs (chaine de caractères) se trouvant dans une liste.
Prenons un exemple, je fabrique 10 valeurs numériques entières au hasard entre 1 et 9999:
import random L = [] for i in range(0,10): L.append("%s" % random.randint(1, 9999))
Voilà un exemple de code pour écrire tout le contenu de la liste L dans le fichier en accès direct “datas.bdd” avec une longueur d'enregistrement lge=5:
fichier = "datas.bdb" lge = 5 # ouverture du fichier en mode mise à jour 'r+b' f = open(fichier, 'r+b') # écriture de toutes les valeurs de la liste L for i in range(0, len(L)): f.seek(i*lge) f.write(L[i] + " "[0:lge-len(L[i])]) f.flush() # fermeture du fichier f.close()
Vous voyez comment on dit à quel endroit précis on veut que l'écriture se fasse: avec f.seek(i*lge). En fait, l'écriture se fait à l'emplacement d'un curseur (nombre entier) qui commence à zéro au tout début du fichier, et qui compte le nombre d'octets à partir de ce début. Si j'utilise les indices de la liste L, le 1er enregistrement (indice 0) se fait à 0, le second (indice 1) à 1*lge, le troisième (indice 2) à 2*lge, … et le (i+1)ème enregistrement (indice i) se fera à i*lge.
Plusieurs remarques concernant ce code:
* vous voyez que j'écris non pas simplement la chaine L[i], mais cette chaine complétée par des espaces pour atteindre exactement la longueur lge. Pourquoi? C'est très simple: si j'écris une chaine 'xxx' dans un enregistrement de longueur lge=5, cet enregistrement contiendra des valeurs non désirées dans les 2 derniers octets (octets de remplissage). Si j'y mets des espaces, je pourrais retrouver la donnée initiale exacte avec un simple —.rstrip()!
* Le f.flush vide le tampon d'écriture. Ceci parce que, pour des raisons de performances, l'écriture physique sur le disque ne se fait pas au moment de l'ordre d'écriture f.write(), mais après (=écriture différée). Vous voyez déjà que je n'ai pas besoin de vider le tampon à chaque écriture! Mais même à la fin de toutes les écritures, ce f.flush() n'est pas nécessaire ici puisque f.close() le fait aussi. En fait, dans le mode “mise à jour” utilisé ici ('r+b'), vous pouvez lire des données après en avoir écrites, sans fermer le fichier entre les 2. Et dans ce cas, vous devez mettre le f.flush avant les lectures.
On n'a pas besoin de fermer et rouvrir le fichier pour lire, quand il était ouvert précédemment en mode mise à jour ('r+b'). On va suppose ici que l'ouverture a été faite.
f.seek(0,2) fin = f.tell() i = 0 while i*lge<fin: f.seek(i*lge) x = f.read(lge).rstrip() print x i += 1 print "fin de fichier"
Remarques sur ce code:
On veut lire, par exemple, le 6ème enregistrement (donc d'indice 5 puisque le 1er enregistrement est d'indice 0!), et le remplacer par une autre valeur.
Lecture de l'enregistrement d'indice 5:
i = 5 f.seek(i*lge) x = f.read(lge).rstrip() print x
Modification de cette valeur pour la remplacer par “0000”:
i = 5 f.seek(i*lge) x = "0000" f.write(x + " "[0:lge-len(x)]) f.flush()
Remarque sur ce code:
Ajouter une nouvelle donnée au fichier est très simple: il suffit d'écrire à la fin du fichier, et on sait déjà comment se positionner à la fin du fichier: par f.seek(0,2).
Par exemple, on ajoute '9999' comme enregistrement supplémentaire
f.seek(0,2) x = '9999' f.write(x + " "[0:lge-len(x)]) f.flush
Dans notre exemple, il y a donc désormais 11 enregistrements dans le fichier.
Amusez-vous bien!