倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Java の正規表現 API を試す

この前、Groovy の GDK でグループを含む正規表現を試してみました。waman.hatenablog.com
今回は根本に還って、Java の正規表現 API (java.util.regex) の使い方をザッと試してみます。 試した内容は Pattern, Matcher クラスに定義されているメソッドの基本的な使い方で、ほぼ Java Tutorial に書いてあるようなことですが、こういうのって JavaDoc に書いといて欲しいところ。 Matcher の JavaDoc 見ても全然 API の使い方が分からんかったゼ・・・

ちなみに正規表現自体には深入りしてません。

この記事の内容

参考

基本機能

まずは正規表現 API を使う際の基本機能を見ていきましょう。 大きく分けて次の2つの機能があります:

  • 正規表現マッチ
  • 正規表現検索

まぁ、分ける必要があるかという気もしますが。 Groovy でいうと「==~」演算子と「=~」演算子の機能にあたります。 また、ここでは正規表現検索を、グループを使わない場合と使う場合の2つにさらに分けて見ていきます。

正規表現マッチ
正規表現マッチは文字列が全体として指定した正規表現にマッチするかどうかを検証します。 各種クラスを使う次の3つの方法があります:

  • Matcher#matches() メソッドを使う
  • Pattern#matches() メソッドを使う
  • String#matches() メソッドを使う

どのメソッドも正規表現にマッチしているかどうかを boolean 値で返します。 Pattern#matches() メソッドを使う方法は String#matches() メソッドが追加されてからはあまり使用する必要がなくなったかと思います。 同じ正規表現を使い回す場合は Pattern オブジェクトを保持しておいて、各文字列に対して Matcher オブジェクトを生成すると正規表現のパース (compile) にかかるコストが節約できます。

Matcher#matches() メソッドと似た正規表現マッチメソッドとして Matcher#lookingAt() メソッドがあります。 これは指定された文字列の最初から検証して、正規表現にマッチするかどうかをみます。 必ずしも文字列全体がマッチする必要はありません。

ではサンプルを見ていきましょう。 使っている正規表現「a*b」は0個以上の a の後に b が1つ続くパターンです。

import java.util.regex.*;
public class RegexMatchMain {
    public static void main(String... args){

        // Matcher
        // 1. matches メソッド
        Pattern p = Pattern.compile("a*b");
        Matcher m0 = p.matcher("aaab");
        assert m0.matches();

        // 2. lookingAt メソッド
        Matcher m1 = p.matcher("aaabc");
        assert !m1.matches();
        assert m1.lookingAt();

        // Pattern (String#matches を使った方が分かりやすい)
        assert Pattern.matches("a*b", "aaab");

        // String
        assert "aaab".matches("a*b");
    }
}

「2. lookingAt メソッド」の箇所では "aaabc" が最後の c のために matches() メソッドではマッチしていないと判定されてますが、lookingAt() メソッドでは先頭から b までの部分が正規表現にマッチしているので判定は通ります。

正規表現検索
次は指定された文字列中を検索して、正規表現にマッチする部分の情報を取得する方法。 この方法では Matcher クラスの次の4つのメソッドを使います:

  • find() メソッド
  • group() メソッド
  • start() メソッド
  • end() メソッド

この中でも、主に find() と group() を使います。 これらは Iterator の hasNext() と next() みたいなメソッドです。 ただし find() メソッドが Matcher オブジェクトの状態を変え、group() はマッチした文字列を返すだけで状態を変更しないことに注意。 while 文などでループして使うのが素直な使い方。

サンプルコードは以下のようになります:

import java.util.regex.*;
public class RegexFindMain {
    public static void main(String... args){

        // Matcher
        Matcher m = Pattern.compile("a*b").matcher("abaabaaab");
        while(m.find()){
            System.out.printf("%s [%d-%d]", m.group(), m.start(), m.end());
            System.out.println();
        }
    }
}

start() と end() はマッチした部分が元の文字列のどの位置かを返します。 これを実行すると以下のように表示されます:

ab [0-2]
aab [2-5]
aaab [5-9]

正規表現グループ検索 groupCount(), group(int), start(int), end(int)
次は正規表現にグループが定義されている場合。 グループは、正義表現中に丸括弧 () で囲ってその部分を別途簡単に抜き出せるようにします。 『Groovy でグループを使った正規表現マッチを試す - 倭マン's BLOG』ではこの機能を GDK で使いやすくしたものを試したのでした。

グループ機能を使うためのメソッドは、正規表現検索で使った find() メソッドと group() 等に対応する group(int), start(int), end(int) メソッドです。 引数の int 値は参照したいグループの番号ですが、0番目は引数なしの group() メソッドと同じ文字列が返されます。 1番目以降はそれぞれのグループにマッチする部分文字列が返されます。 また、グループの個数を返す groupCount() メソッドもあります。 group(int) に渡せる数値は 0 ~ groupCount() の値となります。

ではサンプルコード。 start(int), end(int) の使い方は start(), end() と同じなので省略。

