PythonでLevelDBを使ってみる(plyvel)


## LevelDBとは?

ファイルに保存するタイプのkey-value storeで、key => valueのひも付けを高速に読み書きできるGoogle製のライブラリです。PythonのここではLevelDBをplyvelというライブラリを使ってpythonで使ってみるチュートリアルをご紹介します。(plyvel以外にもPythonのLevelDBライブラリは存在しますが、使ってみた感じplyvelの出来がよさそうでしたので)

こういうたぐいのものでは古くはBerkeleyDBや、モダンなものだと日本の平林さんが作成された[KyotoCabinet](http://fallabs.com/kyotocabinet/)が有名です。サーバ型では[memcached](http://memcached.org/)や[Redis](http://redis.io/)がありますが、LevelDBはサーバ型ではなく、ローカルのファイルを操作するライブラリです。たくさんのサーバ・プロセスなどからkey-valueのデータを共有して保存したいときはmemcachedや[KyotoTycoon](http://fallabs.com/kyototycoon/), Redisなどの方が向いているでしょう。

Pythonにはキーに値をひもづけて管理するデータ構造として``dict``がありますが、メモリ上に乗り切らない大量のデータなどを扱いたいときに便利なライブラリです。

### 事前知識

- LevelDBはあるバイト列であるkeyに, あるバイト列であるvalueをひもづけて管理するkey-value store。keyからvalueを高速にルックアップすることができる。
- LevelDBのデータベースはディレクトリになっている。
- LevelDBデータベースは同時にたかだか1つのプロセスでしか開けない。(既に別のプロセスが開いているとエラーになる)
- サーバではないので、通信でkey-valueを操作するものではない。(ローカルのファイルを操作するライブラリである。)
- plyvelはnativeのleveldbのライブラリを使っているC拡張で実装されている(ゆえに速いがライブラリのインストールとコンパイルが必要)
- LevelDBのキーと値にはバイナリ値を与えることになる。
  - 保存したい値が``string``ではない場合はpickleやmsgpackなど、なんらかの方法でシリアライズする必要がある
  - pythonの``unicode``型の値は``val.encode('utf-8')``などでバイナリにエンコードしてあげる必要あり

## インストール(1/2)

まずleveldbのネイティブライブラリが必要なのでインストールします。MacでHomebrewを使っている人はbrewで入れると楽です。

```bash
$ brew install leveldb
```

他のプラットフォームでは``Makefile``があるようですので、``make``と``sudo make install``でインストールできるのではないでしょうか。

## インストール(2/2)

``plyvel``をインストールしましょう。なんでこんな綴りかよくわからないですが、とにかくplyvelです。

```bash
$ pip install plyvel
```

システムのincludeパスにPython.hがなかったりすると拡張ライブラリがコンパイルできなくてエラーになるかもしれません。Linuxなら``python-devel``などのパッケージが入ってるか確認してみるとよいでしょう。

## DBをopenしてcloseしてみる

LevelDBのデータベースはディレクトリになってるので注意。

```python
import plyvel
my_db = plyvel.DB('/tmp/test.ldb', create_if_missing=True)  # なければつくる
my_db.close()
```

``create_if_missing=False``の場合、DBファイルが存在しなければ例外が発生します。

## key => valueを登録する

LevelDBはバイト列にバイト列をひもづけることしかできないので基本は以下のようになります。

```python
my_db.put('key1', 'value1')
my_db.put(u'ほげ'.encode('utf-8'), u'ほげバリュー'.encode('utf-8'))  # unicodeを使うときはバイト列にする
```

あるキーに、複雑なデータ構造をひもづけて保存したいときは下記のpickleやMessagePackを使う方法を試してみてください。


## keyから値を取り出す

そのまんま、``get``してください。

```python
value1 = my_db.get('key1')
value2 = my_db.get(u'ユニコードのキーはencodeでバイト列にする'.encode('utf-8'))
```

## keyを削除

``delete``メソッドで消します。

```python
my_db.delete('key1')
my_db.delete(u'ユニコードのキーはencodeでバイト列にする'.encode('utf-8'))
```

## 保存されている項目をイテレーション

キーと値をたくさん登録して、最後に全部をCSVに出したいとか、そういうことってありますよね。LevelDBでも当然保存されてるキーと値をイテレータで取り出すことができます。

```python
my_db.put('key1', '1')
my_db.put('key2', '1')
my_db.put('key3', '3')

for key, value in my_db:
	print '%s => %s' % (key, value)

# 出力:
# key1 => 1
# key2 => 2
# key3 => 3
```

キーの範囲を指定したり、キーのprefixを指定して取り出すこともできるようです。詳しくは[plyvelのドキュメントのiteratorsのセクション](https://plyvel.readthedocs.org/en/latest/user.html#iterators)を見て見てください。


## 構造化されたvalueを登録してみる: pickle

冒頭にも書きましたが、valueはバイト列でなければいけないので、keyに対してdictやlist/などの構造化されたデータをひもづけてLevelDBに保存したい場合、シリアライズする必要があります。Pythonにpickleというシリアライズ用のライブラリが標準であるので、これを使うと簡単です。Pickleは基本的なデータ型だけでなく、関数やオブジェクトなどもシリアライズすることができ、非常に強力です。ただ、Python以外の言語ではpickleデータをデシリアライズ(元のデータ構造に戻す)ことが難しいので他の言語でもデータを流用したい場合は後述のMessagePack形式でシリアライズするとよいでしょう。

```python
fukuzatsu1 = dict(a=10, b=20, c=[123, 234, 456])
my_db.put('key1', fukuzatsu1)  # エラーになる

import pickle
serialized1 = pickle.dumps(fukuzatsu1)
my_db.put('key1', serialized1)  # OK 

# valueを使うとき
serialized1 = my_db.get('key1')
fukuzatsu1 = pickle.loads(serialized1)
print fukuzatsu1['a']  # => 10
```

pickleの使い方は詳しくは[pickleのドキュメント](http://docs.python.jp/2/library/pickle.html)を参照してください。

## 構造化されたvalueを登録してみる: msgpack

pickleの他にはMessagePackという日本発のシリアライジングフォーマットがおすすめです。MessagePackはデータをコンパクトかつ高速にシリアライズできることが特徴です。MessagePackをPythonで使うときはmsgpack-pythonというパッケージをインストールします。

```bash
 $ pip install msgpack-python
```

msgpackではpickleの``dumps``/``loads``に変わって``packb``/``unpackb``を使います。

```python
fukuzatsu1 = dict(a=10, b=20, c=[123, 234, 456])

import msgpack
serialized1 = msgpack.packb(fukuzatsu1, encoding='utf-8')
my_db.put('key1', serialized1)

# valueを使うとき
serialized1 = my_db.get('key1')
fukuzatsu1 = msgpack.unpackb(serialized1, encoding='utf-8')
print fukuzatsu1['a']  # => 10
```

詳しくは[msgpack-pythonのAPIドキュメント](http://pythonhosted.org//msgpack-python/api.html)を参照してください。

## 参考リンク

- [グーグルがNoSQL軽量ライブラリ「LevelDB」をオープンソース化。SQLiteとの比較ベンチマークも公開](http://www.publickey1.jp/blog/11/nosqlleveldbsqlite.html)
- [LevelDB](http://leveldb.org/)
- [Plyvel Document](https://plyvel.readthedocs.org/en/latest/index.html)
- シリアライズ関連
    - [pickle](http://docs.python.jp/2/library/pickle.html)
    - [MessagePack Project]()
    - [msgpack-python](https://pypi.python.org/pypi/msgpack-python/0.4.4)
- サーバ型 key-value store
    - [memcached](http://memcached.org/)
    - [Redis](redis.io)
    - [KyotoTycoon](http://fallabs.com/kyototycoon/)