Outils pour utilisateurs

Outils du site


fichier_acces_direct

Création, exploitation et mise à jour de fichiers en accès direct

Objectif

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:

  • je veux pouvoir lire le 5000 ème enregistrement sans être obligé de lire les 4999 enregistrements précédents
  • je veux pouvoir modifier l'enregistrement 2792 et seulement celui-là
  • mon fichier ne tient pas en mémoire

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.

Création d'un nouveau fichier en accès direct

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.

Détermination et conservation de la longueur des enregistrements

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:

  • si elle est trop grande, on va perdre bêtement de la place sur le disque
  • si elle est trop courte, certaines valeurs ne pourront être enregistrées
  • elle sera peut-être trop courte ou trop longue pour les évolutions futures du fichier

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.

Lecture/écriture des données du fichier

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.

Ecriture initiale des données dans le fichier

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:

  • à l'ouverture, le curseur se positionne par défaut à l'octet 0. Et à chaque fois que vous enregistrez une chaine de lge octets par f.write(), ce curseur avance de lge octets pour se positionner au début de l'enregistrement suivant. Donc, vous n'avez pas besoin de la ligne: f.seek(i*lge). Mais si vous enregistrez une chaine qui n'a pas exactement la longueur lge, le déplacement du curseur deviendra faux pour tous les enregistrements suivants! Je vous conseille donc de la conserver.

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

Lecture de tout le fichier

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:

  • Quand on n'a pas noté quelque part le nombre d'enregistrements présents dans le fichier, on a une astuce pour le retrouver:
    • f.seek(0,2) positionne le curseur à la fin du fichier
    • fin = f.tell() renvoie cette position
    • ⇒ on sait donc jusqu'où on peut lire: on doit s'arrêter quand i*lge>=fin!
    • et on sait aussi qu'il y a déjà: fin//lge enregistrements dans le fichier
  • vous voyez l'utilisation de .rstrip() pour éliminer les octets de remplissage

Lecture/écriture de données particulières

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:

  • le f.flush() n'est nécessaire que si vous voulez lire juste après. Cela veut dire que vous pouvez écrire plusieurs valeurs sans le mettre.

Ajout de données au fichier

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.

Recherche rapide dans un fichier en accès direct trié

Tri indexé et recherche rapide dans un fichier en accès direct


Amusez-vous bien!

fichier_acces_direct.txt · Dernière modification: 2010/10/28 15:43 par tyrtamos

Outils de la page