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


[言語処理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)からどうぞ。

## 第4章: 形態素解析
>夏目漱石の小説『吾輩は猫である』の文章（neko.txt）をMeCabを使って形態素解析し，その結果をneko.txt.mecabというファイルに保存せよ．このファイルを用いて，以下の問に対応するプログラムを実装せよ．
>
>なお，問題37, 38, 39はmatplotlibもしくはGnuplotを用いるとよい．

###34. 「AのB」
>2つの名詞が「の」で連結されている名詞句を抽出せよ．

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

```python:main.py
# coding: utf-8
import MeCab
fname = 'neko.txt'
fname_parsed = 'neko.txt.mecab'


def parse_neko():
	'''「吾輩は猫である」を形態素解析
	「吾輩は猫である」(neko.txt)を形態素解析してneko.txt.mecabに保存する
	'''

	with open(fname) as data_file, \
			open(fname_parsed, mode='w') as out_file:

		mecab = MeCab.Tagger()
		out_file.write(mecab.parse(data_file.read()))


def neco_lines():
	'''「吾輩は猫である」の形態素解析結果のジェネレータ
	「吾輩は猫である」の形態素解析結果を順次読み込んで、各形態素を
	・表層形（surface）
	・基本形（base）
	・品詞（pos）
	・品詞細分類1（pos1）
	の4つをキーとする辞書に格納し、1文ずつ、この辞書のリストとして返す

	戻り値：
	1文の各形態素を辞書化したリスト
	'''
	with open(fname_parsed) as file_parsed:

		morphemes = []
		for line in file_parsed:

			# 表層形はtab区切り、それ以外は','区切りでバラす
			cols = line.split('\t')
			if(len(cols) < 2):
				raise StopIteration		# 区切りがなければ終了
			res_cols = cols[1].split(',')

			# 辞書作成、リストに追加
			morpheme = {
				'surface': cols[0],
				'base': res_cols[6],
				'pos': res_cols[0],
				'pos1': res_cols[1]
			}
			morphemes.append(morpheme)

			# 品詞細分類1が'句点'なら文の終わりと判定
			if res_cols[1] == '句点':
				yield morphemes
				morphemes = []


# 形態素解析
parse_neko()

# 1文ずつ辞書のリストを取得し抽出
list_a_no_b = []		# 出現順リスト、重複あり
lines = neco_lines()
for line in lines:
	if len(line) > 2:
		for i in range(1, len(line) - 1):
			if line[i]['surface'] == 'の' \
					and line[i - 1]['pos'] == '名詞' \
					and line[i + 1]['pos'] == '名詞':
				list_a_no_b.append(line[i - 1]['surface'] + 'の' + line[i + 1]['surface'])
a_no_b = set(list_a_no_b)		# 重複除去

# 確認しやすいようlist_a_no_bを使って出現順にソートして表示
print(sorted(a_no_b, key=list_a_no_b.index))
```

####実行結果：
長いので先頭部分のみです。

```console:端末（先頭部分）
['彼の掌', '掌の上', '書生の顔', 'はずの顔', '顔の真中', '穴の中', '書生の掌', '掌の裏', '何の事', '肝心の母親', '藁の上', '笹原の中', '池の前', '一樹の蔭', '垣根の穴', '隣家の三', '時の通路', '一刻の猶予', '家の内', '彼の書生', '以外の人間', '前の書生', 'おさんの隙', 'おさんの三', '胸の痞', '家の主人', '主人の方', '鼻の下', '吾輩の顔', '自分の住', '吾輩の主人', '家のもの', 'うちのもの', '彼の書斎', '本の上', '皮膚の色', '彼の毎夜', '以外のもの', '主人の傍', '彼の膝', '膝の上', '経験の上', '飯櫃の上', '炬燵の上', 'ここのうち', '供の寝床', '彼等の中間', '供の一', '例の神経', '性の主人', '次の部屋', '自分の勝手', 'へっついの中', '吾輩の方',
```

結果全体は[GitHub](https://github.com/segavvy/nlp100_Python/tree/master/34)にアップしています。


###`range()`の仕様
`parse_neko()`と`neco_lines()`は[前問](http://qiita.com/segavvy/items/229d0a672a866b3e09a1)と同じです。今回は先にリストを作り、その後、重複除去のために集合に変換しました。内包表記は混乱するので未挑戦です^^;

今回の条件に該当するためには最低でも形態素が3つ必要ですので、形態素が2つ以下の行は調べる必要がありません。そのため、最初のループの直後で`if len(line) > 2:`というチェックを入れています。ただし、この条件はなくても正しく動作することに気づきました。それは次の`range(1, len(line) - 1)`が、`len(line)`が2以下の時でもいい具合に動いてくれるためです。

```console:インタプリタ
>>> list(range(1, -1))
[]
>>> list(range(1, 0))
[]
>>> list(range(1, 1))
[]
>>> list(range(1, 2))
[1]
>>> list(range(1, 3))
[1, 2]
```

リストが空になれば実害がありません。そのため、`if len(line) > 2:`のチェックは不要ということですね。とりあえず分かりやすいように、コードにはチェックを入れたままにしてあります。`range()`の仕様の詳細は[こちら](http://docs.python.jp/3/library/stdtypes.html#range)をご参照ください。

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

*実行結果には、[100本ノックで用いるコーパス・データ](http://www.cl.ecei.tohoku.ac.jp/nlp100/#data)で配布されているデータの一部が含まれます。この第4章で用いているデータは[青空文庫](http://www.aozora.gr.jp)で公開されている夏目漱石の長編小説『吾輩は猫である』が元になっています。*