Sérialisation avec JSON, XML, pickle

La sérialisation répond à un problème simple : comment échanger des données complexes autres que des tableaux ?

Si l’énoncé est simple, la solution ne l’est pas toujours. Il est assez facile d’échanger des données qui se présentent sous la forme d’un tableau, d’un texte, d’un nombre mais comment échanger un assemblage de données hétérogènes ? La sérialisation désigne un méanisme qui permet de permet de représenter un assemblage de données en un seul tableau de caractères. La désérialisation désigne le mécanisme inverse qui consiste à reconstruire les données initiales à partir de ce tableau de caractères.

Notion de stream ou flux

Un stream en informatique définit une façon de parcourir une séquence d’octets. Un fichier est un stream : on écrit les octets ou caractères les uns après les autres, chaque nouveau caractère est ajouté à la fin. Lors de la lecture, on procède de même en lisant les caractères du début à la fin. Dans un stream, on ne revient jamais en arrière, on lit toujours le caractère suivant.

Les streams sont optimisés pour ce type de lecture et d’écriture, ils sont très lents lorsqu’il s’agit d’aller lire ou écrire des caractères de façon non séquentielle.

Pour faire des calculs mathématiques, il faut pouvoir accéder à tout moment à n’importe quel élément de la matrice. L’utilisation d’un stream est contre-indiquée. En revanche, ils sont très adaptés à la lecture et l’écriture de fichiers. Ils sont également utilisés pour communiquer des données, lorsqu’un ordinateur envoie des données à un autre ordinateur.

[1]:
import math
from io import StringIO

st = StringIO()
st.write("pi=")
st.write(str(math.pi))
st.write(";")
value = st.getvalue()
print(value)
pi=3.141592653589793;
[2]:
st = StringIO("pi=3.141592653589793;")
while text := st.read(1):
    print(text)
p
i
=
3
.
1
4
1
5
9
2
6
5
3
5
8
9
7
9
3
;
[3]:
def f1(text):
    st = StringIO()
    for t in text:
        st.write(t)
        st.write(";")
    value = st.getvalue()
    return value


def f2(text):
    s = ""
    for t in text:
        s += t + ";"
    return s


data = ["petit", "essai", "de", "comparaison"] * 300
f1(data)[:100], f2(data)[:100]
[3]:
('petit;essai;de;comparaison;petit;essai;de;comparaison;petit;essai;de;comparaison;petit;essai;de;comp',
 'petit;essai;de;comparaison;petit;essai;de;comparaison;petit;essai;de;comparaison;petit;essai;de;comp')
[4]:
%timeit f1(data)
314 µs ± 73.2 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
[5]:
%timeit f2(data)
320 µs ± 66 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

Q1: quelle est la fonction la plus rapide ?

Il vaut mieux faire varier la longueur de la liste data avant de répondre.

Format JSON

Le format JSON est le format le plus répandu sur Internet. C’est un assemblage récursif de listes et de dictionnaires. Chaque conteneur peut contenir des listes, des dictionnaires, des nombres, des chaînes de caractères.

Il est possible de télécharger tout Wikipédia au format JSON : Wikidata:Téléchargement de la base de données.

[6]:
data = {
    "nom": "magoo",
    "naissance": 1949,
    "creator": ["Millard Kaufman", "John Hubley"],
    "cartoons": [
        {"title": "Les Aventures célèbres de Monsieur Magoo", "durée": 5},
        {"title": "Quoi de neuf Mr. Magoo ?", "durée": 10},
    ],
}
data
[6]:
{'nom': 'magoo',
 'naissance': 1949,
 'creator': ['Millard Kaufman', 'John Hubley'],
 'cartoons': [{'title': 'Les Aventures célèbres de Monsieur Magoo',
   'durée': 5},
  {'title': 'Quoi de neuf Mr. Magoo ?', 'durée': 10}]}

Le langage python propose une librairie standard json pour manipuler les informations. Et comme c’est d’un usage fréquent, il existe d’autres options plus rapides ujson, simplejson, ijson, …

La page Index of /wikidatawiki/entities/ contient des fichiers json issues de wikipedia. Le fichier latest-lexemes-sample.json contient les premières lignes de latest-lexemes.json.bz2.

Q1. lire du json

Télécharger et lire ce fichier avec la libraire json.

[7]:
from urllib.request import urlopen

