素人の言語処理100本ノック:60


[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)の挑戦記録です。環境はUbuntu 16.04 LTS ＋ Python 3.5.2 \:\: Anaconda 4.1.1 (64-bit)です。過去のノックの一覧は[こちら](http://qiita.com/segavvy/items/fb50ba8097d59475f760)からどうぞ。

## 第7章: データベース
>artist.json.gzは，オープンな音楽データベースMusicBrainzの中で，アーティストに関するものをJSON形式に変換し，gzip形式で圧縮したファイルである．このファイルには，1アーティストに関する情報が1行にJSON形式で格納されている．JSON形式の概要は以下の通りである．
>
>|フィールド|型|内容|例|
>|--------|--------|-----|----|
>|id|ユニーク識別子|整数|20660|
>|gid|グローバル識別子|文字列|"ecf9f3a3-35e9-4c58-acaa-e707fba45060"|
>|name|アーティスト名|文字列|"Oasis"|
>|sort_name|アーティスト名（辞書順整列用）|文字列|"Oasis"|
>|area|活動場所|文字列|"United Kingdom"|
>|aliases|別名|辞書オブジェクトのリスト||
>|aliases[].name|別名|文字列|"オアシス"|
>|aliases[].sort_name|別名（整列用）|文字列|"オアシス"|
>|begin|活動開始日|辞書||
>|begin.year|活動開始年|整数|1991|
>|begin.month|活動開始月|整数||
>|begin.date|活動開始日|整数||
>|end|活動終了日|辞書||
>|end.year|活動終了年|整数|2009|
>|end.month|活動終了月|整数|8|
>|end.date|活動終了日|整数|28|
>|tags|タグ|辞書オブジェクトのリスト||
>|tags[].count|タグ付けされた回数|整数|1|
>|tags[].value|タグ内容|文字列|"rock"|
>|rating|レーティング|辞書オブジェクト||
>|rating.count|レーティングの投票数|整数|13|
>|rating.value|レーティングの値（平均値）|整数|86|
>
>artist.json.gzのデータをKey-Value-Store (KVS) およびドキュメント志向型データベースに格納・検索することを考える．KVSとしては，LevelDB，Redis，KyotoCabinet等を用いよ．ドキュメント志向型データベースとして，MongoDBを採用したが，CouchDBやRethinkDB等を用いてもよい．

###60. KVSの構築
>Key-Value-Store (KVS) を用い，アーティスト名（name）から活動場所（area）を検索するためのデータベースを構築せよ．

####出来上がったコード：

```python:main.py
# coding: utf-8
import gzip
import json
import leveldb

fname = 'artist.json.gz'
fname_db = 'test_db'

# LevelDBオープン、なければ作成
db = leveldb.LevelDB(fname_db)

# gzファイル読み込み、パース
with gzip.open(fname, 'rt') as data_file:
	for line in data_file:
		data_json = json.loads(line)

		# key=name+id、value=areaとしてDBへ追加
		key = data_json['name'] + '\t' + str(data_json['id'])
		value = data_json.get('area', '')		# areaはないことがある
		db.Put(key.encode(), value.encode())

# 確認のため登録件数を表示
print('{}件登録しました。'.format(len(list(db.RangeIter(include_value=False)))))
```

####実行結果：

```console:実行結果
921337件登録しました。
```

###LevelDBについて

今回はKVSとしてLevelDBを使うことにしました。LevelDBはGoogleがBigTableのアーキテクチャをベースにOSS化したKVSだそうです。オフィシャルサイトは[こちら](https://github.com/google/leveldb)です。日本語での解説は、yo-chanさんのブログの[LevelDB入門 (基本編)]
(http://yosuke-furukawa.hatenablog.com/entry/2014/05/05/095207)が非常に分かりやすかったです。

###LevelDBのインストール

問題に取り組む前に環境構築が必要です。インストールはcondaで見つかったので簡単にできました。LevelDBをpythonで使うためのpython-leveldbも同時に見つかります。

```console:LevelDBのインストール
segavvy@ubuntu:~$ conda search leveldb
Fetching package metadata .......
leveldb                      1.19                          0  defaults        
python-leveldb               0.194                    py27_0  defaults        
                             0.194                    py34_0  defaults        
                             0.194                    py35_0  defaults        
segavvy@ubuntu:~$ conda install leveldb
Fetching package metadata .......
Solving package specifications: ..........

Package plan for installation in environment /home/segavvy/anaconda3:

The following packages will be downloaded:

    package                    |            build
    ---------------------------|-----------------
    snappy-1.1.3               |                0         214 KB
    leveldb-1.19               |                0         320 KB
    conda-4.2.13               |           py35_0         402 KB
    ------------------------------------------------------------
                                           Total:         936 KB

The following NEW packages will be INSTALLED:

    leveldb: 1.19-0       
    snappy:  1.1.3-0      

The following packages will be UPDATED:

    conda:   4.2.12-py35_0 --> 4.2.13-py35_0

Proceed ([y]/n)? y

Fetching packages ...
snappy-1.1.3-0 100% |################################| Time: 0:00:00 436.20 kB/s
leveldb-1.19-0 100% |################################| Time: 0:00:00 586.67 kB/s
conda-4.2.13-p 100% |################################| Time: 0:00:00   8.57 MB/s
Extracting packages ...
[      COMPLETE      ]|############################################################| 100%
Unlinking packages ...
[      COMPLETE      ]|############################################################| 100%
Linking packages ...
[      COMPLETE      ]|############################################################| 100%
```

```console:python-leveldbのインストール
segavvy@ubuntu:~$ conda install python-leveldb
Fetching package metadata .......
Solving package specifications: ..........

Package plan for installation in environment /home/segavvy/anaconda3:

The following packages will be downloaded:

    package                    |            build
    ---------------------------|-----------------
    python-leveldb-0.194       |           py35_0          75 KB

The following NEW packages will be INSTALLED:

    python-leveldb: 0.194-py35_0

Proceed ([y]/n)? y

Fetching packages ...
python-leveldb 100% |#########################################| Time: 0:00:00 459.40 kB/s
Extracting packages ...
[      COMPLETE      ]|############################################################| 100%
Linking packages ...
[      COMPLETE      ]|############################################################| 100%
```

これでimportできるようになります。

```console:LevelDBインストールの確認
segavvy@ubuntu:~$ python
Python 3.5.2 |Anaconda 4.1.1 (64-bit)| (default, Jul  2 2016, 17:53:06) 
[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import leveldb
>>> 
```

###LevelDBの使い方
LevelDBは非常にシンプルなので、helpでだいたい使い方が把握できました。

```console:LevelDBのヘルプの表示
segavvy@ubuntu:~$ python 
Python 3.5.2 |Anaconda 4.1.1 (64-bit)| (default, Jul  2 2016, 17:53:06) 
[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import leveldb
>>> help(leveldb)
```

```console:LevelDBのヘルプ（先頭部分）
Help on module leveldb:

NAME
    leveldb

CLASSES
    builtins.Exception(builtins.BaseException)
        LevelDBError
    builtins.object
        LevelDB
        Snapshot
        WriteBatch
    
    class LevelDB(builtins.object)
     |  LevelDB(filename, **kwargs) -> leveldb object
     |  
     |  Open a LevelDB database, from the given directory.
     |  
     |  Only the parameter filename is mandatory.
     |  
     |  filename                                    the database directory
     |  create_if_missing (default: True)           if True, creates a new database if none exists
     |  error_if_exists   (default: False)          if True, raises and error if the database already exists
     |  paranoid_checks   (default: False)          if True, raises an error as soon as an internal corruption is detected
     |  block_cache_size  (default: 8 * (2 << 20))  maximum allowed size for the block cache in bytes
     |  write_buffer_size (default  2 * (2 << 20))  
     |  block_size        (default: 4096)           unit of transfer for the block cache in bytes
     |  max_open_files:   (default: 1000)
     |  block_restart_interval           
     |  
     |  Snappy compression is used, if available.
     |  
     |  Some methods support the following parameters, having these semantics:
     |  
     |   verify_checksum: iff True, the operation will check for checksum mismatches
     |   fill_cache:      iff True, the operation will fill the cache with the data read
     |   sync:            iff True, the operation will be guaranteed to sync the operation to disk
     |  
     |  Methods supported are:
     |  
     |   Get(key, verify_checksums = False, fill_cache = True): get value, raises KeyError if key not found
     |  
     |      key: the query key
     |  
     |   Put(key, value, sync = False): put key/value pair
     |  
     |      key: the key
（以下略）
```

実装の流れは、まず`leveldb.LevelDB()`で保存先のディレクトリを指定してデータベースを作ります。デフォルトでは存在しなければ新規作成してくれます。あとは、`LevelDB.Put()`でkeyとvalueのペアを追加していくだけです。

###活動場所の欠落

このデータの活動場所（area）の情報は、半分弱のアーティストにしか登録されていないようです。そのため、活動場所が登録されていないアーティストに関しては空文字列を登録するようにしました。

###アーティスト名の重複

最初はアーティスト名が重複していることに気づかずに、keyとしてアーティスト名、valueとして活動場所を登録する、次のようなコードにしていました。

```python:アーティスト名の重複を考慮していなかったコード
# gzファイル読み込み、パース
with gzip.open(fname, 'rt') as data_file:
	for line in data_file:
		data_json = json.loads(line)

		# nameとareaをDBへ追加
		name = data_json['name']			# nameはなければエラー
		value = data_json.get('area', '')		# areaはないことがある
		db.Put(name.encode(), area.encode())
```

しかし、対象データには問題の例で使われているOasisでさえ3件も存在していることに気づきました。
`LevelDB.Put()`はkeyが同じ場合にvalueを更新してしまうため、重複していると後勝ちになってしまい、先に登録した情報が欠落してしまいます。

そのため今回は、keyにアーティスト名とユニーク識別子（id）を`\t`で連結したものを使う形にしてみました。

###文字列とバイト列の変換

LebelDBは、keyとvalueをどちらもバイト列で指定する必要があります。そのため、文字列からバイト列への変換が必要です。

文字列からバイト列への変換は[`str.encode()`](http://docs.python.jp/3/library/stdtypes.html#str.encode)でできます。デフォルトではUTF-8にしてくれます。逆にUTF-8のバイト列から文字列には、[`bytes.decode()`]
(http://docs.python.jp/3/library/stdtypes.html#bytes.decode)で戻せます。

###登録の確認

今回の問題は登録のみで、登録できているかどうかの確認は次の問題になりますが、最後の`LevelDB.RangeIter()`で登録内容全件にアクセスするためのイテレータを取得して、件数だけ表示してみました。件数を取得したいだけなので、`include_value=False`を指定して、keyのみを取得しています。

　
61本目のノックは以上です。誤りなどありましたら、ご指摘いただけますと幸いです。
<hr>

*実行結果には、[100本ノックで用いるコーパス・データ](http://www.cl.ecei.tohoku.ac.jp/nlp100/#data)で配布されているデータの一部が含まれます。この第7章で用いているデータのライセンスは[クリエイティブ・コモンズ 表示 - 非営利 - 継承 3.0 非移植](https://creativecommons.org/licenses/by-nc-sa/3.0/)（[日本語訳](https://creativecommons.org/licenses/by-nc-sa/3.0/deed.ja)）です。*