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


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

## 第5章: 係り受け解析
>夏目漱石の小説『吾輩は猫である』の文章（neko.txt）をCaboChaを使って係り受け解析し，その結果をneko.txt.cabochaというファイルに保存せよ．このファイルを用いて，以下の問に対応するプログラムを実装せよ．

###46. 動詞の格フレーム情報の抽出
>45のプログラムを改変し，述語と格パターンに続けて項（述語に係っている文節そのもの）をタブ区切り形式で出力せよ．45の仕様に加えて，以下の仕様を満たすようにせよ．
>
> - 項は述語に係っている文節の単語列とする（末尾の助詞を取り除く必要はない）
> - 述語に係る文節が複数あるときは，助詞と同一の基準・順序でスペース区切りで並べる
>
>「吾輩はここで始めて人間というものを見た」という例文（neko.txt.cabochaの8文目）を考える． この文は「始める」と「見る」の２つの動詞を含み，「始める」に係る文節は「ここで」，「見る」に係る文節は「吾輩は」と「ものを」と解析された場合は，次のような出力になるはずである．
>
>```
>始める  で      ここで
>見る    は を   吾輩は ものを
>```

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

```python:main.py
# coding: utf-8
import CaboCha
import re

fname = 'neko.txt'
fname_parsed = 'neko.txt.cabocha'
fname_result = 'result.txt'


def parse_neko():
	'''「吾輩は猫である」を係り受け解析
	「吾輩は猫である」(neko.txt)を係り受け解析してneko.txt.cabochaに保存する
	'''
	with open(fname) as data_file, \
			open(fname_parsed, mode='w') as out_file:

		cabocha = CaboCha.Parser()
		for line in data_file:
			out_file.write(
				cabocha.parse(line).toString(CaboCha.FORMAT_LATTICE)
			)


class Morph:
	'''
	形態素クラス
	表層形（surface）、基本形（base）、品詞（pos）、品詞細分類1（pos1）を
	メンバー変数に持つ
	'''
	def __init__(self, surface, base, pos, pos1):
		'''初期化'''
		self.surface = surface
		self.base = base
		self.pos = pos
		self.pos1 = pos1

	def __str__(self):
		'''オブジェクトの文字列表現'''
		return 'surface[{}]\tbase[{}]\tpos[{}]\tpos1[{}]'\
			.format(self.surface, self.base, self.pos, self.pos1)


class Chunk:
	'''
	文節クラス
	形態素（Morphオブジェクト）のリスト（morphs）、係り先文節インデックス番号（dst）、
	係り元文節インデックス番号のリスト（srcs）をメンバー変数に持つ
	'''

	def __init__(self):
		'''初期化'''
		self.morphs = []
		self.srcs = []
		self.dst = -1

	def __str__(self):
		'''オブジェクトの文字列表現'''
		surface = ''
		for morph in self.morphs:
			surface += morph.surface
		return '{}\tsrcs{}\tdst[{}]'.format(surface, self.srcs, self.dst)

	def normalized_surface(self):
		'''句読点などの記号を除いた表層形'''
		result = ''
		for morph in self.morphs:
			if morph.pos != '記号':
				result += morph.surface
		return result

	def chk_pos(self, pos):
		'''指定した品詞（pos）を含むかチェックする

		戻り値：
		品詞（pos）を含む場合はTrue
		'''
		for morph in self.morphs:
			if morph.pos == pos:
				return True
		return False

	def get_morphs_by_pos(self, pos, pos1=''):
		'''指定した品詞（pos）、品詞細分類1（pos1）の形態素のリストを返す
		pos1の指定がない場合はposのみで判定する

		戻り値：
		形態素（morph）のリスト、該当形態素がない場合は空のリスト
		'''
		if len(pos1) > 0:
			return [res for res in self.morphs
					if (res.pos == pos) and (res.pos1 == pos1)]
		else:
			return [res for res in self.morphs if res.pos == pos]

	def get_kaku_prt(self):
		'''助詞を1つ返す
		複数ある場合は格助詞を優先し、最後の助詞を返す。

		戻り値：
		助詞、ない場合は空文字列
		'''
		prts = self.get_morphs_by_pos('助詞')
		if len(prts) > 1:

			# 2つ以上助詞がある場合は、格助詞を優先
			kaku_prts = self.get_morphs_by_pos('助詞', '格助詞')
			if len(kaku_prts) > 0:
				prts = kaku_prts

		if len(prts) > 0:
			return prts[-1].surface		# 最後を返す
		else:
			return ''


def neco_lines():
	'''「吾輩は猫である」の係り受け解析結果のジェネレータ
	「吾輩は猫である」の係り受け解析結果を順次読み込んで、
	1文ずつChunkクラスのリストを返す

	戻り値：
	1文のChunkクラスのリスト
	'''
	with open(fname_parsed) as file_parsed:

		chunks = dict()		# idxをkeyにChunkを格納
		idx = -1

		for line in file_parsed:

			# 1文の終了判定
			if line == 'EOS\n':

				# Chunkのリストを返す
				if len(chunks) > 0:

					# chunksをkeyでソートし、valueのみ取り出し
					sorted_tuple = sorted(chunks.items(), key=lambda x: x[0])
					yield list(zip(*sorted_tuple))[1]
					chunks.clear()

				else:
					yield []

			# 先頭が*の行は係り受け解析結果なので、Chunkを作成
			elif line[0] == '*':

				# Chunkのインデックス番号と係り先のインデックス番号取得
				cols = line.split(' ')
				idx = int(cols[1])
				dst = int(re.search(r'(.*?)D', cols[2]).group(1))

				# Chunkを生成（なければ）し、係り先のインデックス番号セット
				if idx not in chunks:
					chunks[idx] = Chunk()
				chunks[idx].dst = dst

				# 係り先のChunkを生成（なければ）し、係り元インデックス番号追加
				if dst != -1:
					if dst not in chunks:
						chunks[dst] = Chunk()
					chunks[dst].srcs.append(idx)

			# それ以外の行は形態素解析結果なので、Morphを作りChunkに追加
			else:

				# 表層形はtab区切り、それ以外は','区切りでバラす
				cols = line.split('\t')
				res_cols = cols[1].split(',')

				# Morph作成、リストに追加
				chunks[idx].morphs.append(
					Morph(
						cols[0],		# surface
						res_cols[6],	# base
						res_cols[0],	# pos
						res_cols[1]		# pos1
					)
				)

		raise StopIteration


# 係り受け解析
parse_neko()

# 結果ファイル作成
with open(fname_result, mode='w') as out_file:

	# 1文ずつリスト作成
	for chunks in neco_lines():

		# chunkを列挙
		for chunk in chunks:

			# 動詞を含むかチェック
			verbs = chunk.get_morphs_by_pos('動詞')
			if len(verbs) < 1:
				continue

			# 係り元に助詞を含むchunkを列挙
			chunks_include_prt = []
			for src in chunk.srcs:
				if len(chunks[src].get_kaku_prt()) > 0:
					chunks_include_prt.append(chunks[src])
			if len(chunks_include_prt) < 1:
				continue

			# chunkを助詞の辞書順でソート
			chunks_include_prt.sort(
				key=lambda x: x.get_kaku_prt()
			)

			# 出力
			out_file.write('{}\t{}\t{}\n'.format(
				verbs[0].base,		# 最左の動詞の基本系
				' '.join([chunk.get_kaku_prt() \
						for chunk in chunks_include_prt]),		# 助詞
				' '.join([chunk.normalized_surface() \
						for chunk in chunks_include_prt])		# 項
			))
```

