なぜかRustで言語処理100本ノック ～第5章 前編～

Rustで[言語処理100本ノック](http://www.cl.ecei.tohoku.ac.jp/nlp100/)しています。

# 第5章: 係り受け解析
開発環境上にインストールした[cabocha](https://taku910.github.io/cabocha/)と、DOTファイルのPNG変換用に[Graphviz](http://www.graphviz.org/)を用いています。

## 40. 係り受け解析結果の読み込み（形態素）
> 形態素を表すクラスMorphを実装せよ．このクラスは表層形（surface），基本形（base），品詞（pos），品詞細分類1（pos1）をメンバ変数に持つこととする．さらに，CaboChaの解析結果（neko.txt.cabocha）を読み込み，各文をMorphオブジェクトのリストとして表現し，3文目の形態素列を表示せよ．

Rustにはクラスは無いので代わりに`struct`を使います。cabochaの出力結果については次のところで説明します。

```rust
pub struct Morph {
    surface: String,
    base: String,
    pos: String,
    pos1: String,
}

impl Morph {
    pub fn from_file(path: &Path) -> Result<Vec<Vec<Self>>> {
        let file = File::open(path)?;
        let br = BufReader::new(file);
        let mut lines = br.lines().filter(|line| if let Ok(line) = line { !line.starts_with('*') } else { false });
        let mut results = Vec::new();
        let mut buffer = Vec::new();
        while let Some(Ok(line)) = lines.next() {
            if line == "EOS" {
                if !buffer.is_empty() { results.push(buffer); }
                buffer = Vec::new();
            } else {
                let line = line.replace("\t", ",");
                let tmp: Vec<_> = line.split_terminator(',').collect();
                buffer.push(Self {
                    surface: tmp[0].to_string(),
                    base: tmp[7].to_string(),
                    pos: tmp[1].to_string(),
                    pos1: tmp[2].to_string(),
                });
            }
        }
        Ok(results)
    }
}
```

## 41. 係り受け解析結果の読み込み（文節・係り受け）
> 40に加えて，文節を表すクラスChunkを実装せよ．このクラスは形態素（Morphオブジェクト）のリスト（morphs），係り先文節インデックス番号（dst），係り元文節インデックス番号のリスト（srcs）をメンバ変数に持つこととする．さらに，入力テキストのCaboChaの解析結果を読み込み，１文をChunkオブジェクトのリストとして表現し，8文目の文節の文字列と係り先を表示せよ．第5章の残りの問題では，ここで作ったプログラムを活用せよ．

中身はほとんど上のと同じですが、ちょっと処理することが増えました。かなりの量なので、折りたたんでみました。

<details><summary>実装コード</summary><div>

```rust
pub struct Chunk {
    morphs: Vec<Morph>,
    dst: isize,
    srcs: Vec<usize>,
}

impl Chunk {
    pub fn from_file(path: &Path) -> Result<Vec<Vec<Self>>> {
        let file = File::open(path)?;
        let br = BufReader::new(file);
        let mut lines = br.lines();

        let mut results = Vec::new();
        let mut sentence = Vec::new();
        let mut chunk = Self::new();
        let mut pairs = Vec::new();
        while let Some(Ok(line)) = lines.next() {
            if line.starts_with("*") {          // Chunk開始
                if !chunk.morphs.is_empty() {   // 前回分のChunkの処理
                    sentence.push(chunk);
                    chunk = Self::new();
                }

                let line = line.split_whitespace().collect::<Vec<_>>();

                // エラー時も-1とすることで処理を続行
                let dst = match line[2].trim_end_matches("D").parse::<isize>() {
                    Ok(d) => d,
                    Err(_) => -1
                };
                chunk.dst = dst;
                if let Ok(src) = line[1].parse::<usize>() {
                    pairs.push((src, dst));
                }
            } else if line == "EOS" {           // 文の切れ目
                if !chunk.morphs.is_empty() {   // Chunk処理
                    sentence.push(chunk);
                    chunk = Self::new();
                }

                // 係り先が-1でない場合は係り先が存在している
                pairs.into_iter()
                     .filter(|(_, dst)| *dst != -1)
                     .for_each(|(src, dst)| sentence[dst as usize].srcs.push(src));
                pairs = Vec::new();

                if !sentence.is_empty() {       // 空文でない場合のみ登録
                    results.push(sentence);
                    sentence = Vec::new();
                }
            } else {
                chunk.morphs.push(Morph::from_line(&line));
            }
        }
        Ok(results)
    }
}
```

</div></details>

cabochaの出力は`-f1`オプションを付けるとmecabの出力と近い形式になり処理しやすくなるので、その形で見ると、

```
* ${src_index} ${dst_index}D ${主辞_index}/${機能語_index} ${score}
以下mecabと同じ形式
EOS
以下これの繰り返し
```

なぜ`dst`の部分に`D`が入っているのか不明ですが、形式自体は単純です。

## 42. 係り元と係り先の文節の表示
> 係り元の文節と係り先の文節のテキストをタブ区切り形式ですべて抽出せよ．ただし，句読点などの記号は出力しないようにせよ．

まずは`Chunk`単位で文字列化する関数を作りました。

```rust
pub fn get_text(&self) -> String {
    self.morphs.iter()
               .filter(|m| m.pos != "記号")
               .map(|m| m.surface.clone())
               .collect::<String>()
}
```

続いて、表示する部分の関数です。

```rust
pub fn show_src_and_dst(cabocha: &Vec<Vec<Chunk>>) {
    cabocha.iter().for_each(|sentence|
        sentence.iter().for_each(|chunk| if chunk.dst != -1 {
            let chunk_text = chunk.get_text();
            if !chunk_text.is_empty() { println!("{}\t{}", chunk_text,sentence[chunk.dst as usize].get_text()); }
        })
    );
}
```

## 43. 名詞を含む文節が動詞を含む文節に係るものを抽出
> 名詞を含む文節が，動詞を含む文節に係るとき，これらをタブ区切り形式で抽出せよ．ただし，句読点などの記号は出力しないようにせよ．

関数名が非常に悩ましいです。

```rust
pub fn noun_verb(cabocha: &Vec<Vec<Chunk>>) {
    cabocha.iter().for_each(|sentence| {
        sentence.iter()
                .filter(|chunk| {
                    chunk.dst != -1
                    && chunk.morphs.iter().find(|m| m.pos == "名詞").is_some()
                    && sentence[chunk.dst as usize].morphs.iter().find(|m| m.pos == "動詞").is_some()
                })
                .for_each(|chunk| println!("{}\t{}", chunk.get_text(), sentence[chunk.dst as usize].get_text()));
    });
}
```

## 44. 係り受け木の可視化
> 与えられた文の係り受け木を有向グラフとして可視化せよ．可視化には，係り受け木を[DOT言語](http://ja.wikipedia.org/wiki/DOT%E8%A8%80%E8%AA%9E)に変換し，[Graphviz](http://www.graphviz.org/)を用いるとよい．

Rust側でやることは、Chunk化された文をDOT言語の形に変換するだけです。

```rust
pub fn to_graph(sentence: &Vec<Chunk>, out_file: &Path) -> Result<()> {
    let file = OpenOptions::new().write(true).create(true).open(out_file)?;
    let mut bw = BufWriter::new(file);
    bw.write(b"digraph cabocha {\n")?;
    sentence.iter().map(|chunk| {
        bw.write(chunk.morphs.iter().map(|m| m.surface.clone()).collect::<String>().as_bytes())?;
        if chunk.dst != -1 {
            bw.write(b" -> ")?;
            bw.write(sentence[chunk.dst as usize].morphs.iter().map(|m| m.surface.clone()).collect::<String>().as_bytes())?;
            bw.write(b";\n")?;
        }
        Ok(())
    }).collect::<Result<()>>()?;
    bw.write(b"}")?;
    Ok(())
}
```