稀によくあるKNPのはまりどころ

# はじめに

※本記事の内容は下記のバージョン時点のものです

* JUMAN++: 2.0.0-rc3
* KNP: 4.19

業務でKNPを使用しているのですが、扱うテキストの性質上あまり日本語文的でないテキストを入力として与えることが少なからずあります。
その際に正しく解析結果が得られない場合があったため、それらを共有します。

なお、[KNPのFAQ](http://nlp.ist.i.kyoto-u.ac.jp/index.php?KNP%2FFAQ)では
> **半角スペースを含む文が解析できないのですが。**
> JUMAN/KNPでは、入力文が全角文字で記述されていることを前提としています。半角スペースなどの半角文字は全角文字に変換してから入力してください。

という項目がありますが、現時点では(解析結果への影響がある可能性はありますが)例として挙げられている半角スペースも含めほとんどの半角文字の場合は含まれていても正常に結果を返してくれます。
~~なので実はそもそも全角文字入力前提ということを、この記事を書いているときに知りました……~~

# ERROR:Cannot make mrphが出る入力
* 半角`+`
* 半角`*`

上記の文字を含む入力を与えると、下記の結果が返されます。

下記は`てすと*`を入力としたときの結果です。

```
てすと*
;; Invalid input <* * * 特殊 1 記号 5 * 0 * 0 NIL
> !
# S-ID:1 KNP:4.19-CF1.1 DATE:2020/02/07 SCORE:0.00000 ERROR:Cannot make mrph
EOS
```

Web上から取ってきたテキストや、数式を含む可能性があるテキストを入力として想定する場合は注意が必要です。

これについては事前に全角文字に置換するか、解析に失敗した場合はその旨を返すようにするなど比較的対処が容易です。

# Segmentation faultが出る入力
KNPには[照応解析オプション](http://nlp.ist.i.kyoto-u.ac.jp/index.php?KNP%2F%E7%85%A7%E5%BF%9C%E8%A7%A3%E6%9E%90%E7%B5%90%E6%9E%9C%E6%9B%B8%E5%BC%8F)`-anaphora`というものがあります。

これは正常であれば述語項構造等を結果に付与してくれるオプションで、下記のように述語項や照応関係の情報を得ることができます。

* -anaphoraオプションあり

```
私は空腹なのでカレーを食べた
# S-ID:3 KNP:4.19-CF1.1 DATE:2020/02/08 SCORE:-37.23823
* 3D <文頭><一人称><SM-主体><SM-人><ハ><助詞><体言><一文字漢字><係:未格><提題><区切:3-5><主題表現><格要素><連用要素><正規化代表表記:私/わたし><主辞代表表記:私/わたし>
+ 3D <文頭><一人称><SM-主体><SM-人><ハ><助詞><体言><一文字漢字><係:未格><提題><区切:3-5><主題表現><格要素><連用要素><名詞項候補><先行詞候補><正規化代表表記:私/わたし><照応詞候補:私><解析格:ガ><EID:5>
私 わたし 私 名詞 6 普通名詞 1 * 0 * 0 "代表表記:私/わたし カテゴリ:人 漢字読み:訓" <代表表記:私/わたし><カテゴリ:人><漢字読み:訓><正規化代表表記:私/わたし><文頭><一人称><漢字><かな漢字><名詞相当語><自立><内容語><タグ単位始><文節始><文節主辞>
は は は 助詞 9 副助詞 2 * 0 * 0 NIL <かな漢字><ひらがな><付属>
* 3D <用言:形><係:連用><レベル:B+><区切:3-5><ID:〜ので><提題受:20><連用要素><連用節><状態述語><正規化代表表記:空腹だ/くうふくだ><主辞代表表記:空腹だ/くうふくだ>
+ 3D <用言:形><係:連用><レベル:B+><区切:3-5><ID:〜ので><提題受:20><連用要素><連用節><状態述語><節機能-理由><正規化代表表記:空腹だ/くうふくだ><用言代表表記:空腹だ/くうふくだ><時制-無時制><格解析結果:空腹だ/くうふくだ:形0:ガ/U/-/-/-/-;ニ/U/-/-/-/-;ヨリ/U/-/-/-/-><EID:6><項構造:空腹だ/くうふくだ:形0:ガ/O/私/5>
空腹な くうふくな 空腹だ 形容詞 3 * 0 ナ形容詞 21 ダ列基本連体形 3 "代表表記:空腹だ/くうふくだ ドメイン:料理・食事 反義:形容詞:満腹だ/まんぷくだ" <代表表記:空腹だ/くうふくだ><ドメイン:料理・食事><反義:形容詞:満腹だ/まんぷくだ><正規化代表表記:空腹だ/くうふくだ><かな漢字><活用語><自立><内容語><タグ単位始><文節始><文節主辞>
ので ので のだ 助動詞 5 * 0 ナ形容詞 21 ダ列タ系連用テ形 12 NIL <かな漢字><ひらがな><活用語><付属>
* 3D <ヲ><助詞><体言><係:ヲ格><区切:0-0><格要素><連用要素><正規化代表表記:カレー/かれー><主辞代表表記:カレー/かれー>
+ 3D <ヲ><助詞><体言><係:ヲ格><区切:0-0><格要素><連用要素><名詞項候補><先行詞候補><正規化代表表記:カレー/かれー><照応詞候補:カレー><解析格:ヲ><C用;【カレー】;=;2;1;9.99:1(2文前):1文節><共参照><COREFER_ID:1><REFERRED:2-1><EID:1>
カレー かれー カレー 名詞 6 普通名詞 1 * 0 * 0 "代表表記:カレー/かれー ドメイン:料理・食事 カテゴリ:人工物-食べ物" <代表表記:カレー/かれー><ドメイン:料理・食事><カテゴリ:人工物-食べ物><正規化代表表記:カレー/かれー><記英数カ><カタカナ><名詞相当語><自立><内容語><タグ単位始><文節始><固有キー><文節主辞>
を を を 助詞 9 格助詞 1 * 0 * 0 NIL <かな漢字><ひらがな><付属>
* -1D <文末><時制-過去><用言:動><レベル:C><区切:5-5><ID:（文末）><提題受:30><主節><動態述語><正規化代表表記:食べる/たべる><主辞代表表記:食べる/たべる>
+ -1D <文末><時制-過去><用言:動><レベル:C><区切:5-5><ID:（文末）><提題受:30><主節><動態述語><正規化代表表記:食べる/たべる><用言代表表記:食べる/たべる><主題格:一人称優位><格関係0:ガ:私><格関係2:ヲ:カレー><格解析結果:食べる/たべる:動0:ガ/N/私/0/0/3;ヲ/C/カレー/2/0/3;ニ/U/-/-/-/-;ヘ/U/-/-/-/-;ヨリ/U/-/-/-/-><EID:7><項構造:食べる/たべる:動0:ガ/N/私/5;ヲ/C/カレー/1>
食べた たべた 食べる 動詞 2 * 0 母音動詞 1 タ形 10 "代表表記:食べる/たべる ドメイン:料理・食事" <代表表記:食べる/たべる><ドメイン:料理・食事><正規化代表表記:食べる/たべる><文末><表現文末><かな漢字><活用語><自立><内容語><タグ単位始><文節 始><文節主辞>
EOS
```

* -anaphoraオプションなし

```
私は空腹なのでカレーを食べた
# S-ID:1 KNP:4.19-CF1.1 DATE:2020/02/08 SCORE:-37.23823
* 3D <文頭><一人称><SM-主体><SM-人><ハ><助詞><体言><一文字漢字><係:未格><提題><区切:3-5><主題表現><格要素><連用要素><正規化代表表記:私/わたし><主辞代表表記:私/わたし>
+ 3D <文頭><一人称><SM-主体><SM-人><ハ><助詞><体言><一文字漢字><係:未格><提題><区切:3-5><主題表現><格要素><連用要素><名詞項候補><先行詞候補><正規化代表表記:私/わたし><解析格:ガ>
私 わたし 私 名詞 6 普通名詞 1 * 0 * 0 "代表表記:私/わたし カテゴリ:人 漢字読み:訓" <代表表記:私/わたし><カテゴリ:人><漢字読み:訓><正規化代表表記:私/わたし><文頭><一人称><漢字><かな漢字><名詞相当語><自立><内容語><タグ単位始><文節始><文節主辞>
は は は 助詞 9 副助詞 2 * 0 * 0 NIL <かな漢字><ひらがな><付属>
* 3D <用言:形><係:連用><レベル:B+><区切:3-5><ID:〜ので><提題受:20><連用要素><連用節><状態述語><正規化代表表記:空腹だ/くうふくだ><主辞代表表記:空腹だ/くうふくだ>
+ 3D <用言:形><係:連用><レベル:B+><区切:3-5><ID:〜ので><提題受:20><連用要素><連用節><状態述語><節機能-理由><正規化代表表記:空腹だ/くうふくだ><用言代表表記:空腹だ/くうふくだ><時制-無時制><格解析結果:空腹だ/くうふくだ:形0:ガ/U/-/-/-/-;ニ/U/-/-/-/-;ヨリ/U/-/-/-/->
空腹な くうふくな 空腹だ 形容詞 3 * 0 ナ形容詞 21 ダ列基本連体形 3 "代表表記:空腹だ/くうふくだ ドメイン:料理・食事 反義:形容詞:満腹だ/まんぷくだ" <代表表記:空腹だ/くうふくだ><ドメイン:料理・食事><反義:形容詞:満腹だ/まんぷくだ><正規化代表表記:空腹だ/くうふくだ><かな漢字><活用語><自立><内容語><タグ単位始><文節始><文節主辞>
ので ので のだ 助動詞 5 * 0 ナ形容詞 21 ダ列タ系連用テ形 12 NIL <かな漢字><ひらがな><活用語><付属>
* 3D <ヲ><助詞><体言><係:ヲ格><区切:0-0><格要素><連用要素><正規化代表表記:カレー/かれー><主辞代表表記:カレー/かれー>
+ 3D <ヲ><助詞><体言><係:ヲ格><区切:0-0><格要素><連用要素><名詞項候補><先行詞候補><正規化代表表記:カレー/かれー><解析格:ヲ>
カレー かれー カレー 名詞 6 普通名詞 1 * 0 * 0 "代表表記:カレー/かれー ドメイン:料理・食事 カテゴリ:人工物-食べ物" <代表表記:カレー/かれー><ドメイン:料理・食事><カテゴリ:人工物-食べ物><正規化代表表記:カレー/かれー><記英数カ><カタカナ><名詞相当語><自立><内容語><タグ単位始><文節始><固有キー><文節主辞>
を を を 助詞 9 格助詞 1 * 0 * 0 NIL <かな漢字><ひらがな><付属>
* -1D <文末><時制-過去><用言:動><レベル:C><区切:5-5><ID:（文末）><提題受:30><主節><動態述語><正規化代表表記:食べる/たべる><主辞代表表記:食べる/たべる>
+ -1D <文末><時制-過去><用言:動><レベル:C><区切:5-5><ID:（文末）><提題受:30><主節><動態述語><正規化代表表記:食べる/たべる><用言代表表記:食べる/たべる><主題格:一人称優位><格関係0:ガ:私><格関係2:ヲ:カレー><格解析結果:食べる/たべる:動0:ガ/N/私/0/0/1;ヲ/C/カレー/2/0/1;ニ/U/-/-/-/-;ヘ/U/-/-/-/-;ヨリ/U/-/-/-/->
食べた たべた 食べる 動詞 2 * 0 母音動詞 1 タ形 10 "代表表記:食べる/たべる ドメイン:料理・食事" <代表表記:食べる/たべる><ドメイン:料理・食事><正規化代表表記:食べる/たべる><文末><表現文末><かな漢字><活用語><自立><内容語><タグ単位始><文節 始><文節主辞>
EOS
```

このオプションをつけており、かつ下記を満たした場合に`Segmentation fault`が発生します。

**半角スラッシュ`/`を含む文を同一のKNPのprocessに対して2回入力として与えた**

※入力文が`a/1`, `あ/あ`等の場合は2回入力として与えても問題なかったので、厳密には条件はもう少し複雑そうですがおおむね上記の認識でいるのが安全なように思います。

コンソール(wsl)で試した結果は下記のようになりました。
ただし、macのterminal上では`Segmentation fault (コアダンプ)`ではなく`Segmentation fault: 11`だったので細かいエラーの内容は環境に依存する可能性があります。

```
$ jumanpp | knp -tab -anaphora
てすと/てすと
# S-ID:1 KNP:4.19-CF1.1 DATE:2020/02/08 SCORE:-23.04048
* -1D <文頭><文末><ト><助詞><体言><用言:判><体言止><レベル:C><区切:5-5><ID:（文末）><並キ:述:&ST:3.0><提題受:30><主節><状態述語><正規化代表表記:てすと/てすと+てす/てす><主辞代表表記:てす/てす><並列類似度:-100.000>
+ 1D <文節内><係:文節内><文頭><体言><名詞項候補><先行詞候補><正規化代表表記:てすと/てすと><照応詞候補:てすと><EID:0>
てすと てすと てすと 名詞 6 普通名詞 1 * 0 * 0 "未知語:ひらがな 品詞推定:名詞 疑似代表表記 代表表記:てすと/てすと 品詞変更:てすと-てすと-てすと-15-1-0-0" <未知語><品詞推定:名詞><疑似代表表記><代表表記:てすと/てすと><正規化代表表記:てすと/てすと><文頭><品詞変更:てすと-てすと-てすと-15-1-0-0-"未知語:ひらがな 品詞推定:名詞 疑似代表表記 代表表記:てすと/てすと"><品曖-その他><かな漢字><ひらがな><名詞相当語><自立><内容語><タグ単位始><文節始>
+ 2D <文節内><係:文節内><体言><名詞項候補><先行詞候補><照応詞候補:てすと/><EID:1>
/ / / 特殊 1 記号 5 * 0 * 0 NIL <記英数カ><英記号><記号><自立><複合←><内容語><タグ単位始>
+ -1D <文末><ト><助詞><体言><用言:判><体言止><レベル:C><区切:5-5><ID:（文末）><並キ:述:&ST:3.0><提題受:30><主節><状態述語><判定詞><名詞項候補><先行詞候補><正規化代表表記:てす/てす><用言代表表記:てす/てす><時制-無時制><照応詞候補:てすと/ てす><格解析結果:てす/てす:判0:ガ/U/-/-/-/-><EID:2><項構造:てす/てす:判0>
てす てす てす 名詞 6 普通名詞 1 * 0 * 0 "未知語:ひらがな 品詞推定:名詞 疑似代表表記 代表表記:てす/てす 品詞変更:てす-てす-てす-15-1-0-0" <未知語><品詞推定:名詞><疑似代表表記><代表表記:てす/てす><正規化代表表記:てす/てす><品詞変更:てす- てす-てす-15-1-0-0-"未知語:ひらがな 品詞推定:名詞 疑似代表表記 代表表記:てす/てす"><品曖-その他><かな漢字><ひらがな><名詞相当語><自立><複合←><内容語><タグ単位始><文節主辞>
と と と 助詞 9 格助詞 1 * 0 * 0 NIL <文末><表現文末><かな漢字><ひらがな><付属>
EOS
てすと/てすと

Segmentation fault (コアダンプ)
```


ちなみに、私はJavaからKNPのプロセスを立てる方式でKNPを使用しており、下記のようなコードで出力の取得を行っていました。

```java
class ProcessStreamThread implements Runnable {
  private final InputStream inputStream;
  private final Consumer<List<String>> outputConsumer;

  ProcessStreamThread(InputStream inputStream, Consumer<List<String>> outputConsumer) {
    this.inputStream = inputStream;
    this.outputConsumer = outputConsumer;
  }

  @Override
  public void run() {
    try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
      while (true) {
        List<String> streamLines = readLines(bufferedReader);

        if (streamLines.isEmpty()) {
          continue;
        }

        outputConsumer.accept(streamLines);
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  private List<String> readLines(BufferedReader bufferedReader) throws IOException {
    List<String> streamLines = new ArrayList<>();
    String line;
    while (bufferedReader.ready()) {
      line = bufferedReader.readLine();
      if (line == null) {
        continue;
      }
      streamLines.add(line);
    }
    return streamLines;
  }
}
```

このコードで今回の問題が発生すると2回目の入力を与えた時点で`ERROR:Cannot make mrph`等の出力も返ってこないため、例外も吐かずに`bufferedReader.ready()`が永遠にfalseを返し続けることになり原因の特定に非常に苦労しました。

この問題については`+`や`*`で`ERROR:Cannot make mrph`のレスポンスが返ってくる場合と異なり、入力を与えたあとに発生を検知・リカバリーをするのがやや難しい内容になります。
そのため事前に入力文に含まれる半角スラッシュ`/`を全角文字や問題ない他の記号に置換するか、`-anaphora`オプション自体を使用しないなどで対処するのが無難です。

# おわりに
KNPはルールベースでテキストを処理したり、付与される情報等を見てヒューリスティックに分析する際には有用なツールです。

一方で(一応)全角文字を入力の前提としているため、適切な前処理を行わずにWeb上のテキストなど半角記号が含まれるテキストを与えると予期せぬ問題に遭遇する場合がありますので、月並みではありますが扱う内容に応じて適切に前処理等を行う必要があります。