Java
正規表現

Java での正規表現の使い方メモ

More than 1 year has passed since last update.

Java で正規表現を使う方法を色々メモ。

String クラスの正規表現を使うメソッド

String には正規表現を受け取るメソッドがいくつか存在する。

matches(String)

package sample.regexp;

public class Main {

    public static void main(String[] args) {
        String text = "abc123";

        System.out.println(text.matches("[a-z0-9]+"));
        System.out.println(text.matches("[a-z]+"));
    }
}
実行結果
true
false
  • 文字列が指定した正規表現と完全に一致することを検証する
  • 一部が一致するだけの場合は false になる

replaceAll(String, String)

package sample.regexp;

public class Main {

    public static void main(String[] args) {
        String text = "abc123";

        System.out.println(text.replaceAll("[a-z]", "*"));
    }
}
実行結果
***123
  • 第一引数に正規表現を渡し、マッチする部分を全て第二引数の文字列に置換する

置換文字列にマッチしたグループを使用する

package sample.regexp;

public class Main {

    public static void main(String[] args) {
        String text = "<<abc123>>";

        System.out.println(text.replaceAll("([a-z]+)([0-9]+)", "$0, $1, $2"));
    }
}
実行結果
<<abc123, abc, 123>>
  • 置換文字列に $n を含めることで、マッチしたグループを置換後の文字列再利用できる
    • n0 はじまり
    • 0 はマッチした文字列全体を指している
      • ([a-z]+)([0-9]+) にマッチした部分なので、 abc123 が対象になる
    • 1 から先は、 () で囲ったグループを順番に参照できる
      • $1([a-z]+) にマッチした abc が、
      • $2([0-9]+) にマッチした 123 が対象になる
  • マッチしているグループの数より大きい数を n に指定した場合は、 IndexOutOfBoundsException がスローされる
  • 単純に $ という文字列に置き換えたい場合はバックスラッシュ (\) でエスケープする
    • text.replaceAll("[a-z]+", "\\$")
    • エスケープしていない場合は、 IllegalArgumentException がスローされる
  • グループはインデックス以外にも名前を付けて名前で参照することも可能

replaceFirst(String, String)

package sample.regexp;

public class Main {

    public static void main(String[] args) {
        String text = "abc123";

        System.out.println(text.replaceFirst("[a-z]", "*"));
    }
}
実行結果
*bc123
  • 正規表現にマッチした部分文字列のうち、最初にマッチした箇所だけを置換する
  • $n で部分文字列を参照できるのは、 replaceAll() と同じ

split(String, int)

package sample.regexp;

import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        String text = "a1b2";

        for (int i=-1; i<5; i++) {
            String[] elements = text.split("[0-9]", i);
            System.out.println("limit=" + i + ",\telements=" + Arrays.toString(elements));
        }
    }
}
実行結果
limit=-1,   elements=[a, b, ]
limit=0,    elements=[a, b]
limit=1,    elements=[a1b2]
limit=2,    elements=[a, b2]
limit=3,    elements=[a, b, ]
limit=4,    elements=[a, b, ]
  • 第一引数で指定した正規表現にマッチする場所で文字列を分割する
  • 第二引数の limit は、戻り値の配列のサイズの上限を決める
    • limit1 以上の値を指定した場合、マッチした部分文字列の分割は limit - 1 番目までになる
    • limit==1 の場合は、 limit - 1 => 0 になるので、分割は行われない(結果、配列のサイズは 1 になる)
    • limit==2 の場合は、 limit - 1 => 1 になるので、正規表現 [0-9] に最初にマッチした a1b21 の部分で分割が行われ、そこで分割は終了する(結果、配列のサイズは 2 になる)
    • limit0 以下の値を指定した場合は制限無しの扱いになり、文字列の末尾まで分割が実行される
      • ただし、分割の結果末尾に文字列が残っていない(空白になった)ときの挙動が、 0 と負数とで異なる
      • 負数の場合は、最後の空白も配列の要素として残される
      • 0 の場合は、最後の空白は破棄される

