Dans le cadre de ce cours, nous aborderons :
Les principaux formats d'échange de données sont :
Exemples :
La conservation des données repose principalement sur la structure de stockage,
Un jeu de valeurs encodé et stocké sur un support informatique est appelé un “enregistrement”,
Encodage binaire :
Encodage textuel :
Un bloc de données correspond à une série d’enregistrements obéissant au même format:
La structure de base servant à stocker à une série d’enregistrements est le tableau de données à 2 dimensions :
Structure sous-jacente : tableau à 2 dimensions (ou matrice de données)
Un bloc de données obéit formellement à une structure de type tableau statique:
T
, la position des cases reste fixe au cours de l'utilisation :T[i]
désigne toujours la mếme zone mémoireT[i]
ne désigne pas toujours la mếme zone mémoireLes mémoires informatiques sont des structures statiques. Une fois les données sauvegardées sur le disque, il est difficile d’insérer ou de supprimer des éléments.
La difficulté consiste à définir une organisation logique du tableau permettant de gérer efficacement un tel ensemble (qui peut être de grande taille) :
On parle de structure de stockage. Une telle structure doit permettre :
On considère un ensemble de k
données stockés dans un tableau de données statique T
de N
cases (avec 0
≤k
≤ N
).
Remarques :
0
à N-1
i
est un indice de case, T[i]
désigne le contenu de la caseN
est fixé mais k
varie en fonction du nombre de données stockées
Propriété : les données sont stockés dans les k
premières cases du tableau. Ainsi, les cases de 0
à k-1
sont occupées et l'indice k
désigne la première case libre.
Opérations fondamentales :
On conserve une table des cases libres
B=0010010100100.....01n−1B=0010010100100.....01n−1
Stratégies d’allocation
Stratégie par bloc: on alloue des blocs composés de plusieurs cases (S’il existe des blocs libres consécutifs, ils sont fusionnés en un seul)
On souhaite :
Écrire un algorithme permettant d'insérer une donnée dd de taille mm dans le premier bloc libre disponible (pensez à mettre à jour la table d'allocation BB).
Autre stratégie (allocation rapide): On alloue des blocs de 1,2,4,8,…2K pages. Pour une taille donnée 2i-1 < n < 2i, on commence par chercher les blocs de taille 2i, puis 2i+1, … jusqu’à 2K , en divisant ces blocs le cas échéant.
Problème des stratégies par bloc: s’il y a trop de données, on obtient des blocs de taille 1 –> nécessité de réorganiser (défragmenter)
La mémoire secondaire n’est pas directement accessible (les programmes n’ont pas la possibilité d’aller écrire directement sur le disque). Le système d’exploitation assure ainsi l’indépendance du programme par rapport à l’emplacement des données, au travers d’instructions d’entrée/sortie spécialisées.
Pour que les programmes puissent écrire sur le disque, on introduit des objets intermédiaires : les fichiers. Un fichier est caractérisé par (nom, emplacement (volume, arborescence), droit d’accès, taille,…). Il s’agit d’une entité logique. Tout programme utilisant un fichier passe par le système d’exploitation qui, à partir des informations, détermine la localisation des données sur le support.
Le volume est le support sur lequel sont enregistrées les données. On parle de mémoire secondaire (Disque dur, disquette,CD-ROM, etc…). Un volume est divisé en pistes concentriques numérotées de 0 à n ( par ex n = 1024). Chaque piste supporte plusieurs enregistrements physiques appelés secteurs, de taille constante (1 secteur = 1 page).
Page (ou secteur)
Les pages sont les unités de base pour la lecture et l'écriture. une page est une zone contiguë de la mémoire secondaire qui peut être chargée en mémoire centrale en une opération de lecture. Taille standard : une page = 1-2 ko.
La mémoire secondaire est donc organisée comme un tableau de pages : (T[0],…,T[L-1]), où L est le nombre de pages. Chaque page fait m octets. Chaque page peut être libre ou occupée.
Répertoires Chaque volume possède un numéro appelé le label du volume. Tous les descripteurs de fichiers sont regroupés dans une table des matières appelée Répertoire (Directory).
Remarque : cette table des matières est en fait un fichier dont le descripteur est contenu dans le label du volume.
Organisation hiérarchique :
Méthode traditionnelle pour le traitement de fichiers de petite taille. La consultation des données nécessite d’ouvrir une “communication” entre le programme et les fichiers. Ce canal de communication permet de recevoir un flux de données.
Pour établir la communication, il faut connaître : le “chemin d’accès” aux données (disque dur local) l’”adresse” des données (lorsqu’il s’agit de données stockées sur un serveur distant)
L’opération d’ouverture de fichier initialise un descripteur de fichier, qui sert à désigner (dans le programme) le fichier sur lequel on travaille, et d’accéder au contenu du flux.
Ouverture simple:
f = open('/chemin/vers/mon/fichier.dat','r')
Le deuxième argument représente le mode d’ouverture, ici ’r’ représente une ouverture en mode lecture.
Il est important de vérifier que cette opération d’ouverture s’effectue correctement avant de poursuivre le programme (nombreuses possibilités d’erreur : fichier effacé, erreur de nom, pas de droits de lecture,…).
On utilise une instruction de test spécifique pour vérifier que l’ouverture du fichier s’est correctement effectuée, de type try
…catch
… (essaye … sinon …) permettant de prévoir une action de secours lorsqu’une une opération “risquée” échoue.
try : f = open('/chemin/vers/mon/fichier.dat','r') except IOError: print "Erreur d'ouverture!"
Lorsque l’opération d’ouverture est réalisée avec succès, le flux de données devient accessible en lecture (les premières lignes du fichier sont chargées en mémoire et une tête de lecture se positionne sur le premier caractère de la première ligne). Il ne reste plus qu’à lire les données.
La consultation des données s’effectue séquentiellement à l’aide de l’opérateur de lecture readline
. Chaque appel à cet opérateur charge les nouvelles données et déplace la tête de lecture vers les données suivantes. Cette opération peut être effectuée plusieurs fois jusqu’à atteindre la fin de fichier.
Remarque : Lecture/Ecriture
Les opérateurs lire_ligne (readline) et écrire_ligne (write) ne travaillent pas directement sur les données du fichier. Les données du fichier sont chargées en mémoire centrale dans une mémoire “tampon”. L’objet f servant à décrire le fichier a comme attributs :
Lors des opération de lecture, la mémoire tampon est mise à jour au fur et à mesure qu’on avance dans la lecture du fichier par des opérations de lecture sur le disque. En général, plusieurs pages sont chargées en avance. Lors d’une opération d’écriture, la mémoire tampon reçoit les nouvelles données à écrire. Ces données sont effectivement écrites sur le disque lorsque le tampon est suffisamment rempli ou lors de l’opération de fermeture. Au moment de l’écriture effective, le système d’exploitation fait appel à un opérateur d’allocation pour choisir le “meilleur” bloc où stocker les données.
Si on suppose que les données sont rangées sous la forme d’un tableau de lignes, chaque opération de lecture consiste à consulter une ligne du tableau, et à positionner la tête de lecture sur la ligne suivante.
1. Lecture d’une ligne unique :
Python
s = f.readline()
2. Lecture de toutes les lignes (la lecture s’effectue dans une boucle) + affichage de la ligne:
Python
for s in f : print s
L’opération de sauvegarde des données est l’opération complémentaire de la lecture. De nouvelles données doivent être enregistrées sur le disque dur en vue d’une consultation future. Le format de sauvegarde peut être de type texte ou de type binaire. Nous présentons ici la sauvegarde des données dans des formats texte.
Comme dans le cas de l’opération de lecture, il faut au préalable définir dans le programme un descripteur de fichier servant de point d’entrée pour les opération d’écriture. On effectue ce qu’on appelle une ouverture en mode “écriture”.
Python
try : f = open('monfichier.dat','w') except IOError: print "Erreur d'ouverture!!"
On notera qu’il existe en python (ainsi qu’en C, C++, …) plusieurs modes d’ouverture exprimés par le deuxième argument de la fonction open. On retiendra le mode ‘w’ (création d’un nouveau fichier vide) et le mode ‘a’ (ajout de nouvelles données à la suite d’un fichier déjà existant).
La sauvegarde dans le fichier s’effectue à l’aide d’un opérateur d’écriture. Dans le cas des chaînes de caractères, l’opérateur d’écriture sauvegarde ces données à la suite des données déjà écrites.
Python
f.write("Bonjour!\n")
La sauvegarde de données nécessite d’effectuer un choix sur le mode d’encodage, obéissant en général à une norme bien précise (csv, json, xml, etc…). Voir section 1.3.
Une fois les opérations de lecture ou d’écriture terminées, il est nécessaire de fermer le fichier. L’opération de fermeture assure que les données sont effectivement enregistrées sur le disque (et non simplement stockées dans la mémoire tampon - voir section XXX).
Voir aussi :
Une donnée informatique est un élément d'information ayant subi un encodage numérique
Pour une gestion efficace des données, il est nécessaire de pouvoir identifier chaque enregistrement de façon unique.
L'indexation des données repose sur un principe simple d'étiquetage consistant à attribuer une étiquette différente à chaque enregistrement.
On peut représenter l'opération d'indexation sous la forme d'une fonction. Si dd est le jeu de valeurs, k(d)k(d) désigne l'identifiant de ce jeu de valeurs.
L'indexation suppose l'existence d'une bijection entre l'ensemble des données et l'ensemble des clés, permettant d'assurer l'unité et la spécificité de la clé
L'existence d'un identifiant unique pour chaque jeu de valeurs dd permet la mise en œuvre d'une recherche par identifiant (ou recherche par clé).
La recherche par identifiant repose sur une fonction d'adressage II qui à tout identifiant kk associe sa position (entrée) ii dans un tableau de données: I:k→iI:k→i. Ainsi si kk est l'identifiant servant à la recherche, l'extraction des informations se fait en 2 étapes:
L'identifiant doit en pratique tenir sur un nombre d'octets le plus petit possible pour que la liste LL puisse être manipulée en mémoire centrale. Autrement dit, il faut que :
pour que :
Définir un ordre sur les données
La présence d'un identifiant permet de définir un ordre total sur les données :
Lier les données
Dans le cas des bases de données, l'identifiant constitue une référence vers les jeux de valeurs des tableaux de données. Une référence permet de lier les données d'une table aux données d'une autre table.
Exemple :
Structure d'ensemble
L'index définit l'appartenance d'une donnée à un ensemble.
Soit EE un ensemble de données indexées : E={d1,d2,...,dK}E={d1,d2,...,dK} On a l'équivalence : d∈E⇔k(d)∈Id∈E⇔k(d)∈I
Ainsi, il ne peut y avoir de doublon car ∀d∀d :
L'exemple le plus simple d'indexation est celui fourni par les numéros de case d'un tableau.
Dans le cas général, l'identifiant n'est pas égal à l'entrée!
On sépare donc l'index kk de l'entrée ii:
Il existe différentes méthodes permettant d'assurer l'intégrité de l'index:
Utilisation d'une fonction de hachage :
Attribution d'un identifiant arbitraire entre 0 et n-1
i = int.from_bytes(d, byteorder='big')
s = 'paul.poitevin@centrale-marseille.fr' d = bytes(s, 'ascii') i = int.from_bytes(d, byteorder='big') print("i =", i)
donne :
i = 852806586114976881510056778471769180697471859150728801241505293627173168229624211058
k = H(i) = i mod n
j = i + 1
, H(j) = H(i) + 1
(presque toujours)paul.poitevin@centrale-marseille.fr
) = 1697539698
martin.mollo@centrale-marseille.fr
) = 1697539698
paul.poitevin@centrale-marseille.fr
) = 36148249469507
martin.mollo@centrale-marseille.fr
) = 65330032132071
k = H(i) = (i * m) mod n
j = i + 1
, j * m - i * m = m
i * m
peut etre coûteux à calculerj != i
tel que H(i) = H(j)
. Le gestionnaire de bases de données MongoDB utilise une indexation des données par clé cryptographique.
Que faire lorsque l'index ne tient pas en totalité dans la mémoire centrale ?
Considérons un index composé de couples (clé, numero de page). On suppose que l’index est également découpé en pages, chaque page d'index contenant au maximum b clés. Si l’index comporte beaucoup de pages, il est intéressant de l’organiser hiérarchiquement en pages et sous-pages, selon une organisation appelée "B-arbre" (Balanced Tree):
<!--
<note> **algo : lecture de l'Index**
* paramètre d’entrée : k
I ← charger la racine de l’arbre
tant que I n’est pas une feuille :
k’ ← première clé de I
tant que k > k’
k’ ← clé suivante
I ← charger sous-page de k’
</note>
-->
Remarque : Pour que l’accès aux données soit efficace,
<!--
* Dans ce cas,
* chaque noeud a ''b'' fils,
* et la profondeur de l’arbre est de l’ordre de logb(N).
* Pour charger la page contenant le tuple cherché,
* il faut donc logb(N) + 1 lectures sur disque.
*
En pratique, il existe des algos permettant d’assurer que chaque noeud contient entre b/2 et b clés (sauf la racine).
-->