import java.util.regex.*;
public class RegexFindGroupMain {
    public static void main(String... args){

        Pattern p = Pattern.compile("((a+)b+)c+");
        Matcher m = p.matcher("aaaabcaabbccabbbbc");
        assert m.groupCount() == 2;
        // 定義されているグループ
        // 0. ((a+)b+)c+  // マッチした文字列全体
        // 1. ((a+)b+)
        // 2. (a+)

        // 1つ目のマッチ
        m.find();
        assert m.group().equals("aaaabc");
        assert m.group(0).equals("aaaabc");
        assert m.group(1).equals("aaaab");
        assert m.group(2).equals("aaaa");
        assert !m.hitEnd();

        // 2つ目のマッチ
        m.find();
        assert m.group().equals("aabbcc");
        assert m.group(0).equals("aabbcc");
        assert m.group(1).equals("aabb");
        assert m.group(2).equals("aa");
        assert !m.hitEnd();

        // 3つ目のマッチ
        m.find();
        assert m.group().equals("abbbbc");
        assert m.group(0).equals("abbbbc");
        assert m.group(1).equals("abbbb");
        assert m.group(2).equals("a");

        assert m.hitEnd();
        assert !m.find();
        assert m.hitEnd();
    }
}

Matcher#hitEnd() メソッドは次のマッチがなければ true、あれば false を返します。 グループ機能を使ってなくても使用可。

正規表現のグループが何かというのさえ分かれば、API 自体は使うのに苦労はないと思います。

文字列の操作

さて、上記の基本機能は、それらを使えば正規表現でできる大体のことはできるという低レベル機能でした。 正規表現を使った文字列の置換や分割はこれらの機能を使っても書けますが、よく使われるのでそれ独自の API が定義されています。

文字列の置換
与えられた文字列の中で正規表現にマッチする部分を別の文字列に変換するメソッドには

  • Matcher#replaceFirst(String)
  • Matcher#replaceAll(String)

の2つがあります。 使い方はメソッド名の通り。 同名同機能のメソッド(ただし第1引数は正規表現を表す文字列)が String クラスにも定義されています:

  • String#replaceFirst(String, String)
  • String#replaceAll(String, String)

使い分けは正規表現マッチと同じく、同じ正規表現を使い回すかどうかで行えばよいかと思います。

また、もうすこし低レベルの置換機能のために Matcher クラスに以下のメソッドが定義されています:

  • Matcher#appendReplacement(StringBuffer, String)
  • Matcher#appendTail(StringBuffer)

使い方はサンプルコード参照。

ではサンプルコードを見ていきましょう:

import java.util.regex.*;
public class RegexReplaceMain {
    public static void main(String... args){

        // Matcher
        Pattern p = Pattern.compile("b+");
        Matcher m = p.matcher("abbbaabbaaab");
        assert m.replaceFirst("-").equals("a-aabbaaab");
        assert m.replaceAll("-").equals("a-aa-aaa-");

        // Matcher append
        StringBuffer buffer = new StringBuffer();
        m.reset();
        while(m.find()) {
            m.appendReplacement(buffer, "*");
        }
        m.appendTail(buffer);
        assert buffer.toString().equals("a*aa*aaa*");

        // String
        assert "abbbaabbaaa".replaceFirst("b+", "-").equals("a-aabbaaa");
        assert "abbbaabbaaa".replaceAll("b+", "-").equals("a-aa-aaa");
    }
}

Matcher#reset() メソッドは Matcher の状態を初期化します。 もちろん置換機能とは関係なく使えます。 appendReplacement(), appendTail() を使う場合は appendTail() を最後に呼ぶのを忘れないようにしないといけませんね。 String メソッドの replaceFirst(), replaceAll() は、第1引数が正規表現であることに注意が必要ですね。

文字列の分割
与えられた文字列を正規表現にマッチする部分で分割するには以下のメソッドを使います:

  • Matcher#split(String)
  • Matcher#split(String, int)
  • Matcher#splitAsStream(String)

Stream として返すメソッドも定義されてますねぇ。 2つ目の split() メソッドの int 引数は分割する個数です。 また、String メソッドにも split() メソッドと同機能のメソッドが定義されています:

  • String#split(String)

ではサンプルコード。

import java.util.regex.*;
import java.util.Arrays;
import java.util.stream.Stream;
public class RegexSplitMain {
    public static void main(String... args){

        // Pattern
        Pattern p = Pattern.compile("\\s+");
        String[] sa1 = p.split("abc def\tghi\n");
        assert Arrays.equals(sa1, new String[]{"abc", "def", "ghi"});

        String[] sa2 = p.split("jkl mno  pqr", 2);
        assert Arrays.equals(sa2, new String[]{"jkl", "mno  pqr"});
            // 最初のスペースだけで分割される

        Stream<String> ss = p.splitAsStream("stu vwx yz");
        assert Arrays.equals(ss.toArray(), new String[]{"stu", "vwx", "yz"});

        // String
        String[] sa3 = "abbbaabbaaab".split("b+");
        assert Arrays.equals(sa3, new String[]{"a", "aa", "aaa"});
    }
}

String の split() メソッドは replace と同様に、引数の文字列が正規表現であることに注意しないといけませんね。

さて、これで Pattern クラス、Matcher クラスに定義されているメソッドの半分くらいを消化しました。 これらのメソッドで充分いろいろなことができると思います。 残りのメソッドは正規表現の詳細にもうちょっと深入りしないといけないので、続きがあるかどうかは不明。 暇ができたらやろうかと。

正規表現技術入門 ――最新エンジン実装と理論的背景 (WEB+DB PRESS plus)

正規表現技術入門 ――最新エンジン実装と理論的背景 (WEB+DB PRESS plus)

詳説 正規表現 第3版

詳説 正規表現 第3版

  • 作者: Jeffrey E.F. Friedl,株式会社ロングテール,長尾高弘
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2008/04/26
  • メディア: 大型本
  • 購入: 24人 クリック: 754回
  • この商品を含むブログ (85件) を見る
正規表現クックブック

正規表現クックブック