言語処理100本ノック 2015の挑戦記録です。環境はUbuntu 16.04 LTS + Python 3.5.2 :: Anaconda 4.1.1 (64-bit)です。過去のノックの一覧はこちらからどうぞ。
第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)を検索するためのデータベースを構築せよ.
####出来上がったコード:
# 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)))))
####実行結果:
921337件登録しました。
###LevelDBについて
今回はKVSとしてLevelDBを使うことにしました。LevelDBはGoogleがBigTableのアーキテクチャをベースにOSS化したKVSだそうです。オフィシャルサイトはこちらです。日本語での解説は、yo-chanさんのブログの[LevelDB入門 (基本編)]
(http://yosuke-furukawa.hatenablog.com/entry/2014/05/05/095207)が非常に分かりやすかったです。
###LevelDBのインストール
問題に取り組む前に環境構築が必要です。インストールはcondaで見つかったので簡単にできました。LevelDBをpythonで使うためのpython-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%
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できるようになります。
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でだいたい使い方が把握できました。
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)
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として活動場所を登録する、次のようなコードにしていました。
# 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()
でできます。デフォルトではUTF-8にしてくれます。逆にUTF-8のバイト列から文字列には、[bytes.decode()
]
(http://docs.python.jp/3/library/stdtypes.html#bytes.decode)で戻せます。
###登録の確認
今回の問題は登録のみで、登録できているかどうかの確認は次の問題になりますが、最後のLevelDB.RangeIter()
で登録内容全件にアクセスするためのイテレータを取得して、件数だけ表示してみました。件数を取得したいだけなので、include_value=False
を指定して、keyのみを取得しています。
61本目のノックは以上です。誤りなどありましたら、ご指摘いただけますと幸いです。
実行結果には、100本ノックで用いるコーパス・データで配布されているデータの一部が含まれます。この第7章で用いているデータのライセンスはクリエイティブ・コモンズ 表示 - 非営利 - 継承 3.0 非移植(日本語訳)です。