url = "https://github.com/sdpython/teachpyx/raw/main/_data/latest-lexemes-sample.json"
with urlopen(url) as f:
    text = f.read().decode("utf-8")
print(text[:150] + "...")
[
{"type":"lexeme","id":"L4","lemmas":{"en":{"language":"en","value":"windsurf"}},"lexicalCategory":"Q24905","language":"Q1860","claims":{"P5238":[{"m...
[ ]:

Q2: écrire du json

Modifier les données et les écrire de nouveau sur disque.

[ ]:

Q3: gros json

Le dump de la version anglaise de wikipedia fait plus de 100 Go (en version compressée). Il tient sur disque mais pas en mémoire. Comment faire pour le lire malgré tout ? Quelques lignes pour vous données des idées… Les plus courageux utiliseront la librairie ijson ou orjson.

[8]:
with open("dummy.json", "w", encoding="utf-8") as f:
    f.write(text)

with open("dummy.json", "r", encoding="utf-8") as f:
    for i, line in enumerate(f):
        if i > 2:
            break
        print(line)
[

{"type":"lexeme","id":"L4","lemmas":{"en":{"language":"en","value":"windsurf"}},"lexicalCategory":"Q24905","language":"Q1860","claims":{"P5238":[{"mainsnak":{"snaktype":"value","property":"P5238","datavalue":{"value":{"entity-type":"lexeme","numeric-id":3324,"id":"L3324"},"type":"wikibase-entityid"},"datatype":"wikibase-lexeme"},"type":"statement","qualifiers":{"P1545":[{"snaktype":"value","property":"P1545","hash":"2a1ced1dca90648ea7e306acbadd74fc81a10722","datavalue":{"value":"1","type":"string"},"datatype":"string"}]},"qualifiers-order":["P1545"],"id":"L4$faad30b0-421c-803a-c1fd-b9a99a0eb35d","rank":"normal"},{"mainsnak":{"snaktype":"value","property":"P5238","datavalue":{"value":{"entity-type":"lexeme","numeric-id":18537,"id":"L18537"},"type":"wikibase-entityid"},"datatype":"wikibase-lexeme"},"type":"statement","qualifiers":{"P1545":[{"snaktype":"value","property":"P1545","hash":"7241753c62a310cf84895620ea82250dcea65835","datavalue":{"value":"2","type":"string"},"datatype":"string"}]},"qualifiers-order":["P1545"],"id":"L4$d15285a1-4880-7a9b-bb1f-85403e1a785a","rank":"normal"}],"P5187":[{"mainsnak":{"snaktype":"value","property":"P5187","datavalue":{"value":{"text":"windsurf","language":"en"},"type":"monolingualtext"},"datatype":"monolingualtext"},"type":"statement","id":"L4$d4a63d17-43ea-749d-5860-21b90feb83f7","rank":"normal"}]},"forms":[{"id":"L4-F1","representations":{"en":{"language":"en","value":"windsurfing"}},"grammaticalFeatures":["Q10345583"],"claims":[]},{"id":"L4-F3","representations":{"en":{"language":"en","value":"windsurfs"}},"grammaticalFeatures":["Q110786","Q3910936","Q51929074"],"claims":[]},{"id":"L4-F4","representations":{"en":{"language":"en","value":"windsurfed"}},"grammaticalFeatures":["Q1392475"],"claims":[]},{"id":"L4-F5","representations":{"en":{"language":"en","value":"windsurfed"}},"grammaticalFeatures":["Q1230649"],"claims":[]},{"id":"L4-F6","representations":{"en":{"language":"en","value":"windsurf"}},"grammaticalFeatures":["Q3910936"],"claims":[]}],"senses":[{"id":"L4-S1","glosses":{"fr":{"language":"fr","value":"faire de la planche \u00e0 voile"},"ms":{"language":"ms","value":"meluncur angin"},"zh":{"language":"zh","value":"\u6ed1\u6d6a\u98a8\u5e06"},"zh-hant":{"language":"zh-hant","value":"\u6ed1\u6d6a\u98a8\u5e06"},"zh-tw":{"language":"zh-tw","value":"\u6ed1\u6d6a\u98a8\u5e06"},"nan":{"language":"nan","value":"h\u00e1i-\u00edng hong-ph\u00e2ng"},"th":{"language":"th","value":"\u0e40\u0e25\u0e48\u0e19\u0e27\u0e34\u0e19\u0e14\u0e4c\u0e40\u0e0b\u0e34\u0e23\u0e4c\u0e1f"},"tg":{"language":"tg","value":"\u0441\u0451\u0440\u0444\u0438\u043d\u0433\u0431\u043e\u0437\u0438\u0438 \u0448\u0430\u043c\u043e\u043b\u04e3"},"fi":{"language":"fi","value":"purjelautailla"}},"claims":{"P5137":[{"mainsnak":{"snaktype":"value","property":"P5137","datavalue":{"value":{"entity-type":"item","numeric-id":191051,"id":"Q191051"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"L4-S1$13e5f498-4deb-ea41-4d60-02c852b88b4c","rank":"normal"}],"P5972":[{"mainsnak":{"snaktype":"value","property":"P5972","datavalue":{"value":{"entity-type":"sense","id":"L144039-S1"},"type":"wikibase-entityid"},"datatype":"wikibase-sense"},"type":"statement","id":"L4-S1$7218013F-B84B-40FA-B57B-BC1BA2239BB8","rank":"normal"}]}}],"pageid":54387040,"ns":146,"title":"Lexeme:L4","lastrevid":1710596079,"modified":"2022-08-22T19:28:34Z"},

{"type":"lexeme","id":"L314","lemmas":{"ca":{"language":"ca","value":"pi"}},"lexicalCategory":"Q1084","language":"Q7026","claims":{"P5185":[{"mainsnak":{"snaktype":"value","property":"P5185","datavalue":{"value":{"entity-type":"item","numeric-id":1775415,"id":"Q1775415"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"L314$45650151-4ed8-025d-2442-e36ef22e6a2a","rank":"normal"}]},"forms":[{"id":"L314-F1","representations":{"ca":{"language":"ca","value":"pis"}},"grammaticalFeatures":["Q146786"],"claims":[]},{"id":"L314-F2","representations":{"ca":{"language":"ca","value":"pi"}},"grammaticalFeatures":["Q110786"],"claims":[]}],"senses":{},"pageid":54387050,"ns":146,"title":"Lexeme:L314","lastrevid":684359491,"modified":"2018-05-24T07:28:21Z"},

XML

Le XML était utilisé avant le format json. Il permet de faire la même chose, sérialiser, mais est plus verbeux. Il a été abandonné car le résultat est plus long qu’avec le format json.

[10]:
from dict2xml import dict2xml

print(dict2xml(data))
<cartoons>
  <durée>5</durée>
  <title>Les Aventures célèbres de Monsieur Magoo</title>
</cartoons>
<cartoons>
  <durée>10</durée>
  <title>Quoi de neuf Mr. Magoo ?</title>
</cartoons>
<creator>Millard Kaufman</creator>
<creator>John Hubley</creator>
<naissance>1949</naissance>
<nom>magoo</nom>

pickle

Le format JSON a un inconvénient majeur : il impose la conversion des données au format texte, en particulier les nombres. Chaque nombre doit être converti en chaînes de caractères et réciproquement. Pourquoi ne pas garder la représentation binaire des nombres tels qu’ils sont utilisés en mémoire ?

C’est l’objectif du module pickle. Comme il n’y pas de conversion au format texte et qu’il s’agit de recopier la mémoire sur disque en un seule, cette sérialisation s’applique à tout objet python. Elle n’est pas restreinte aux dictionnaires et aux listes.

Q1: comparer le temps de sérialisation entre pickle et json

On pourra utiliser les données json récupérées ci-dessus.

[ ]:

Q2: comparer le temps de désérialisation entre pickle et json

Même exercice en sens inverse.

[ ]:

Peut-on tout sérialiser ?

La plupart des objets contenant des données peuvent être sérialisées, les listes, ldes dictionnaires, les matrices (numpy), les dataframes)… Il n’est pas possible de sérialiser les fonctions à moins d’utiliser des librairies comme cloudpickle ou dill.

La sérialisation fonctionne de façon implicite avec toutes les classes python à l’exception de celles définies en C++. Pour celles-ci, il faudra coder explicitement la sérialisation et la désérialisation. Pour cela il faut redéfinir les méthodes getstate et_setstate.

Il reste une contrainte majeure à cette sérialisation, elle dépend de la version du langage et de chaque extension. Sérialisation avec python 3.7 et désérialisation avec python 3.10 a peu de chance de fonctionner.

[ ]:


Notebook on github