Edited at

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

More than 1 year has passed since last update.

言語処理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)を検索するためのデータベースを構築せよ.



出来上がったコード:


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)))))



実行結果:


実行結果

921337件登録しました。



LevelDBについて

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


LevelDBのインストール

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


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%



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できるようになります。


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でだいたい使い方が把握できました。


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)


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()で戻せます。


登録の確認

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

 

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


実行結果には、100本ノックで用いるコーパス・データで配布されているデータの一部が含まれます。この第7章で用いているデータのライセンスはクリエイティブ・コモンズ 表示 - 非営利 - 継承 3.0 非移植日本語訳)です。