web-dev-qa-db-fra.com

Clé: magasin de valeurs dans Python pour éventuellement 100 Go de données, sans client / serveur

Il existe de nombreuses solutions pour sérialiser un petit dictionnaire: json.loads/json.dumps, pickle, shelve, ujson, ou même en utilisant sqlite.

Mais lorsqu'il s'agit éventuellement de 100 Go de données, il n'est plus possible d'utiliser de tels modules qui pourraient éventuellement réécrire l'ensemble des données lors de la fermeture/sérialisation.

redis n'est pas vraiment une option car il utilise un schéma client/serveur.

Question: Quelle clé: magasin de valeurs, sans serveur, capable de travailler avec plus de 100 Go de données, sont fréquemment utilisés en Python?

Je recherche une solution avec un standard "Pythonic" d[key] = value syntaxe:

import mydb
d = mydb.mydb('myfile.db')
d['hello'] = 17          # able to use string or int or float as key
d[183] = [12, 14, 24]    # able to store lists as values (will probably internally jsonify it?)
d.flush()                # easy to flush on disk 

Remarque: BsdDB (BerkeleyDB) semble obsolète. Il semble y avoir un LevelDB pour Python , mais il ne semble pas bien connu - et je je n'ai pas trouvé une version prête à l'emploi sur Windows. Lesquels seraient les plus courants?

9
Basj

Vous pouvez utiliser sqlitedict qui fournit une interface de valeur-clé à la base de données SQLite.

SQLite page limites indique que le maximum théorique est de 140 TB selon page_size et max_page_count. Cependant, les valeurs par défaut pour Python 3.5.2-2ubuntu0 ~ 16.04.4 (sqlite3 2.6.0), sont page_size=1024 et max_page_count=1073741823. Cela donne ~ 1100 Go de taille de base de données maximale qui correspond à vos besoins.

Vous pouvez utiliser le package comme:

from sqlitedict import SqliteDict

mydict = SqliteDict('./my_db.sqlite', autocommit=True)
mydict['some_key'] = any_picklable_object
print(mydict['some_key'])
for key, value in mydict.items():
    print(key, value)
print(len(mydict))
mydict.close()

Mettre à jour

À propos de l'utilisation de la mémoire. SQLite n'a pas besoin de votre jeu de données pour tenir dans la RAM. Par défaut, il met en cache jusqu'à cache_size pages, qui fait à peine 2 Mo (le même Python comme ci-dessus). Voici le script que vous pouvez utiliser pour le vérifier avec vos données. Avant de lancer:

pip install lipsum psutil matplotlib psrecord sqlitedict

sqlitedct.py

#!/usr/bin/env python3

import os
import random
from contextlib import closing

import lipsum
from sqlitedict import SqliteDict

def main():
    with closing(SqliteDict('./my_db.sqlite', autocommit=True)) as d:
        for _ in range(100000):
            v = lipsum.generate_paragraphs(2)[0:random.randint(200, 1000)]
            d[os.urandom(10)] = v

if __name__ == '__main__':
    main()

Exécutez-le comme ./sqlitedct.py & psrecord --plot=plot.png --interval=0.1 $!. Dans mon cas, il produit ce graphique: chart

Et fichier de base de données:

$ du -h my_db.sqlite 
84M my_db.sqlite
14
saaj

Je considérerais HDF5 pour cela. Il présente plusieurs avantages:

  • Utilisable à partir de nombreux langages de programmation.
  • Utilisable depuis Python via l'excellent package h5py .
  • Testé en combat, y compris avec de grands ensembles de données.
  • Prend en charge les valeurs de chaîne de longueur variable.
  • Les valeurs sont adressables par un "chemin" de type système de fichiers (/foo/bar).
  • Les valeurs peuvent être des tableaux (et le sont généralement), mais ne doivent pas l'être.
  • Compression intégrée en option.
  • "Blocage" facultatif pour permettre l'écriture incrémentielle de morceaux.
  • Ne nécessite pas le chargement de l'ensemble des données en mémoire à la fois.

