RedPen を Java ライブラリとして使う

# 概要
[RedPen](http://redpen.cc/) は JJUG CCC 2016 Spring 「[Javaでつくる技術ドキュメントのバリデーション環境](http://www.slideshare.net/recruitcojp/java-62249302)」で紹介されていた文書校正用ツールで、CLIやWebサーバ上で動作します。今すぐ試してみたい場合は公式サイトに [デモ用 Web ツール](http://redpen.herokuapp.com/) が用意されていますので、そちらを試してみるとよいでしょう。

また、RedPen は Java のライブラリとしても提供されており、自分のアプリケーションでこのライブラリが提供するドキュメントチェック・校正の機能を簡単に利用することが可能です。というわけで、今回はこの RedPen を Java ライブラリとして使ってみました。

## 要件
[ソースコード](https://github.com/redpen-cc/redpen) に Lambda 式や Stream API が利用されているため、 Java8 が必要です。

| Software | Version
|:---|:---
| Java  | 1.8.40 or greater
| Maven | 3.0.0 or greater

## 依存の追加
build.gradle の dependencies に下記を追記します。Gradle 以外は [こちら](http://search.maven.org/#artifactdetails|cc.redpen|redpen-core|1.5.5|jar) をご参照ください。

```groovy:dependencies
compile 'cc.redpen:redpen-core:1.5.5'
```

RedPen では文章を形態素に分割するのに kuromoji を使用しているようです。

```:dependencies_tree
\--- cc.redpen:redpen-core:1.5.5
     +--- com.atilika.kuromoji:kuromoji-ipadic:0.9.0
     |    \--- com.atilika.kuromoji:kuromoji-core:0.9.0
     +--- org.json:json:20080701
     +--- org.slf4j:slf4j-api:1.7.6
     +--- org.pegdown:pegdown:1.4.2
     |    \--- org.parboiled:parboiled-java:1.1.6
     |         +--- org.parboiled:parboiled-core:1.1.6
     |         +--- org.ow2.asm:asm:4.1
     |         +--- org.ow2.asm:asm-tree:4.1
     |         |    \--- org.ow2.asm:asm:4.1
     |         +--- org.ow2.asm:asm-analysis:4.1
     |         |    \--- org.ow2.asm:asm-tree:4.1 (*)
     |         \--- org.ow2.asm:asm-util:4.1
     |              \--- org.ow2.asm:asm-tree:4.1 (*)
     \--- org.apache.commons:commons-lang3:3.4
```

---

# 軽く試す

## ソースコード全体

```java:軽く試す用のコード
import java.util.ArrayList;
import java.util.List;

import cc.redpen.RedPen;
import cc.redpen.RedPenException;
import cc.redpen.config.Configuration;
import cc.redpen.config.ValidatorConfiguration;
import cc.redpen.model.Document;
import cc.redpen.model.Sentence;
import cc.redpen.validator.ValidationError;

public class RedPenVerification {
    public static void main(final String[] args) {
        final List<Document> documents = new ArrayList<>();
        documents.add(Document.builder()
                .setFileName("tested file")
                .addSection(0)
                .addParagraph()
                .addSentence(new Sentence("it is a piece of a cake.", 1))
                .addSentence(new Sentence("that is also a piece of a cake.", 2))
                .build());

        final RedPen redPen = getRedPenWithSentenceValidator();
        final List<ValidationError> errors = redPen.validate(documents).get(documents.get(0));
        errors.forEach(error -> {
            System.out.println(error.getLineNumber() + " " + error.getMessage());
        });
    }

    private static RedPen getRedPenWithSentenceValidator() {

        final Configuration configuration = Configuration.builder()
                .addValidatorConfig(
                        new ValidatorConfiguration("SentenceLength")
                            .addProperty("max_len", "5"))
                .build();
        try {
            return new RedPen(configuration);
        } catch (final RedPenException e) {
            e.printStackTrace();
        }
        return null;
    }
}
```

## 処理の概要
1. main メソッドで documents という Document クラスの List オブジェクトを定義
2. RedPen オブジェクトを生成して返す getRedPenWithSentenceValidator() メソッドをコール
3. バリデーションを実施し、結果を List<ValidationError> で受け取る
4. errors の全要素に対し、行番号とエラーメッセージを表示


## 実行結果

```:実行結果
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
1 文長（"24"）が最大値 "5" を超えています。
2 文長（"31"）が最大値 "5" を超えています。
```

---

# Validator
RedPen には多数の Validator が用意されています。どんな Validator が使えるかは公式ドキュメントの [Validator](http://redpen.cc/docs/1.5/index.html#validator) に載っています。

RedPen オブジェクトを生成する際に configuration を渡せます。その configuration に、何をチェックさせるかの Validator を設定するようです。


```java:Configuration
final Configuration configuration = Configuration.builder()
                .addValidatorConfig(
                        new ValidatorConfiguration("SentenceLength")
                            .addProperty("max_len", "5"))
                .build();
final RedPen redPen = new RedPen(configuration);
```

ValidatorConfiguration の初期化は下記の通り、コンストラクタで大項目を、addProperty で小項目を設定します。

```java:ValidatorConfiguration
new ValidatorConfiguration("SentenceLength")
              .addProperty("max_len", "5")
```

## SentenceLength
1 Sentence の長さが指定値以上なら警告します。

```java:ValidatorConfiguration
new ValidatorConfiguration("SentenceLength")
              .addProperty("max_len", "5")
```

```:警告例
1 文長（"24"）が最大値 "5" を超えています。
```

## InvalidExpression
不適切な表現を検出します。指定はファイルと文字列のいずれかで可能です。両方同時に指定してもよいです。

| Property | Description
|:---|:---
| dict | ファイル、1行に1要素
| list | 文字列、「,」で区切る

```java:ValidatorConfiguration(list)
new ValidatorConfiguration("InvalidExpression")
              .addProperty("list", "cake,a piece"))
```

```java:ValidatorConfiguration(dict)
new ValidatorConfiguration("InvalidExpression")
                            .addProperty("dict", "invalid.txt")
```

```:invalid.txt
it is
that
```

```:警告例
1 不正な表現 "cake" がみつかりました。
1 不正な表現 "a piece" がみつかりました。
2 不正な表現 "cake" がみつかりました。
2 不正な表現 "a piece" がみつかりました。
1 不正な表現 "it is" がみつかりました。
2 不正な表現 "that" がみつかりました。
```

### 備考
「,」を含む表現を検出したい場合は dict を使う必要があります。

## InvalidWord
不適切な単語を検出します。指定はファイルと文字列のいずれかで可能です。両方同時に指定してもよいです。 InvalidExpression との違いは **スペースを含められない** ことです。仮にスペースを含めたものを渡しても、コンパイルは通るが無視されます。

| Property | Description
|:---|:---
| dict | ファイル、1行に1要素
| list | 文字列、「,」で区切る

```java:ValidatorConfiguration(list)
new ValidatorConfiguration("InvalidWord")
              .addProperty("list", "cake,a piece"))
```

```java:ValidatorConfiguration(dict)
new ValidatorConfiguration("InvalidWord")
              .addProperty("dict", "invalid.txt"))
```

```:invalid.txt
it is
that
```

```:警告例
1 不正な単語 "cake" がみつかりました。
2 不正な単語 "cake" がみつかりました。
2 不正な単語 "that" がみつかりました。
```

## CommaNumber
1 sentence 内の「,」が指定数以上なら警告を出します。

```java:ValidatorConfiguration
new ValidatorConfiguration("CommaNumber")
              .addProperty("max_num", "1")
```

```:警告例
2 カンマの数 (2) が最大の "1" を超えています。
```

## WordNumber
1 sentence 内の単語が指定数以上なら警告を出します。

```java:ValidatorConfiguration
new ValidatorConfiguration("WordNumber")
              .addProperty("max_num", "5")
```

```:警告例
1 一文に存在する単語数 (8) が最大値 "5" を超えています．。
2 一文に存在する単語数 (9) が最大値 "5" を超えています．。
```

### 備考
日本語ほか一部の言語では対応していません。

## SuggestExpression
不適切な表現を検出し、代替表現を提示します。不適切表現と代替表現のペアはファイルに tsv 形式で保存し、指定します。

```java:ValidatorConfiguration(dict)
new ValidatorConfiguration("SuggestExpression")
              .addProperty("dict", "suggest.txt")
```

```:suggest.tsv
noriben	Nori-Bento
```
左が不適切表現、右が代替表現です。

```:警告例
1 不正な単語 "noriben" がみつかりました．かわりに "Nori-Bento" を利用してください。
```

## InvalidSymbol
デフォルトだとマルチバイトの記号を検出します。

```java:ValidatorConfiguration(dict)
new ValidatorConfiguration("InvalidSymbol")
```

```:警告例
2 不正なシンボル "？" がみつかりました。
3 不正なシンボル "。" がみつかりました。
```

## SymbolWithSpace
「(」や「{」の前にスペースを置いていない場合は警告します。

```java:ValidatorConfiguration(dict)
new ValidatorConfiguration("SymbolWithSpace")
```

```:警告例
1 シンボルの前にスペースが必要です (()。
2 シンボルの前にスペースが必要です ({)。
```

## KatakanaEndHyphen
カタカナ語が長音記号で終わっている場合に警告します。技術系文書で3字以上のカタカナ語は長音記号を除くという慣習が由来です。

```java:ValidatorConfiguration(dict)
new ValidatorConfiguration("KatakanaEndHyphen")
```

例えばこんな文章だと

```
.addSentence(new Sentence("彼は今日コンピューターを買った。", 3))
.addSentence(new Sentence("彼はキーとスキーとメジャーが好きだ。", 4))
.addSentence(new Sentence("彼はいつもラー油について熱弁をふるっています。", 5))
```

下記のように検出されます。

```:警告例
3 カタカナ単語 "コンピューター" に不正なハイフンが見つかりました。
4 カタカナ単語 "メジャー" に不正なハイフンが見つかりました。
```

### 備考
日本語特有の慣習であるため、日本語でしか使えません。

## KatakanaSpellCheck
カタカナ語の表記揺れを検出します。辞書は効いているのかよくわかりません。

```java:ValidatorConfiguration(dict)
new ValidatorConfiguration("KatakanaSpellCheck")
              .addProperty("dict", "spelling.txt")
              .addProperty("min_ratio", 0.8)
              .addProperty("min_freq", 1)
```

min_ratio で最低類似度(0.0-1.0)、min_freq(1-) で最低出現回数を設定します。マイナスや極端に大きい値も設定可能ですが、別に何もエラーは出ませんし、検出もされなくなるので意味はないでしょう。

例えばこんな文章だと

```java:文章例
.addSentence(new Sentence("彼は今日ジェネリクスを知った。", 3))
.addSentence(new Sentence("彼はジェネリックスが好きだ。", 4))
.addSentence(new Sentence("彼はいつもジェネリクッスについて熱弁をふるっています。", 5))
```

下記の通り警告されます。

```:警告例
4 カタカナ単語 "ジェネリックス" に類似する単語 "ジェネリクス" が行 "3" でみつかりました。
5 カタカナ単語 "ジェネリクッス" に類似する単語 "ジェネリクス" が行 "3" でみつかりました。
```

### 備考
日本語でしか使えません。

## SectionLength
Section 内の文字数が指定値を超えている場合は警告します。

```java:ValidatorConfiguration
new ValidatorConfiguration("SectionLength")
              .addProperty("max_num", "40")
```

次のような Document が定義されている場合、

```java:文章例
Document.builder()
                .setFileName("tested file")
                .addSection(0)
                .addParagraph()
                .addSentence(new Sentence("it is a noriben.", 1))
                .addSentence(new Sentence("is that also a piece of a cake？", 2))
                .addSentence(new Sentence("彼は今日ジェネリクスを知った。", 3))
                .addSentence(new Sentence("彼はジェネリックスが好きだ。", 4))
                .addSentence(new Sentence("彼はいつもジェネリクッスについて熱弁をふるっています。", 5))
```

下記のように警告されます。

```:警告例
0 セクション内の文字数が最大の"103"を超えています
```

残念ながらわかりにくいメッセージです。この103という数字は実際の文字数で、制限値が表示されていません。

## ParagraphNumber
1セクション内のパラグラフ数が指定値を超えている場合は警告します。

```java:ValidatorConfiguration(dict)
new ValidatorConfiguration("ParagraphNumber")
              .addProperty("max_num", "0")
```

```:警告例
0 セクション内のパラグラフ数が最大の"1"を超えています
```

## SpaceBetweenAlphabeticalWord
アルファベット表記語の前後に**半角**スペースがない場合、警告します。

```java:ValidatorConfiguration(dict)
new ValidatorConfiguration("SpaceBetweenAlphabeticalWord")
```

```java:文章例
.addSentence(new Sentence("彼は今日ジェネリクスを知った。", 3))
.addSentence(new Sentence("彼はgenericsが好きだ。", 4))
.addSentence(new Sentence("彼はいつも　inverted indexについて熱弁をふるっています。", 5))
```

```:警告例
2 アルファベット単語の後にスペースが存在しません。
4 アルファベット単語の前にスペースが存在しません。
4 アルファベット単語の後にスペースが存在しません。
5 アルファベット単語の前にスペースが存在しません。
5 アルファベット単語の後にスペースが存在しません。
```

## Spelling
綴りが誤っている可能性を指摘します。

```java:ValidatorConfiguration(dict)
new ValidatorConfiguration("Spelling")
              .addProperty("dict", "spelling.txt")
              .addProperty("list", "cake")
```

```:spelling.txt
noriben
```

```java:文章例
.addSentence(new Sentence("it is a noribene.", 1))
.addSentence(new Sentence("is that also a piece of a cako?", 2))
```

```:警告例
1 ミススペルの可能性がある単語 "noribene" がみつかりました。
2 ミススペルの可能性がある単語 "cako" がみつかりました。
```

## 備考
英語でしか使えません。日本語の文章は全体が誤りだと判定されます。

## DoubledWord
1つの文で同じ言葉が複数回登場したら警告します。

```java:ValidatorConfiguration
new ValidatorConfiguration("DoubledWord")
                        .addProperty("dict", "white_list.txt")
                        .addProperty("list", "item")
```

dict と list で検査から除外する言葉を設定できます。

```:white_list.txt
のり弁
```

```
.addSentence(new Sentence("this good item is very good and item.", 3))
.addSentence(new Sentence("彼の今日のお昼はのり弁のり弁アンドのり弁だ", 4))
.addSentence(new Sentence("「のり弁のり弁」と、やけにのり弁を推すじゃないか。", 5))
```

```:警告例
3 一文に二回以上利用されている単語 "good" がみつかりました。
```

## DoubledJoshi
同じ助詞が複数回続いた場合は警告します。

```java:ValidatorConfiguration(dict)
new ValidatorConfiguration("DoubledJoshi")
              .addProperty("dict", "white_list.txt")
              .addProperty("list", "の")
```

```java:文章例
.addSentence(new Sentence("彼の今日のお昼の弁当はのり弁とのり弁とのり弁だ", 1))
.addSentence(new Sentence("「のり弁のり弁」と、やけにのり弁を推すじゃないか。", 2))
```

```:警告例
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:653)
	at java.util.ArrayList.get(ArrayList.java:429)
	at java.util.Collections$UnmodifiableList.get(Collections.java:1309)
	at cc.redpen.validator.sentence.DoubledJoshiValidator.validate(DoubledJoshiValidator.java:43)
	at cc.redpen.RedPen.lambda$null$33(RedPen.java:221)
	at java.util.ArrayList.forEach(ArrayList.java:1249)
	at cc.redpen.RedPen.lambda$runSentenceValidators$34(RedPen.java:221)
	at java.util.ArrayList.forEach(ArrayList.java:1249)
	at cc.redpen.RedPen.runSentenceValidators(RedPen.java:221)
	at cc.redpen.RedPen.validate(RedPen.java:135)
```

えっと、これはバグだと思いますが、いったいなぜ……

## SuccessiveWord
同じ言葉が続いたら警告します。

```java:ValidatorConfiguration
new ValidatorConfiguration("SuccessiveWord")
```

```java:文章例
.addSentence(new Sentence("the item is is very good.", 1))
```

```:警告例
1 単語 "is" は連続して使用されています。
```

### 備考
英語や一部の言語(除日本語)でしか使えません。

## JapaneseStyle
常体(だ、である調)と敬体(ですます調)が混在していたら警告を発するはずですが、動きませんでした。

```java:ValidatorConfiguration
new ValidatorConfiguration("JapaneseStyle")
```

```java:文章例
.addSentence(new Sentence("彼の今日のお昼の弁当はのり弁とのり弁とのり弁です。", 1))
.addSentence(new Sentence("それは贅沢である。", 2))
.addSentence(new Sentence("しかし彼には選択肢がなかったのである。", 3))
```

## DoubleNegative
二重否定を検出します。が、動きませんでした。

```java:ValidatorConfiguration
new ValidatorConfiguration("DoubleNegative")
```

## FrequentSentenceStart
同じ書き出しになっている文章が連続した場合に警告します。

```java:ValidatorConfiguration
new ValidatorConfiguration("FrequentSentenceStart")
              .addProperty("leading_word_limit", "3")
              .addProperty("percentage_threshold", "10")
              .addProperty("min_sentence_count", "1")```
```

```java:文章例
.addSentence(new Sentence("This is a pen.", 1))
.addSentence(new Sentence("This is not a pipe.", 2))
.addSentence(new Sentence("This is a stcik.", 3))
```

```:警告例
3 75% of sentences start with "This is".
3 50% of sentences start with "This is a".
3 75% of sentences start with "This".
3 25% of sentences start with "This is not".
```

## UnexpandedAcronym
アクロニム(=頭字語、名詞句の頭文字だけで構成した語)が拡張されていない時に指摘します。

```java:ValidatorConfiguration
new ValidatorConfiguration("UnexpandedAcronym")
              .addProperty("min_acronym_length", "4")
```

"min_acronym_length" で許可するアクロニム長を指定できます。


```java:文章例
.addSentence(new Sentence("We talk URL, HTTPS, DDoS, AOSSL", 1))
.addSentence(new Sentence("This is not a corba.", 2))
```

```:警告例
2 The expanded form of the acronym HTTPS is not present in the document.
2 The expanded form of the acronym AOSSL is not present in the document.
```

どうも小文字は検出できないようです。

### 備考
英語文でのみ使用できます。

## WordFrequency
単語頻度を計測します。

```java:ValidatorConfiguration
new ValidatorConfiguration("WordFrequency")
              .addProperty("deviation_factor", "4")
              .addProperty("min_word_count", "4")
```

```java:文章例
.addSentence(new Sentence("We talk URL, HTTPS, DDoS, AOSSL", 1))
.addSentence(new Sentence("This is not a corba.", 2))
.addSentence(new Sentence("To be, or not to be, that is a question.", 3))
.addSentence(new Sentence("Let it be, let it be.", 4))
```

```:警告例
4 The word "a" comprises 8.70% of all words. Expected around 1.94%.
4 The word "be" comprises 17.39% of all words. Expected around 0.72%.
4 The word "or" comprises 4.35% of all words. Expected around 0.54%.
4 The word "question" comprises 4.35% of all words. Expected around 0.05%.
4 The word "this" comprises 4.35% of all words. Expected around 0.55%.
4 The word "is" comprises 8.70% of all words. Expected around 1.00%.
4 The word "it" comprises 8.70% of all words. Expected around 0.99%.
4 The word "we" comprises 4.35% of all words. Expected around 0.33%.
4 The word "not" comprises 8.70% of all words. Expected around 0.64%.
4 The word "talk" comprises 4.35% of all words. Expected around 0.01%.
4 The word "let" comprises 8.70% of all words. Expected around 0.05%.
```

### 備考
英語の文章でのみ使用できます。

## NumberFormat
数値に「,」での区切りがない場合は指摘します。

```java:ValidatorConfiguration
new ValidatorConfiguration("NumberFormat")
              .addProperty("decimal_delimiter_is_comma", "true")
              .addProperty("ignore_years", "false")
```

```java:文章例
.addSentence(new Sentence("We was born 1969.", 1))
.addSentence(new Sentence("I spend \10000000.", 2))
```

```:警告例
1 Number 1969 requires a delimiter every three digits (eg: 1,000.00 or 1.000,00).
2 Number 00000 requires a delimiter every three digits (eg: 1,000.00 or 1.000,00).
```

### 備考
英語ないしフランス語の文章でのみ使用できます。

## HankakuKana
半角カナを検出します。

```java:ValidatorConfiguration
new ValidatorConfiguration("HankakuKana")
```

```java:文章例
.addSentence(new Sentence("ｶﾞｸｶﾞｸﾌﾟﾙﾌﾟﾙ", 1))
```

```:警告例
1 半角カナの文字 "ｶ" が見つかりました。
1 半角カナの文字 "ﾞ" が見つかりました。
1 半角カナの文字 "ｸ" が見つかりました。
1 半角カナの文字 "ｶ" が見つかりました。
1 半角カナの文字 "ﾞ" が見つかりました。
1 半角カナの文字 "ｸ" が見つかりました。
1 半角カナの文字 "ﾌ" が見つかりました。
1 半角カナの文字 "ﾟ" が見つかりました。
1 半角カナの文字 "ﾙ" が見つかりました。
1 半角カナの文字 "ﾌ" が見つかりました。
1 半角カナの文字 "ﾟ" が見つかりました。
1 半角カナの文字 "ﾙ" が見つかりました。
```

なお、半角スペースを含む文章だと半角スペースを全部検出します。使いどころが限定されかねないです。

### 備考
日本語でしか使えません。


## Okurigana
不正な送り仮名を検出します。

```java:ValidatorConfiguration
new ValidatorConfiguration("Okurigana")
```

```java:文章例
.addSentence(new Sentence("声を荒らげるのは新らしいルールに従て必らずやめましょう。", 1))
```

```:警告例
1 不正な送り仮名 "荒ら" がみつかりました。
1 不正な送り仮名 "必ら" がみつかりました。
```

うーん……

### 備考
日本語でしか使えません。

---
# まとめ
RedPen は Java のライブラリとして使うことができます。テキストを扱うツールやアプリケーションに組み込むことで、文章校正機能を提供することが可能となります。

---
# リンク
1. [公式サイト](http://redpen.cc/)
2. [公式サイトのデモ](http://redpen.herokuapp.com/)
3. [GitHub repository](https://github.com/redpen-cc/redpen)
4. [Javaでつくる技術ドキュメントのバリデーション環境](http://www.slideshare.net/recruitcojp/java-62249302)……JJUG CCC 2016 Spring での発表資料です。