####実行結果：

以下、結果の先頭部分です。

```console:result.txt（先頭部分）
生れる	で	どこで
つく	か が	生れたか 見当が
泣く	で	所で
する	は	事だけは
始める	で	ここで
見る	は を	吾輩は ものを
捕える	を	我々を
煮る	て	捕えて
食う	て	煮て
思う	から	なかったから
載せる	に	掌に
持ち上げる	て と	載せられて スーと
ある	が	感じが
落ちつく	で	上で
見る	て を	落ちついて 顔を
いう	と	人間と
見る	ものの	ものの
思う	と	ものだと
残る	が でも	感じが 今でも
```

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

###項（文節そのもの）の列挙
Chunkクラスに`get_kaku_prt()`を追加しました。文節の中の助詞を取得するもので、複数ある場合は格助詞を優先し、それでも複数ある場合は一番最後の助詞を返します。
それ以外のクラスや関数は[前問](http://qiita.com/segavvy/items/2f8387a5973a9966a12d)のままです。
今回は文節そのものを列挙する必要があるため、該当するchunkのリストを作り、それを先にソートしています。

###格フレームとは
調べてみたのですが、軽くググっただけでは深くは理解できませんでした^^;
どうやら、文を分析するための「格文法」という文法理論において、動詞と、それに関わる文節とのルールのようなものを格フレームと呼ぶようです。
詳しくは[ウィキペディア「格文法」](https://ja.wikipedia.org/wiki/%E6%A0%BC%E6%96%87%E6%B3%95)などを参照してください。

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

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