Il présente également certains inconvénients:

  • Extrêmement flexible, au point de rendre difficile la définition d'une approche unique.
  • Format complexe, impossible à utiliser sans la bibliothèque officielle HDF5 C (mais il existe de nombreux wrappers, par exemple h5py).
  • API baroque C/C++ (la Python on ne l'est pas).
  • Peu de support pour les écrivains simultanés (ou écrivain + lecteurs). Les écritures peuvent avoir besoin de se verrouiller à une granularité grossière.

Vous pouvez considérer HDF5 comme un moyen de stocker des valeurs (scalaires ou tableaux à N dimensions) dans une hiérarchie à l'intérieur d'un seul fichier (ou même de plusieurs de ces fichiers). Le plus gros problème avec simplement le stockage de vos valeurs dans un seul fichier disque serait que vous submergez certains systèmes de fichiers; vous pouvez considérer HDF5 comme un système de fichiers dans un fichier qui ne tombera pas lorsque vous mettez un million de valeurs dans un "répertoire".

4
John Zwinck

Tout d'abord, bsddb (ou sous son nouveau nom Oracle BerkeleyDB) n'est pas obsolète.

Par expérience, LevelDB/RocksDB/bsddb sont plus lents que wiredtiger , c'est pourquoi je recommande wiredtiger.

wiredtiger est le moteur de stockage de mongodb, il est donc bien testé en production. Il y a peu ou pas d'utilisation de wiredtiger dans Python en dehors de mon projet AjguDB; j'utilise wiredtiger (via AjguDB) pour stocker et interroger les wikidata et le concept d'environ 80 Go.

Voici un exemple de classe qui permet d'imiter le module python2 shelve . Fondamentalement, c'est un dictionnaire backend wiredtiger où les clés ne peuvent être que des chaînes:

import json

from wiredtiger import wiredtiger_open


WT_NOT_FOUND = -31803


class WTDict:
    """Create a wiredtiger backed dictionary"""

    def __init__(self, path, config='create'):
        self._cnx = wiredtiger_open(path, config)
        self._session = self._cnx.open_session()
        # define key value table
        self._session.create('table:keyvalue', 'key_format=S,value_format=S')
        self._keyvalue = self._session.open_cursor('table:keyvalue')

    def __enter__(self):
        return self

    def close(self):
        self._cnx.close()

    def __exit__(self, *args, **kwargs):
        self.close()

    def _loads(self, value):
        return json.loads(value)

    def _dumps(self, value):
        return json.dumps(value)

    def __getitem__(self, key):
        self._session.begin_transaction()
        self._keyvalue.set_key(key)
        if self._keyvalue.search() == WT_NOT_FOUND:
            raise KeyError()
        out = self._loads(self._keyvalue.get_value())
        self._session.commit_transaction()
        return out

    def __setitem__(self, key, value):
        self._session.begin_transaction()
        self._keyvalue.set_key(key)
        self._keyvalue.set_value(self._dumps(value))
        self._keyvalue.insert()
        self._session.commit_transaction()

Voici le programme de test adapté de @saaj answer:

#!/usr/bin/env python3

import os
import random

import lipsum
from wtdict import WTDict


def main():
    with WTDict('wt') as wt:
        for _ in range(100000):
            v = lipsum.generate_paragraphs(2)[0:random.randint(200, 1000)]
            wt[os.urandom(10)] = v

if __name__ == '__main__':
    main()

En utilisant la ligne de commande suivante:

python test-wtdict.py & psrecord --plot=plot.png --interval=0.1 $!

J'ai généré le diagramme suivant:

wt performance without wal

$ du -h wt
60M wt

Lorsque le journal d'écriture anticipée est actif:

wt performance with wal

$ du -h wt
260M    wt

Ceci sans réglage ni compression des performances.

Wiredtiger n'a pas de limite connue jusqu'à récemment, la documentation a été mise à jour comme suit:

WiredTiger prend en charge les tables de pétaoctets, enregistre jusqu'à 4 Go et enregistre des nombres jusqu'à 64 bits.

http://source.wiredtiger.com/1.6.4/architecture.html

2
amirouche