JavaのRegular Expression

先日、Javaでは無かったのですが、Regular Expressionを使う機会があり、ふと、Javaではどうなっていたかが気になって試してみました。

JavaでRegular Expressionを試すとなるとJavaチュートリアルに掲載されているコード(Test Harness (The Java™ Tutorials > Essential Classes > Regular Expressions))が有名ではないかと思うのですが、JDK 6で追加されたjava.io.Consoleを使っているところがうまく動きません。このコードをNetBeansで試すと、Consoleが無いとかで例外になってしまうし、Macのターミナルで試すと日本語を満足に入力できないし、、、で、とりあえず、こんなRegular Expression実行用のコードを書いて試してみました。日本語がまじっているとRegular Expressionのパターンを毎回入力するのがかなり面倒だったので、評価文字列だけ何度も入力できるようにしてあります。

package cirrus;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SimpleRegex {
    public enum Choice {r, regex, i, inputs, e, exit};

    private SimpleRegex() throws IOException {
        Pattern pattern = null;
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        while (true) {
            System.out.println("r[egex], i[nputs] or e[xit]? ");
            try {
                switch (Enum.valueOf(Choice.class, reader.readLine())) {
                    case regex:
                    case r:
                        pattern = getPattern(reader);
                        break;
                    case inputs:
                    case i:
                        eval(pattern, reader);
                        break;
                    case exit:
                    case e:
                        System.exit(0);
                        break;
                    default:
                        continue;
                }
            } catch (IllegalArgumentException e) {
            }
        }
    }

    private Pattern getPattern(BufferedReader reader) throws IOException {
        System.out.print("regex? ");
        return Pattern.compile(reader.readLine(), Pattern.UNICODE_CASE);
    }

    private void eval(Pattern pattern, BufferedReader reader) throws IOException {
        if (pattern == null) {
            System.out.println("no regex");
            return;
        }
        System.out.print("inputs? ");
        Matcher matcher = pattern.matcher(reader.readLine());
        boolean found = false;
        while (matcher.find()) {
            System.out.println("Found \"" + matcher.group() + "\" starting at " + matcher.start() +
                    " and enging at " + matcher.end() + ".");
            found = true;
        }
        if (!found) {
            System.out.println("No match found.");
        }
    }

    public static void main(String[] args) throws IOException {
        new SimpleRegex();
    }
}

日本語まじりのパターンを指定するときは Pattern.compile(reader.readLine(), Pattern.UNICODE_CASE); のようにcompileメソッドでflagの指定が必要でした。このflagを指定しない場合、簡単なパターンでは問題が無いのですが、いろいろな記号やアルファベットもまぜた複雑なパターンではマッチするはずのところもマッチしませんでした。このflagというのは他の言語やgrepコマンドなどでは i(case insensitive), m (multi-line mode; Rubyではmは違う意味)といったオプション指定がありますが、それをJavaで行いたい場合に指定する引数です。

Javaの場合、文字は全てUnicodeで扱われているので、Regular ExpressionもUnicodeベースです。パターンに通常の文字を書いても内部的にはUnicodeでマッチングが行われているので、例えば、パターンに [a-ん]を指定すると、Unicodeのcodepointで英語のアルファベットの小文字の"a"のところから日本語のひらがなの"ん"の部分まで含まれます。試しにひらがな、フランス語、英語を入力してみたら、3つともちゃんとマッチしました。

java -cp . cirrus.SimpleRegex
r[egex], i[nputs] or e[xit]? 
r
regex? [a-ん]
r[egex], i[nputs] or e[xit]? 
i
inputs? すみません
Found "す" starting at 0 and enging at 1.
Found "み" starting at 1 and enging at 2.
Found "ま" starting at 2 and enging at 3.
Found "せ" starting at 3 and enging at 4.
Found "ん" starting at 4 and enging at 5.
r[egex], i[nputs] or e[xit]? 
i
inputs? Désolé
Found "D" starting at 0 and enging at 1.
Found "é" starting at 1 and enging at 2.
Found "s" starting at 2 and enging at 3.
Found "o" starting at 3 and enging at 4.
Found "l" starting at 4 and enging at 5.
Found "é" starting at 5 and enging at 6.
r[egex], i[nputs] or e[xit]? 
i
inputs? Sorry!
Found "S" starting at 0 and enging at 1.
Found "o" starting at 1 and enging at 2.
Found "r" starting at 2 and enging at 3.
Found "r" starting at 3 and enging at 4.
Found "y" starting at 4 and enging at 5.
r[egex], i[nputs] or e[xit]? 

言語やコマンドに採用されているRegular Expression engineによってはUnicode blockをまたがった指定はできないらしいのですが、Javaの場合、codepointのどこからどこの範囲と指定できるので、Unicode blockの境界は無いみたいです。加えて、[a-\u30fc](アルファベットの"a"からカタカナの"ン"までの範囲)のようなパターンも有効です。例えば、"Java[あ-ん]*[a-\u30fc]{2}"というパターンを指定してみると、

java -cp . cirrus.SimpleRegex
r[egex], i[nputs] or e[xit]? 
r
regex? Java[あ-ん]*[a-\u30fc]{2}
r[egex], i[nputs] or e[xit]? 
i
inputs? Javaとpナッツ
Found "Javaとpナ" starting at 0 and enging at 7.
r[egex], i[nputs] or e[xit]? 
i
inputs? Javaあかさたなハマヤラワ
Found "Javaあかさたなハマ" starting at 0 and enging at 11.
r[egex], i[nputs] or e[xit]? 

という結果になりました。


UnicodeとRegular Expressionについては

に詳しい解説がありました。Wikipediaでもそれなりに解説されています。