言語処理100本ノック 2015の挑戦記録です。環境はUbuntu 16.04 LTS + Python 3.5.2 :: Anaconda 4.1.1 (64-bit)です。過去のノックの一覧はこちらからどうぞ。
第5章: 係り受け解析
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をCaboChaを使って係り受け解析し,その結果をneko.txt.cabochaというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.
###43. 名詞を含む文節が動詞を含む文節に係るものを抽出
名詞を含む文節が,動詞を含む文節に係るとき,これらをタブ区切り形式で抽出せよ.ただし,句読点などの記号は出力しないようにせよ.
####出来上がったコード:
# coding: utf-8
import CaboCha
import re
fname = 'neko.txt'
fname_parsed = 'neko.txt.cabocha'
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 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()
# 1文ずつリスト作成
for chunks in neco_lines():
# 係り先があるものを列挙
for chunk in chunks:
if chunk.dst != -1:
# 係り元に名詞があるか、係り先に動詞があるかチェック
if chunk.chk_pos('名詞') and chunks[chunk.dst].chk_pos('動詞'):
# 記号を除いた表層形をチェック、空なら除外
src = chunk.normalized_surface()
dst = chunks[chunk.dst].normalized_surface()
if src != '' and dst != '':
print('{}\t{}'.format(src, dst))
####実行結果:
以下、結果の先頭部分です。
どこで 生れたか
見当が つかぬ
所で 泣いていた
ニャーニャー 泣いていた
事だけは 記憶している
吾輩は 見た
ここで 始めて
ものを 見た
我々を 捕えて
掌に 載せられて
スーと 持ち上げられた
時 フワフワした
感じが あったばかりである
上で 落ちついて
顔を 見たのが
人間と いう
ものだと 思った
感じが 残っている
今でも 残っている
結果全体はGitHubにアップしておきました。
###品詞のチェック
名詞や動詞が含まれているかどうかのチェックは、前問で作ったChunkクラスにchk_pos()
を追加し、そこで判定しています。
それ以外のクラスや関数は前問のままです。
44本目のノックは以上です。誤りなどありましたら、ご指摘いただけますと幸いです。
実行結果には、100本ノックで用いるコーパス・データで配布されているデータの一部が含まれます。この第5章で用いているデータは青空文庫で公開されている夏目漱石の長編小説『吾輩は猫である』が元になっています。