分割の結果先頭が空白になる場合は、そのまま空白が配列の要素として設定される。

package sample.regexp;

import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        String text = "0a1b2";

        String[] elements = text.split("[0-9]", 0);
        System.out.println(Arrays.toString(elements));
    }
}
実行結果
[, a, b]

split(String)

これは、 split(String, int) の第二引数を 0 にしたのと同じ挙動になる。

Pattern クラス

String のメソッドとの違い

一部の例外1を除いて、 String クラスの正規表現を使ったメソッドは、裏では Pattern クラスに処理を委譲している。
たとえば、 replaceAll() メソッドの実装を確認すると次のようになっている。

String.replaceAll()
    public String replaceAll(String regex, String replacement) {
        return Pattern.compile(regex).matcher(this).replaceAll(replacement);
    }

Java における正規表現の処理は、この Pattern クラス(と Matcher)が担当している。

Pattern クラスは compile() で渡された文字列を正規表現として解釈する。
使用する正規表現が固定なのであれば、この compile() は最初の1回だけ実行して、あとは Pattern インスタンスを使いまわしたほうが効率がいい。
Pattern クラスはイミュータブルなので、マルチスレッドでも安全に使いまわすことができる)

しかし、 String クラスの正規表現を使うメソッドを利用していると、この compile() が毎回実行されてしまう。
そのため、固定の正規表現を何度も繰り返し実行するような場合に String のメソッドを利用していると、 Pattern インスタンスを使いまわすよりも処理速度が遅くなる。

Patternを使いまわす例
public class Hoge {
    // コンパイル済みの Pattern インスタンスを使いまわす
    private static final Pattern HOGE_PATTERN = Pattern.compile("[0-9]+");

    public boolean test(String text) {
        return HOGE_PATTERN.matcher(text).matches(); // 動きは text.maches("[0-9]+") と同じ
    }
}

基本的な使い方

package sample.regexp;

import org.openjdk.jmh.runner.RunnerException;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws RunnerException {
        Pattern pattern = Pattern.compile("[0-9]+");

        Matcher abc = pattern.matcher("123abc");
        System.out.println(abc.matches());

        Matcher _123 = pattern.matcher("123");
        System.out.println(_123.matches());
    }
}
実行結果
false
true
  • まず、 Pattern.compile(String) で正規表現をコンパイルし、 Pattern インスタンスを取得する
  • つづいて Pattern.matcher(String) メソッドで検証したい文字列(入力シーケンス)を渡し、 Matcher インスタンスを取得する
  • 取得した Matcher インスタンスを使って、マッチしたかどうかなどの検証を行う
  • Matcher.matches() は入力シーケンス全体が正規表現と一致するかどうかを検証し、 boolean で結果を返す

分割

package sample.regexp;

import org.openjdk.jmh.runner.RunnerException;

import java.util.Arrays;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws RunnerException {
        Pattern pattern = Pattern.compile("[a-z]+");

        String[] elements = pattern.split("123abc456def789ghi");
        System.out.println(Arrays.toString(elements));

        elements = pattern.split("123abc456def789ghi", -1);
        System.out.println(Arrays.toString(elements));
    }
}
実行結果
[123, 456, 789]
[123, 456, 789, ]
  • Pattern.split(String) で、指定した文字列のうち正規表現にマッチする部分で文字列を分割する
  • 動きは String.split(String), String.split(String, int) と同じ

Matcher

  • Pattern は正規表現を解釈するクラスで、次のような処理は Matcher が行う
    • 入力シーケンスが正規表現にマッチしているかどうか
    • マッチした部分の抽出
    • マッチした部分の置換
  • Matcher は、大きく次のようなステップで使用する
    1. マッチ操作を実行する
    2. マッチ操作の結果を問い合わせる
    3. 必要であれば、 1, 2 を繰り返す
  • マッチ操作の結果は、以下のメソッドで参照するできる
    • start() マッチした入力シーケンス上の開始インデックス
    • end() マッチした入力シーケンス上の終了インデックス + 1
    • group() マッチした部分文字列
    • マッチ操作を実行していない状態でこれらのメソッドを実行すると、 IllegalStateException がスローされる
  • Matcher はスレッドセーフではないので注意

