言語処理100本ノック 2020 第4章 前半
第4章 形態素解析の前半(30-34まで)解説書きます。
ますます言語処理っぽくなってきた。MeCabとお友達になる章。
ここだけの話、「吾輩は猫である」読んだことなかった。
ファイルの最後までスクロールして、衝撃のオチだった。
目次
準備
「吾輩は猫である」(夏目漱石)の全文がneko.txtで渡されるので、
コマンドラインでMeCabを使って形態素解析をかけてその結果をneko.txt.mecabとして保存する。
私の環境はMacOS Catalina(10.15.4)なので、まずはMeCabのインストールをして、
Pythonから呼び出せるようにする。
$ brew install mecab $ brew install mecab-ipadic $ pip install mecab-python3
続いて、形態素解析をかけてその結果ファイルneko.txt.mecab
を得る
$ mecab neko.txt > neko.txt.mecab
neko.txt.mecabの中身は以下の通り(結構長いので最初の方だけ。)
一 名詞,数,*,*,*,*,一,イチ,イチ EOS EOS 記号,空白,*,*,*,*, , , 吾輩 名詞,代名詞,一般,*,*,*,吾輩,ワガハイ,ワガハイ は 助詞,係助詞,*,*,*,*,は,ハ,ワ 猫 名詞,一般,*,*,*,*,猫,ネコ,ネコ で 助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ ある 助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル 。 記号,句点,*,*,*,*,。,。,。 EOS 名前 名詞,一般,*,*,*,*,名前,ナマエ,ナマエ は 助詞,係助詞,*,*,*,*,は,ハ,ワ まだ 副詞,助詞類接続,*,*,*,*,まだ,マダ,マダ 無い 形容詞,自立,*,*,形容詞・アウオ段,基本形,無い,ナイ,ナイ 。 記号,句点,*,*,*,*,。,。,。 EOS
行ごとの出力形式は、以下の通りになっている。
表層形\t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音
詳しくは、以下のドキュメントを参照
30. 形態素解析結果の読み込み
結果を読み込まないと何にも使えない。4章で避けて通れない問題。
MeCabの出力形式を見ると、Tabとカンマが同居しているので、pandas使いにくい。
素直にファイルオープンして、一行ずつ読み込みながら改行コードやTabを処理していく。
なお、行の終わりにEOS
が混ざっているので、こいつは含めてはいけない。
行の終わりの目印として活用させてもらった。
次の問題からはこの部分を関数化して使う。
解答
# coding:utf-8 # mecab neko.txt > neko.txt.mecab # 表層形\t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音 path = './neko.txt.mecab' neko = [] sentence = [] with open(path) as f: for line in f: # [表層系, それ以外]として一行の中身をリスト化 tmp_line = line.rstrip('\n').split('\t') # EOSの部分で一文とカウント。EOSだった場合はsentenceに何も入らない if len(tmp_line) == 1 and len(sentence) != 0: neko.append(sentence) sentence = [] elif len(tmp_line) == 2: # 品詞以降の部分をカンマ区切りする tmp = tmp_line[1].split(',') morpheme = {'surface': tmp_line[0], 'base': tmp[6], 'pos': tmp[0], 'pos1': tmp[1]} sentence.append(morpheme) print(neko)
結果は以下の通り。全部出すと長いので最後の1文だけ抜粋。
ネタバレではない(多分)
$ python CH4-30.py [{'surface': 'ありがたい', 'base': 'ありがたい', 'pos': '形容詞', 'pos1': '自立'}, {'surface': 'ありがたい', 'base': 'ありがたい', 'pos': '形容詞', 'pos1': '自立'}, {'surface': '。', 'base': '。', 'pos': '記号', 'pos1': '句点'}]]
31. 動詞
最初の問題で各単語を辞書に入れたので、品詞(pos)と表層(surface)に注目し、
二重ループで抜いていくだけ。
解答
# coding:utf-8 def neko_load(): path = './neko.txt.mecab' neko = [] sentence = [] with open(path) as f: for line in f: # [表層系, それ以外]として一行の中身をリスト化 tmp_line = line.rstrip('\n').split('\t') # EOSの部分で一文とカウント。EOSだった場合はsentenceに何も入らない if len(tmp_line) == 1 and len(sentence) != 0: neko.append(sentence) sentence = [] elif len(tmp_line) == 2: # 品詞以降の部分をカンマ区切りする tmp = tmp_line[1].split(',') morpheme = {'surface': tmp_line[0], 'base': tmp[6], 'pos': tmp[0], 'pos1': tmp[1]} sentence.append(morpheme) return(neko) def main(): cat = neko_load() verb = [] for line in cat: for word in line: if word['pos'] == '動詞': # print(word['pos'], word['surface']) verb.append(word['surface']) print(verb) if __name__ == "__main__": main()
出力はこんな感じ。長いので最初の7つだけ。
$ python CH4-31.py ['生れ', 'つか', 'し', '泣い', 'し', 'いる', '始め']
32. 動詞の原形
31.とほぼ一緒。今度は品詞(pos)と原型(base)に注目し、
二重ループで抜いてあげる。
解答
# coding:utf-8 def neko_load(): path = './neko.txt.mecab' neko = [] sentence = [] with open(path) as f: for line in f: # [表層系, それ以外]として一行の中身をリスト化 tmp_line = line.rstrip('\n').split('\t') # EOSの部分で一文とカウント。EOSだった場合はsentenceに何も入らない if len(tmp_line) == 1 and len(sentence) != 0: neko.append(sentence) sentence = [] elif len(tmp_line) == 2: # 品詞以降の部分をカンマ区切りする tmp = tmp_line[1].split(',') morpheme = {'surface': tmp_line[0], 'base': tmp[6], 'pos': tmp[0], 'pos1': tmp[1]} sentence.append(morpheme) return(neko) def main(): cat = neko_load() verb = [] for line in cat: for word in line: if word['pos'] == '動詞': verb.append(word['base']) print(verb) if __name__ == "__main__": main()
出力はこんな感じ。長いのでこれも最初の7つだけ。
$ python CH4-32.py ['生れる', 'つく', 'する', '泣く', 'する', 'いる', '始める']
33. 「AのB」
言語処理的には、俺のベーカリー
のように、名詞 + 助詞(の) + 名詞
になっているものを抜き出す。
私はPython3.8.2でやっているので、満を持してセイウチ演算子を使ってみた。
ポイント
if ( n := len(list) ) == 3:
解答
セイウチ演算子を、リストの長さを取ってそれを変数に格納→使用する部分で発動した。
これ便利だわ。
# coding:utf-8 def neko_load(): path = './neko.txt.mecab' neko = [] sentence = [] with open(path) as f: for line in f: # [表層系, それ以外]として一行の中身をリスト化 tmp_line = line.rstrip('\n').split('\t') # EOSの部分で一文とカウント。EOSだった場合はsentenceに何も入らない if len(tmp_line) == 1 and len(sentence) != 0: neko.append(sentence) sentence = [] elif len(tmp_line) == 2: # 品詞以降の部分をカンマ区切りする tmp = tmp_line[1].split(',') morpheme = {'surface': tmp_line[0], 'base': tmp[6], 'pos': tmp[0], 'pos1': tmp[1]} sentence.append(morpheme) return(neko) def main(): cat = neko_load() a_no_b = [] # print(cat[0]) for line in cat: if (n := len(line)) >= 3: for i in range(n - 2): if line[i]['pos'] == '名詞' and (line[i + 1]['pos'] == '助詞' and line[i + 1]['surface'] == 'の') and line[i + 2]['pos'] == '名詞': a_no_b.append(line[i]['surface'] + 'の' + line[i + 2]['surface']) print(a_no_b) if __name__ == "__main__": main()
出力は以下の通り。長いので最初だけ。
$ python CH4-33.py ['彼の掌', '掌の上', '書生の顔', 'はずの顔', '顔の真中', '穴の中', '書生の掌
参考記事
34. 名詞の連節
連続して名詞が出てくるところを抜いてくる。名詞単体だけのところは除外 ひたすらバグを出しまくった。ちょっときつかった。
解答スクリプト
# coding:utf-8 # coding:utf-8 def neko_load(): path = './neko.txt.mecab' neko = [] sentence = [] with open(path) as f: for line in f: # [表層系, それ以外]として一行の中身をリスト化 tmp_line = line.rstrip('\n').split('\t') # EOSの部分で一文とカウント。EOSだった場合はsentenceに何も入らない if len(tmp_line) == 1 and len(sentence) != 0: neko.append(sentence) sentence = [] elif len(tmp_line) == 2: # 品詞以降の部分をカンマ区切りする tmp = tmp_line[1].split(',') morpheme = {'surface': tmp_line[0], 'base': tmp[6], 'pos': tmp[0], 'pos1': tmp[1]} sentence.append(morpheme) return(neko) def main(): cat = neko_load() # print(cat) ans = [] for line in cat: if (n := len(line)) >= 2: tmp = '' count = 0 # 名詞が連続している部分を探す for i in range(n): if line[i]['pos'] == '名詞': tmp += line[i]['surface'] count += 1 # 名詞が2つ以上続いた時だけ答えに入れる elif line[i]['pos'] != '名詞' and tmp != '' and count >= 2: ans.append(tmp) tmp = '' count = 0 # 名詞がひとつ来てその次名詞以外だった場合はリセット elif line[i]['pos'] != '名詞' and tmp != '' and count == 1: tmp = '' count = 0 print(ans) if __name__ == "__main__": main()
出力は以下の通り。最初だけ抜粋。
['人間中', '一番獰悪', '時妙', '一毛', 'その後猫', '一度', 'ぷうぷうと煙'