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


[言語処理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等を用いてもよい．

###65. MongoDBの検索
>MongoDBのインタラクティブシェルを用いて，"Queen"というアーティストに関する情報を取得せよ．さらに，これと同様の処理を行うプログラムを実装せよ．

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

```python:main.py
# coding: utf-8
import json
from pymongo import MongoClient
from bson.objectid import ObjectId


def support_ObjectId(obj):
	'''json.dumps()でObjectIdを処理するための関数
	ObjectIdはjsonエンコードできない型なので、文字列型に変換する

	戻り値：
	ObjectIdから変換した文字列
	'''
	if isinstance(obj, ObjectId):
		return str(obj)		# 文字列として扱う
	raise TypeError(repr(obj) + " is not JSON serializable")


# MongoDBのデータベースtestdbにコレクションartistにアクセス
client = MongoClient()
db = client.testdb
collection = db.artist

# 検索
for i, doc in enumerate(collection.find({'name': 'Queen'}), start=1):

	# 整形して表示
	print('{}件目の内容：\n{}'.format(i,
		json.dumps(
			doc, indent='\t', ensure_ascii=False,
			sort_keys=True, default=support_ObjectId
		)
	))
```

####実行結果：

まず、インタラクティブシェルの実行結果です。

```console:インタラクティブシェルでの実行結果
segavvy@ubuntu:~$ mongo
MongoDB shell version v3.4.1
connecting to: mongodb://127.0.0.1:27017
MongoDB server version: 3.4.1
Server has startup warnings: 
2017-01-14T17:47:12.528+0900 I STORAGE  [initandlisten] 
2017-01-14T17:47:12.528+0900 I STORAGE  [initandlisten] ** WARNING: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine
2017-01-14T17:47:12.528+0900 I STORAGE  [initandlisten] **          See http://dochub.mongodb.org/core/prodnotes-filesystem
2017-01-14T17:47:12.898+0900 I CONTROL  [initandlisten] 
2017-01-14T17:47:12.898+0900 I CONTROL  [initandlisten] ** WARNING: Access control is not enabled for the database.
2017-01-14T17:47:12.898+0900 I CONTROL  [initandlisten] **          Read and write access to data and configuration is unrestricted.
2017-01-14T17:47:12.898+0900 I CONTROL  [initandlisten] 
2017-01-14T17:47:12.898+0900 I CONTROL  [initandlisten] 
2017-01-14T17:47:12.898+0900 I CONTROL  [initandlisten] ** WARNING: /sys/kernel/mm/transparent_hugepage/enabled is 'always'.
2017-01-14T17:47:12.898+0900 I CONTROL  [initandlisten] **        We suggest setting it to 'never'
2017-01-14T17:47:12.898+0900 I CONTROL  [initandlisten] 
2017-01-14T17:47:12.898+0900 I CONTROL  [initandlisten] ** WARNING: /sys/kernel/mm/transparent_hugepage/defrag is 'always'.
2017-01-14T17:47:12.898+0900 I CONTROL  [initandlisten] **        We suggest setting it to 'never'
2017-01-14T17:47:12.898+0900 I CONTROL  [initandlisten] 
> show dbs
admin   0.000GB
local   0.000GB
testdb  0.131GB
> use testdb
switched to db testdb
> show collections
artist
> db.artist.find( { 'name': 'Queen' } );
{ "_id" : ObjectId("5879fda21d41c85827363e72"), "name" : "Queen", "id" : 701492, "type" : "Character", "tags" : [ { "value" : "kamen rider w", "count" : 1 }, { "value" : "related-akb48", "count" : 1 } ], "gid" : "420ca290-76c5-41af-999e-564d7c71f1a7", "ended" : true, "sort_name" : "Queen", "gender" : "Female", "area" : "Japan", "aliases" : [ { "name" : "Queen", "sort_name" : "Queen" } ] }
{ "_id" : ObjectId("5879fda51d41c8582737051e"), "name" : "Queen", "id" : 192, "tags" : [ { "value" : "hard rock", "count" : 2 }, { "value" : "70s", "count" : 1 }, { "value" : "queen family", "count" : 1 }, { "value" : "90s", "count" : 1 }, { "value" : "80s", "count" : 1 }, { "value" : "glam rock", "count" : 1 }, { "value" : "british", "count" : 4 }, { "value" : "english", "count" : 1 }, { "value" : "uk", "count" : 2 }, { "value" : "pop/rock", "count" : 1 }, { "value" : "pop-rock", "count" : 1 }, { "value" : "britannique", "count" : 1 }, { "value" : "classic pop and rock", "count" : 1 }, { "value" : "queen", "count" : 1 }, { "value" : "united kingdom", "count" : 1 }, { "value" : "langham 1 studio bbc", "count" : 1 }, { "value" : "kind of magic", "count" : 1 }, { "value" : "band", "count" : 1 }, { "value" : "rock", "count" : 6 }, { "value" : "platinum", "count" : 1 } ], "gid" : "0383dadf-2a4e-4d10-a46a-e9e041da8eb3", "sort_name" : "Queen", "ended" : true, "area" : "United Kingdom", "type" : "Group", "begin" : { "date" : 27, "month" : 6, "year" : 1970 }, "aliases" : [ { "name" : "女王", "sort_name" : "女王" } ], "rating" : { "value" : 92, "count" : 24 } }
{ "_id" : ObjectId("5879fdab1d41c8582738bf76"), "id" : 992994, "name" : "Queen", "gid" : "5eecaf18-02ec-47af-a4f2-7831db373419", "sort_name" : "Queen", "ended" : true }
> exit
bye
```

続いてプログラムの実行結果です。

```console:実行結果
1件目の内容：
{
	"_id": "5879fda21d41c85827363e72",
	"aliases": [
		{
			"name": "Queen",
			"sort_name": "Queen"
		}
	],
	"area": "Japan",
	"ended": true,
	"gender": "Female",
	"gid": "420ca290-76c5-41af-999e-564d7c71f1a7",
	"id": 701492,
	"name": "Queen",
	"sort_name": "Queen",
	"tags": [
		{
			"count": 1,
			"value": "kamen rider w"
		},
		{
			"count": 1,
			"value": "related-akb48"
		}
	],
	"type": "Character"
}
2件目の内容：
{
	"_id": "5879fda51d41c8582737051e",
	"aliases": [
		{
			"name": "女王",
			"sort_name": "女王"
		}
	],
	"area": "United Kingdom",
	"begin": {
		"date": 27,
		"month": 6,
		"year": 1970
	},
	"ended": true,
	"gid": "0383dadf-2a4e-4d10-a46a-e9e041da8eb3",
	"id": 192,
	"name": "Queen",
	"rating": {
		"count": 24,
		"value": 92
	},
	"sort_name": "Queen",
	"tags": [
		{
			"count": 2,
			"value": "hard rock"
		},
		{
			"count": 1,
			"value": "70s"
		},
		{
			"count": 1,
			"value": "queen family"
		},
		{
			"count": 1,
			"value": "90s"
		},
		{
			"count": 1,
			"value": "80s"
		},
		{
			"count": 1,
			"value": "glam rock"
		},
		{
			"count": 4,
			"value": "british"
		},
		{
			"count": 1,
			"value": "english"
		},
		{
			"count": 2,
			"value": "uk"
		},
		{
			"count": 1,
			"value": "pop/rock"
		},
		{
			"count": 1,
			"value": "pop-rock"
		},
		{
			"count": 1,
			"value": "britannique"
		},
		{
			"count": 1,
			"value": "classic pop and rock"
		},
		{
			"count": 1,
			"value": "queen"
		},
		{
			"count": 1,
			"value": "united kingdom"
		},
		{
			"count": 1,
			"value": "langham 1 studio bbc"
		},
		{
			"count": 1,
			"value": "kind of magic"
		},
		{
			"count": 1,
			"value": "band"
		},
		{
			"count": 6,
			"value": "rock"
		},
		{
			"count": 1,
			"value": "platinum"
		}
	],
	"type": "Group"
}
3件目の内容：
{
	"_id": "5879fdab1d41c8582738bf76",
	"ended": true,
	"gid": "5eecaf18-02ec-47af-a4f2-7831db373419",
	"id": 992994,
	"name": "Queen",
	"sort_name": "Queen"
}
```

###インタラクティブシェルでの検索
`mongo`でシェルを起動して、`use`でデータベースを切り替えて、`db.(コレクション).find()`で検索できます。[前問](http://qiita.com/segavvy/items/8f93187ec89f4831d863)同様、svjunicさんがまとめられている[MongoDB コマンドメモとか書き](http://qiita.com/svjunic/items/285e9cf20169d70aa1fa)が便利です。

###プログラムでの検索
インタラクティブシェル同様、[`collection.find()`](http://api.mongodb.com/python/3.3.0/api/pymongo/collection.html#pymongo.collection.Collection.find)（最新は[こちら](http://api.mongodb.com/python/current/api/pymongo/collection.html#pymongo.collection.Collection.find)）で検索できます。検索すると[`Cursor`](http://api.mongodb.com/python/3.3.0/api/pymongo/cursor.html#pymongo.cursor.Cursor)（[最新](http://api.mongodb.com/python/current/api/pymongo/cursor.html#pymongo.cursor.Cursor)）オブジェクトが返って、結果を列挙できるようになっています。

###JSON形式で整形
結果から取り出したドキュメントは辞書型になっているので、そのまま表示すると1行になって非常に見にくくなります。

```console:1件目をprint(doc)した結果
{'tags': [{'count': 1, 'value': 'kamen rider w'}, {'count': 1, 'value': 'related-akb48'}], 'type': 'Character', '_id': ObjectId('5879fda21d41c85827363e72'), 'id': 701492, 'gender': 'Female', 'gid': '420ca290-76c5-41af-999e-564d7c71f1a7', 'sort_name': 'Queen', 'name': 'Queen', 'aliases': [{'sort_name': 'Queen', 'name': 'Queen'}], 'ended': True, 'area': 'Japan'}
```

この辞書を見やすく整形できないか考えていたところ、Hyperion13fleetさんの[[python] JSONファイルのフォーマットを整えてDumpする](http://qiita.com/Hyperion13fleet/items/7129623ab32bdcc6e203)を見つけました。辞書を[`json.dumps()`](http://docs.python.jp/3/library/json.html#json.dumps)でJSON形式に戻せば整形できそうです。

ところが、単純に`json.dumps(doc, indent='\t', sort_keys=True)`とかするとエラーになってしまいます。

```console:json.dumps(doc)のエラー
TypeError: ObjectId('5879fda21d41c85827363e72') is not JSON serializable
```

これは、docで指定した辞書の中にJSON形式に変換できない`ObjectId`というオブジェクトがあるためです。[問題20](http://qiita.com/segavvy/items/dc1e63fd8f7bd5d99eea)で勉強しましたが、JSON形式とPythonの辞書への変換は[変換表](http://docs.python.jp/3/library/json.html#json-to-py-table)に従って行われるので、ここにない型はエラーになってしまうのです。

でも、[前問](http://qiita.com/segavvy/items/8f93187ec89f4831d863)でJSON形式のデータを読み込んで辞書に変換してMongoDBに登録し、今回それをMongoDBから辞書として取り出してJSON形式に戻そうとしただけです。可逆じゃないのが不思議で最初はちょっと悩みましたが、この`ObjectId`は元データにはなく、MongoDBに登録する時に自動的に追加される12バイトのユニークキーであることがわかりました。`ObjectId`の詳細は[こちら](http://api.mongodb.com/python/3.3.0/api/bson/objectid.html#module-bson.objectid)（[最新](http://api.mongodb.com/python/current/api/bson/objectid.html#module-bson.objectid)）です。

###JSON形式の変換表にない型の扱い

幸い、変換表にない型については、自分で変換関数を実装して指定することができます。それが[`json.dumps()`](http://docs.python.jp/3/library/json.html#json.dumps)の`default`という引数で指定できるコールバック関数です。引数の説明は[`json.dump()`](http://docs.python.jp/3/library/json.html#json.dump)と共通なのでそちらに説明があります。この`default`の説明部分は翻訳されておらず英語のままなのですが、ちゃんと読むと使い方がわかりました。今回は`support_ObjectId()`を作って文字列に変換しています。

なお、引数`default`を使うのではなく、JSONEncoderの派生クラスを指定する方法もあるそうです。どちらもpodhmoさんの[pythonでjson出力する際で対応していない型(e.g. datetime)の値を変換しながら出力したい](http://qiita.com/podhmo/items/dc748a9d40026c28556d)の解説がわかりやすいです。

　
66本目のノックは以上です。誤りなどありましたら、ご指摘いただけますと幸いです。
<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)）です。*