マッチ操作

Matcher には、3つのマッチ操作が存在する。

  • matches()
    • 入力シーケンス全体と正規表現がマッチするか検証する
  • lookingAt()
    • 入力シーケンスの先頭から正規表現がマッチするか検証する
  • find()
    • 入力シーケンスに正規表現にマッチする部分が存在するか順番に検証する

matches()

package sample.regexp;

import org.openjdk.jmh.runner.RunnerException;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws RunnerException {
        test("abc");
        test("abc123");
    }

    private static void test(String text) {
        Pattern pattern = Pattern.compile("[a-z]+");
        Matcher matcher = pattern.matcher(text);

        System.out.println("[text=" + text + "]");
        if (matcher.matches()) {
            System.out.println("matches = true");
            System.out.println("start = " + matcher.start());
            System.out.println("end = " + matcher.end());
            System.out.println("group = " + matcher.group());
        } else {
            System.out.println("matches = false");
        }
    }
}
実行結果
[text=abc]
matches = true
start = 0
end = 3
group = abc

[text=abc123]
matches = false
  • matches() は、入力シーケンス全体が正規表現とマッチするか検証する
  • マッチする場合は true を返す

lookingAt()

package sample.regexp;

import org.openjdk.jmh.runner.RunnerException;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws RunnerException {
        test("abc");
        test("123abc");
        test("ab12");
    }

    private static void test(String text) {
        Pattern pattern = Pattern.compile("[a-z]+");
        Matcher matcher = pattern.matcher(text);

        System.out.println("[text=" + text + "]");
        if (matcher.lookingAt()) {
            System.out.println("lookingAt = true");
            System.out.println("start = " + matcher.start());
            System.out.println("end = " + matcher.end());
            System.out.println("group = " + matcher.group());
        } else {
            System.out.println("lookingAt = false");
        }
    }
}
実行結果
[text=abc]
lookingAt = true
start = 0
end = 3
group = abc

[text=123abc]
lookingAt = false

[text=ab12]
lookingAt = true
start = 0
end = 2
group = ab
  • lookingAt() は、入力シーケンスの先頭から正規表現にマッチするか検証する
  • 先頭からの検証結果がマッチしていれば、 true が返される(全体が一致している必要はない)

find()

package sample.regexp;

import org.openjdk.jmh.runner.RunnerException;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws RunnerException {
        test("abc");
        test("123abc456def789");
    }

    private static void test(String text) {
        Pattern pattern = Pattern.compile("[a-z]+");
        Matcher matcher = pattern.matcher(text);

        System.out.println("[text=" + text + "]");
        while (matcher.find()) {
            System.out.println("start = " + matcher.start());
            System.out.println("end = " + matcher.end());
            System.out.println("group = " + matcher.group());
        }
    }
}
実行結果
[text=abc]
start = 0
end = 3
group = abc

[text=123abc456def789]
start = 3
end = 6
group = abc

start = 9
end = 12
group = def
  • find() メソッドは、入力シーケンスの先頭から正規表現にマッチする部分がないか走査する
  • マッチする部分文字列が存在した場合は true を返す
  • find() をもう一度実行すると、前回マッチした部分から再びマッチする部分文字列が存在しないか走査が行われる
    • 繰り返し実行することでマッチした部分文字列の抽出が可能
  • start(), end(), group() は、直前にマッチした結果が返る

置換

package sample.regexp;

