言語処理100本ノック-43:名詞を含む文節が動詞を含む文節に係るものを抽出

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)[「第5章: 係り受け解析」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch5)の[43本目「名詞を含む文節が動詞を含む文節に係るものを抽出」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec43)記録です。
前回ノックと比べて、出力する係り元および係り先に条件を加えただけで大きく変わらないです。

# 参考リンク

|リンク|備考|
|:--|:--|
|[043.名詞を含む文節が動詞を含む文節に係るものを抽出.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/05.%E4%BF%82%E3%82%8A%E5%8F%97%E3%81%91%E8%A7%A3%E6%9E%90/043.%E5%90%8D%E8%A9%9E%E3%82%92%E5%90%AB%E3%82%80%E6%96%87%E7%AF%80%E3%81%8C%E5%8B%95%E8%A9%9E%E3%82%92%E5%90%AB%E3%82%80%E6%96%87%E7%AF%80%E3%81%AB%E4%BF%82%E3%82%8B%E3%82%82%E3%81%AE%E3%82%92%E6%8A%BD%E5%87%BA.ipynb)|回答プログラムのGitHubリンク|
|[素人の言語処理100本ノック:43](https://qiita.com/segavvy/items/30d14259aa350f88852f)|多くのソース部分のコピペ元|
|[CaboCha公式](https://taku910.github.io/cabocha/)|最初に見ておくCaboChaのページ|

# 環境
CRF++とCaboChaはインストールしたのが昔すぎてインストール方法忘れました。全然更新されていないパッケージなので、環境再構築もしていません。CaboChaをWindowsで使おうと思い、挫折した記憶だけはあります。確か64bitのWindowsで使えなかった気がします(記憶が曖昧だし私の技術力の問題も多分にあるかも)。

|種類|バージョン|内容|
|:--|:--|:--|
|OS|Ubuntu18.04.01 LTS|仮想で動かしています|
|pyenv|1.2.16|複数Python環境を使うことがあるのでpyenv使っています|
|Python|3.8.1|pyenv上でpython3.8.1を使っています<br>パッケージはvenvを使って管理しています|
|Mecab|0.996-5|apt-getでインストール|
|CRF++|0.58|昔すぎてインストール方法忘れました(多分`make install`)|
|CaboCha|0.69|昔すぎてインストール方法忘れました(多分`make install`)|

# [第5章: 係り受け解析](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch5)
## 学習内容
> 『吾輩は猫である』に係り受け解析器CaboChaを適用し，係り受け木の操作と統語的な分析を体験します．
>> クラス, 係り受け解析, [CaboCha](https://code.google.com/p/cabocha/), 文節, 係り受け, 格, 機能動詞構文, 係り受けパス, [Graphviz](http://www.graphviz.org/)

## ノック内容
> 夏目漱石の小説『吾輩は猫である』の文章（[neko.txt](http://www.cl.ecei.tohoku.ac.jp/nlp100/data/neko.txt)）をCaboChaを使って係り受け解析し，その結果をneko.txt.cabochaというファイルに保存せよ．このファイルを用いて，以下の問に対応するプログラムを実装せよ．

### [43. 名詞を含む文節が動詞を含む文節に係るものを抽出](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec43)
> 名詞を含む文節が，動詞を含む文節に係るとき，これらをタブ区切り形式で抽出せよ．ただし，句読点などの記号は出力しないようにせよ．

# 回答
## 回答プログラム [043.名詞を含む文節が動詞を含む文節に係るものを抽出.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/05.%E4%BF%82%E3%82%8A%E5%8F%97%E3%81%91%E8%A7%A3%E6%9E%90/043.%E5%90%8D%E8%A9%9E%E3%82%92%E5%90%AB%E3%82%80%E6%96%87%E7%AF%80%E3%81%8C%E5%8B%95%E8%A9%9E%E3%82%92%E5%90%AB%E3%82%80%E6%96%87%E7%AF%80%E3%81%AB%E4%BF%82%E3%82%8B%E3%82%82%E3%81%AE%E3%82%92%E6%8A%BD%E5%87%BA.ipynb)

```python
import re

# 区切り文字
separator = re.compile('\t|,')

# 係り受け
dependancy = re.compile(r'''(?:\*\s\d+\s) # キャプチャ対象外
                            (-?\d+)       # 数字(係り先)
                          ''', re.VERBOSE)

class Morph:
    def __init__(self, line):
        
        #タブとカンマで分割
        cols = separator.split(line)
        
        self.surface = cols[0] # 表層形(surface)
        self.base = cols[7]    # 基本形(base)
        self.pos = cols[1]     # 品詞(pos)
        self.pos1 = cols[2]    # 品詞細分類1(pos1)

class Chunk:
    def __init__(self, morphs, dst):
        self.morphs = morphs
        self.srcs = []   # 係り元文節インデックス番号のリスト
        self.dst  = dst  # 係り先文節インデックス番号
        
        self.verb = False
        self.noun = False
        self.phrase = ''
        
        for morph in morphs:            
            # 記号以外の場合文節作成
            if morph.pos != '記号':
                self.phrase += morph.surface
            if morph.pos == '動詞':
                self.verb = True
            if morph.pos == '名詞':
                self.noun = True

# 係り元を代入し、Chunkリストを文のリストを追加
def append_sentence(chunks, sentences):
    
    # 係り元を代入
    for i, chunk in enumerate(chunks):
        if chunk.dst != -1:
            chunks[chunk.dst].srcs.append(i)
    sentences.append(chunks)
    return sentences, []

morphs = []
chunks = []
sentences = []

with open('./neko.txt.cabocha') as f:
    
    for line in f:
        dependancies = dependancy.match(line)
        
        # EOSまたは係り受け解析結果でない場合
        if not (line == 'EOS\n' or dependancies):
            morphs.append(Morph(line))
            
        # EOSまたは係り受け解析結果で、形態素解析結果がある場合
        elif len(morphs) > 0:
            chunks.append(Chunk(morphs, dst))
            morphs = []
       
        # 係り受け結果の場合
        if dependancies:
            dst = int(dependancies.group(1))
        
        # EOSで係り受け結果がある場合
        if line == 'EOS\n' and len(chunks) > 0:
            sentences, chunks = append_sentence(chunks, sentences)

for i, sentence in enumerate(sentences):
    for chunk in sentence:
        if chunk.dst != -1 and \
           chunk.noun and \
           sentence[chunk.dst].verb:
            print('{}\t{}'.format(chunk.phrase, sentence[chunk.dst].phrase))
    
    # 多いので制限
    if i > 50:
        break
```

## 回答解説
### 文節に名詞・動詞を含むか
前回ノックからChunkクラスを変更して文節に名詞・動詞を含むかをクラス変数に定義しています。`for`ループで処理しているので、リスト内包表記での文節の文字列作成をやめました。

```python:
class Chunk:
    def __init__(self, morphs, dst):
        self.morphs = morphs
        self.srcs = []   # 係り元文節インデックス番号のリスト
        self.dst  = dst  # 係り先文節インデックス番号
        
        self.verb = False
        self.noun = False
        self.phrase = ''
        
        for morph in morphs:            
            # 記号以外の場合文節作成
            if morph.pos != '記号':
                self.phrase += morph.surface
            if morph.pos == '動詞':
                self.verb = True
            if morph.pos == '名詞':
                self.noun = True
```

### 出力部
あとは`if`の条件分岐で出力対象を絞り込むだけです。

```python:
for i, sentence in enumerate(sentences):
    for chunk in sentence:
        if chunk.dst != -1 and \
           chunk.noun and \
           sentence[chunk.dst].verb:
            print('{}\t{}'.format(chunk.phrase, sentence[chunk.dst].phrase))
```

### 出力結果(実行結果)
プログラム実行すると以下の結果が出力されます。多いので一部だけ出力しています。

```bash:出力結果
どこで	生れた
かとんと	つかぬ
見当が	つかぬ
した所で	泣いて
いた事だけは	記憶している
吾輩は	見た
ここで	始めて
ものを	見た
あとで	聞くと
我々を	捕えて
掌に	載せられて
スーと	持ち上げられた
時	フワフワした
感じが	あったばかりである
上で	落ちついて
顔を	見たのが
ものの	見始であろう
ものだと	思った
感じが	残っている
今でも	残っている
第一毛をもって	装飾されべきはずの
顔が	つるつるして
その後	逢ったが
猫にも	逢ったが
一度も	出会わし
真中が	突起している
中から	吹く
煙を	吹く
咽せぽくて	弱った
人間の	飲む
事は	知った
頃	知った
裏で	坐って
心持に	坐って
速力で	運転し始めた
書生が	動くのか
動くのか	動くのか
自分だけが	動くのか
動くのか	分らないが
眼が	廻る
胸が	悪くなる
音が	して
眼から	出た
火が	出た
それまでは	記憶しているが
記憶しているが	分らない
あとは	分らない
事やら	分らない
気が	付いて
書生は	いない
たくさん	おった
兄弟が	見えぬ
一疋も	見えぬ
母親さえ	隠してしまった
姿を	隠してしまった
所とは	違って
眼を	明いていられぬくらいだ
吾輩は	棄てられたのである
上から	棄てられたのである
急に	棄てられたのである
中へ	棄てられたのである
思いで	這い出すと
笹原を	這い出すと
向うに	ある
池が	ある
吾輩は	見た
前に	坐って
分別も	出ない
書生がまた	来てくれるかと
迎に	来てくれるかと
ニャーと	やって
誰も	来ない
池の上を	渡って
風が	渡って
日が	かかる
暮れ	かかる
腹が	減って来た
非常に	減って来た
食物の	ある
所まで	ある
決心を	して
池を	廻り始めた
左りに	廻り始めた
そこを	我慢して
我慢して	這って行くと
無理やりに	這って行くと
事で	出た
所へ	出た
ここへ	這入ったら
竹垣の	崩れた
穴から	もぐり込んだ
邸内に	もぐり込んだ
もので	餓死したかも知れんのである
竹垣が	破れていなかったなら
吾輩は	餓死したかも知れんのである
路傍に	餓死したかも知れんのである
蔭とは	云った
穴は	なっている
今日に	至るまで
吾輩が	訪問する
三毛を	訪問する
通路に	なっている
邸へは	忍び込んだものの
うちに	暗くなる
腹は	減る
雨が	降って来るという
始末で	出来なくなった
猶予が	出来なくなった
方へ	あるいて行く
方へと	あるいて行く
今から	考えると
時は	おったのだ
内に	這入って
ここで	遭遇したのである
吾輩は	遭遇したのである
人間を	見るべき
機会に	遭遇したのである
第一に	逢ったのが
これは	抛り出した
書生より	見るや
方で	見るや
吾輩を	見るや
否やいきなり	つかんで
頸筋を	つかんで
表へ	抛り出した
```