言語処理100本ノック-42:係り元と係り先の文節の表示

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)[「第5章: 係り受け解析」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch5)の[42本目「係り元と係り先の文節の表示」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec42)記録です。
係り元と係り先文節を出力するので、係り受けの本番といった感じです。ただ技術的には出力の仕方が少し変わるだけなので前回ノックとそんなに大差ないです。

# 参考リンク

|リンク|備考|
|:--|:--|
|[042.係り元と係り先の文節の表示.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/042.%E4%BF%82%E3%82%8A%E5%85%83%E3%81%A8%E4%BF%82%E3%82%8A%E5%85%88%E3%81%AE%E6%96%87%E7%AF%80%E3%81%AE%E8%A1%A8%E7%A4%BA.ipynb)|回答プログラムのGitHubリンク|
|[素人の言語処理100本ノック:42](https://qiita.com/segavvy/items/58894f76dba367b2925b)|多くのソース部分のコピペ元|
|[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というファイルに保存せよ．このファイルを用いて，以下の問に対応するプログラムを実装せよ．

### [42. 係り元と係り先の文節の表示](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec42)
> 係り元の文節と係り先の文節のテキストをタブ区切り形式ですべて抽出せよ．ただし，句読点などの記号は出力しないようにせよ．

# 回答
## 回答プログラム [042.係り元と係り先の文節の表示.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/042.%E4%BF%82%E3%82%8A%E5%85%83%E3%81%A8%E4%BF%82%E3%82%8A%E5%85%88%E3%81%AE%E6%96%87%E7%AF%80%E3%81%AE%E8%A1%A8%E7%A4%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.phrase = ''.join([morph.surface for morph in morphs if morph.pos!= '記号']) # 文節

# 係り元を代入し、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 si, sentence in enumerate(sentences):
    print('-----', si, '-----')
    for ci, chunk in enumerate(sentence):
        if chunk.dst != -1:
            print('{}:{}\t{}'.format(ci, chunk.phrase, sentence[chunk.dst].phrase))
    
    # 多いので制限
    if si > 5:
        break
```

## 回答解説
### Chunkの文節から記号除外
前回のChunkクラスと少し変えて、文節から記号を除外しています。

```python:
class Chunk:
    def __init__(self, morphs, dst):
        self.morphs = morphs
        self.srcs = []   # 係り元文節インデックス番号のリスト
        self.dst  = dst  # 係り先文節インデックス番号
        self.phrase = ''.join([morph.surface for morph in morphs if morph.pos!= '記号']) # 文節
```

### 出力部
「テキストをタブ区切り形式」というとタブ区切りテキストっぽいですが、全部出ても見ないし、文の区切りがあった方が見やすいので勝手に解釈してタブを`print`で出力しています。

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

### 出力結果(実行結果)
プログラム実行すると以下の結果が出力されます(6文のみ出力)。

```bash:出力結果
----- 0 -----
----- 1 -----
----- 2 -----
0:名前は	無い
1:まだ	無い
----- 3 -----
0:どこで	生れた
1:生れた	かとんと
2:かとんと	つかぬ
3:見当が	つかぬ
----- 4 -----
0:何でも	薄暗い
1:薄暗い	泣いて
2:じめじめ	泣いて
3:した所で	泣いて
4:ニャーニャー	泣いて
5:泣いて	記憶している
6:いた事だけは	記憶している
----- 5 -----
0:吾輩は	見た
1:ここで	始めて
2:始めて	人間という
3:人間という	ものを
4:ものを	見た
----- 6 -----
0:しかも	そうだ
1:あとで	聞くと
2:聞くと	そうだ
3:それは	そうだ
4:書生という	人間中で
5:人間中で	種族であった
6:一番	獰悪な
7:獰悪な	種族であった
8:種族であった	そうだ
```