import org.openjdk.jmh.runner.RunnerException;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws RunnerException {
        Pattern pattern = Pattern.compile("[a-z]+");
        Matcher matcher = pattern.matcher("abc123def");

        System.out.println("replaceAll = " + matcher.replaceAll("*"));
        System.out.println("replaceFirst = " + matcher.replaceFirst("*"));
    }
}
実行結果
replaceAll = *123*
replaceFirst = *123def
  • Matcher.replaceAll(String) で、マッチした部分文字列を全て置換する
  • Matcher.replaceFirst(String) で、最初にマッチした部分文字列だけを置換する

グループ

package sample.regexp;

import org.openjdk.jmh.runner.RunnerException;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws RunnerException {
        Pattern pattern = Pattern.compile("([a-z]+)([0-9]+)");
        Matcher matcher = pattern.matcher("abc123de45fg");

        int groupCount = matcher.groupCount();
        System.out.println("groupCount=" + groupCount);

        while (matcher.find()) {
            System.out.println("==========");
            String group = matcher.group();
            System.out.println("group=" + group);

            for (int i=0; i<=groupCount; i++) {
                String g = matcher.group(i);
                System.out.println("group(" + i + ")=" + g);
            }
        }
    }
}
実行結果
groupCount=2
==========
group=abc123
group(0)=abc123
group(1)=abc
group(2)=123
==========
group=de45
group(0)=de45
group(1)=de
group(2)=45
  • 正規表現で定義したグループ(() で囲われた部分)について参照するには、以下のメソッドが用意されている
    • groupCount() 正規表現で定義されたグループの数を取得する
    • group() 直近のマッチ操作でマッチした文字列全体を取得する
    • group(int) 直近のマッチ操作でマッチしたグループのうち、指定したインデックスのグループを取得する
      • 番号 0 はマッチした文字列全体なので、 group() と同じ結果を返す
      • 1 から先がマッチした部分文字列になる

グループに名前をつける

package sample.regexp;

import org.openjdk.jmh.runner.RunnerException;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws RunnerException {
        Pattern pattern = Pattern.compile("(?<alphabets>[a-z]+)(?<numbers>[0-9]+)");
        Matcher matcher = pattern.matcher("abc123de45fg");

        while (matcher.find()) {
            System.out.println("==========");
            System.out.println("group(alphabets)=" + matcher.group("alphabets"));
            System.out.println("group(numbers)=" + matcher.group("numbers"));
        }
    }
}
実行結果
==========
group(alphabets)=abc
group(numbers)=123
==========
group(alphabets)=de
group(numbers)=45
  • グループを (?<グループ名>パターン) と定義することで、グループに名前を定義することができる
  • group(String) メソッドで定義した名前を指定してグループにマッチした部分文字列を取得できる

グループ名を置換文字列で参照する場合は、次のようにする

package sample.regexp;

import org.openjdk.jmh.runner.RunnerException;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws RunnerException {
        Pattern pattern = Pattern.compile("(?<alphabets>[a-z]+)(?<numbers>[0-9]+)");
        Matcher matcher = pattern.matcher("abc123def456");

        String replaced = matcher.replaceAll("${numbers}${alphabets}");
        System.out.println(replaced);
    }
}
実行結果
123abc456def
  • ${グループ名} でグループを参照できる

フラグ

  • Pattern インスタンスを生成するさい、正規表現を解釈する方法をフラグで調整することができる。
フラグを指定したコンパイル
Pattern pattern = Pattern.compile("[a-z]", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
  • フラグは compile(String, int) の第二引数で指定する
  • 指定できるのは、 Pattern クラスに static で宣言された定数になる
  • ビットマスクになっているので、複数のフラグを指定する場合は | で連結して指定する

CASE_INSENSITIVE (大文字小文字を区別しない)

package sample.regexp;

import org.openjdk.jmh.runner.RunnerException;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws RunnerException {
        Pattern pattern = Pattern.compile("[a-z]+", Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher("ABC");
        System.out.println(matcher.matches());
    }
}
実行結果
true
  • CASE_INSENSITIVE を指定した場合、マッチは大文字小文字を区別せずに行われる
  • 区別しなくなるのは US-ASCII の文字のみ

UNICODE_CASE(Unicode で大文字小文字を区別しない)

package sample.regexp;

import org.openjdk.jmh.runner.RunnerException;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws RunnerException {
        Pattern pattern = Pattern.compile("[a-zA-Z]+", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
        Matcher matcher = pattern.matcher("ABCabc");
        System.out.println(matcher.matches());
    }
}
実行結果
true
  • UNICODE_CASECASE_INSENSITIVE を組み合わせると、 Unicode で大文字小文字の区別なしでマッチングが行われる

LITERAL(正規表現のメタ文字やエスケープ文字を使わない)

package sample.regexp;

import org.openjdk.jmh.runner.RunnerException;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws RunnerException {
        Pattern pattern = Pattern.compile("[a-z]+", Pattern.LITERAL);

        Matcher matcher = pattern.matcher("abc");
        System.out.println(matcher.matches());

        matcher = pattern.matcher("[a-z]+");
        System.out.println(matcher.matches());
    }
}
実行結果
false
true
  • LITERAL を指定すると、 compile(String, int) の第一引数で渡した文字列は単純な文字列として処理される
  • []+ のような正規表現で意味のある文字は、単純にその文字そのものとして解釈される

MULTILINE (複数行の文字列を処理する)

package sample.regexp;

import org.openjdk.jmh.runner.RunnerException;

import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws RunnerException {
        test("[default]", () -> Pattern.compile("^[a-z]+$"));
        test("[MULTILINE]", () -> Pattern.compile("^[a-z]+$", Pattern.MULTILINE));
    }

    private static void test(String label, Supplier<Pattern> patternSupplier) {
        System.out.println(label);
        Pattern pattern = patternSupplier.get();

        String text = "abc\n"
                    + "def\n";

        Matcher matcher = pattern.matcher(text);

        while (matcher.find()) {
            String group = matcher.group();
            System.out.println(group);
        }
    }
}
実行結果
[default]
[MULTILINE]
abc
def
  • MULTILINE を指定した場合、行頭、行末を表す ^, $ の扱いが変化する
  • 何も指定していない場合、 ^, $ は純粋に文字列の先頭・末尾にのみマッチする
  • MULTILINE を指定している場合、改行で区切られたそれぞれが文字列として処理されるため、 ^$ は各行の先頭と末尾にマッチするようになる

COMMENTS (コメントを書けるようにする)

package sample.regexp;

import org.openjdk.jmh.runner.RunnerException;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws RunnerException {
        String regexp = "# この行はコメントとして無視される\n"
                      + "  [a-z]+  ";
        Pattern pattern = Pattern.compile(regexp, Pattern.COMMENTS);

        Matcher matcher = pattern.matcher("abc");

        System.out.println(matcher.matches());
    }
}
実行結果
true
  • COMMENTS を指定すると、以下の文字列はコメント扱いになり無視される
    • # から行末まで
    • 空白スペース

DOTALL (. で行末にもマッチするようにする)

package sample.regexp;

import org.openjdk.jmh.runner.RunnerException;

import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws RunnerException {
        test("[default1]", () -> Pattern.compile(".+"));
        test("[default2]", () -> Pattern.compile(".+$"));
        test("[DOTALL]", () -> Pattern.compile(".+", Pattern.DOTALL));
    }

    private static void test(String label, Supplier<Pattern> patternSupplier) {
        System.out.println(label);
        Pattern pattern = patternSupplier.get();

        String text = "abc\n"
                    + "def\n";

        Matcher matcher = pattern.matcher(text);

        if (matcher.find()) {
            String group = matcher.group();
            System.out.println(group);
        }
    }
}
実行結果
[default1]
abc
[default2]
def
[DOTALL]
abc
def
  • DOTALL を指定すると、 . が行末にもマッチするようになる
  • デフォルトの場合、 . は行末にはマッチしない

参考


  1. 例えば split(String regexp) メソッドは、 regexp が正規表現のメタ文字などが使われていない素の文字列だった場合に、 Pattern を使わずに分割処理を行っている