Java : 正規表現の基本

正規表現の構文は知っているけど、Javaで使うにはどうしたらよいのかな…
そんなかたを対象に、本記事ではJavaにおける正規表現の基本的な使い方を解説していきます。


概要

正規表現(せいきひょうげん、英: regular expression)は、文字列の集合を一つの文字列で表現する方法の一つである。

プログラミングにおける正規表現とは…

  • 検索対象となる文字列に対して、指定した文字列のパターンと一致するか判定する

この 文字列のパターン の表記方法を正規表現といいます。
また、正規表現のパターンと一致するかどうか判定することをパターンマッチングといいます。

本記事では、正規表現自体の詳しい解説は割愛します。
どのようにJavaで正規表現を使うのか、ということを主眼として解説していきます。

Javaでは、

を使いパターンマッチングを行います。

また、Stringクラスにはいくつか正規表現を使うメソッドがあります。
ただし、PatternやMatcherに比べて簡易的なものとなっています。

API 完全一致 前方一致 部分一致 一致した文字列の取得 文字列の分割 文字列の置換 パフォーマンス 手軽さ
String × × ×
Pattern, Matcher

できること、できないことを簡単にまとめてみました。

ちょっとしたことならString
パターンマッチングによる大量の処理を必要とする場合は Pattern, Matcherを使うのがよいかなと思います。

Stringを使う方法

Stringクラスには、正規表現を使うメソッドとして、

  • 完全一致 (matches)
  • 文字列の分割 (split)
  • 文字列の置換 (replaceAll, replaceFirst​)

があります。

完全一致

public boolean matches​(String regex)
この文字列が、指定された正規表現と一致するかどうかを判定します。

文字列が、指定した正規表現に完全一致しているか判定します。
前方一致や部分一致ではないのでご注意ください。

final var regex = "a+bc";

// 完全一致でマッチングします。
System.out.println("abc".matches(regex)); // true
System.out.println("aaaabc".matches(regex)); // true

// 前方一致や部分一致としてはマッチングしません。
System.out.println("abc xyz".matches(regex)); // false
System.out.println("012 abc xyz".matches(regex)); // false

文字列の分割

public String[] split​(String regex)
この文字列を、指定された正規表現に一致する位置で分割します。

文字列を、指定した正規表現の位置で分割します。
(正規表現に関係なく、単純な文字で分割したいときにもよくお世話になるメソッドです)

final var regex = ",";

final var array = "aaa,bbb,ccc".split(regex);
System.out.println(Arrays.toString(array)); // [aaa, bbb, ccc]

もう少し正規表現らしい例も見てみましょう。

final var regex = "a+bc";

final var array = "012abc345aaaaabc678".split(regex);
System.out.println(Arrays.toString(array)); // [012, 345, 678]
final var regex = "[ABC]";

final var array = "vvvAxxxByyyCzzz".split(regex);
System.out.println(Arrays.toString(array)); // [vvv, xxx, yyy, zzz]

問題なく正規表現に一致した部分で分割できていますね。

文字列の置換

public String replaceAll​(String regex, String replacement)
指定された正規表現に一致する、この文字列の各部分文字列に対し、指定された置換を実行します。

ある文字列に対して、指定した正規表現に一致する文字列を、指定した文字列へ置き換えます。

final var regex = "a+bc";

final var result = "abc-aabc-aaabc".replaceAll(regex, "Z");
System.out.println(result); // "Z-Z-Z"
final var regex = "(abc|xyz)";

final var result = "abc-def-xyz".replaceAll(regex, "ZZZ");
System.out.println(result); // "ZZZ-def-ZZZ"

補足

正規表現を必要としない、単純な文字列の置換であれば replace メソッドを使いましょう。
正規表現の特殊文字自体を置換したいときにエスケープが必要ありません。

// "." を "-" に置換したい場合。
final var target = ".";
final var input = "aaa.bbb";

// 期待した結果になりません。
System.out.println(input.replaceAll(target, "-")); // "-------"

// 期待した結果になります。
System.out.println(input.replace(target, "-")); // "aaa-bbb"

Pattern, Matcherを使う方法

クラス構成

Pattern, Matcherを使いパターンマッチングを行う流れは、

  1. 正規表現の文字列をコンパイル → Patternを生成
  2. Patternにマッチングの対象となる文字列を指定 → Matcherを生成
  3. Matcherでパターンマッチングを行う。
  4. MatchResultで結果を取得。

となります。

具体的なコード例を見てみましょう。

// 文字列による正規表現
final var regex = "a+bc";

// 文字列による正規表現をコンパイル → Pattern生成
final var pattern = Pattern.compile(regex);

// マッチング対象となる文字列
final var input = "123 aaabc xyz";

// Matcherを生成
final var matcher = pattern.matcher(input);

// 部分一致によるパターンマッチング
System.out.println(matcher.find()); // true

// 結果を取得
final var result = matcher.toMatchResult();

// 一致した文字列を取得
System.out.println(result.group()); // "aaabc"

なんとなく流れのイメージはつかめましたでしょうか。

Patternクラス

クラス構成

コンパイル済みの正規表現です。
正規表現は、文字列として指定し、このクラスのインスタンスにコンパイルする必要があります。 結果のパターンを使用して、任意の文字シーケンスを正規表現とマッチできるMatcherオブジェクトを作成できます。

Patternクラスは、コンパイル済みの正規表現です。

ここでいうコンパイルとは、プログラムが正規表現を扱いやすい形式に変換することをいいます。
おそらく文字列表現のままだと扱いにくいのでしょう。

コンパイルには Pattern.compile を使います。
コンパイルの結果は Patternオブジェクトとなります。

// 文字列で表現した正規表現。
final String regex = "a+bc";

// コンパイル済みの正規表現。
final Pattern pattern = Pattern.compile(regex);

Patternオブジェクトは不変です。
一度コンパイルした正規表現(Pattern)は、何度でも再利用、共有でき、マルチスレッドでも安全です。

例えば、String.machesを使ったパターンマッチングでは、毎回正規表現をコンパイルすることになるので効率が悪くなります。

// String.machesを使う例。
final var regex = "[0-9]+";

for (int i = 0; i < 100; i++) {
    final var input = String.valueOf(i);
    System.out.println(input.matches(regex)); // true
}

Stringクラスの maches メソッドは、内部的には次のような実装になっています。

// openjdk-16.0.1
public final class String
...
    public boolean matches(String regex) {
        return Pattern.matches(regex, this);
    }

さらに Patternクラスの matches メソッドは次のようになっています。

// openjdk-16.0.1
public final class Pattern
...
    public static boolean matches(String regex, CharSequence input) {
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(input);
        return m.matches();
    }

つまり最初の例に戻ると、以下のコードと同じことになります。

final var regex = "[0-9]+";

for (int i = 0; i < 100; i++) {
    final var input = String.valueOf(i);

    final var pattern = Pattern.compile(regex); // ★効率が悪い
    final var matcher = pattern.matcher(input);

    System.out.println(matcher.matches()); // true
}

forの中で毎回 Pattern.compileしているのは無駄に感じますよね。
コンパイルは1回だけで十分です。

final var regex = "[0-9]+";
final var pattern = Pattern.compile(regex); // ★効率がよい

for (int i = 0; i < 100; i++) {
    final var input = String.valueOf(i);

    final var matcher = pattern.matcher(input);

    System.out.println(matcher.matches()); // true
}

このように、同じ正規表現を何度も使う場合は、コンパイルしたPatternを使いまわすと効率がよくなります。

Matcherクラス

クラス構成

Patternを解釈することによって、 文字シーケンスのマッチ操作を行うエンジンです。
正規表現エンジンは、パターンのmatcherメソッドを呼び出すことによって作成されます。

Matcherクラスには、3種類のパターンマッチングがあります。

  • 完全一致 (matches)
  • 前方一致 (lookingAt)
  • 部分一致 (find, results)

パターンに一致したかどうかは、matches, loolingAt, findの戻り値( boolean )で判定します。
どの部分に一致したのか?という付加的な情報は、MatchResultとして取得できます。

final var regex = "a+bc";
final var pattern = Pattern.compile(regex);

final var input = "012 aaabc xyz";
final var matcher = pattern.matcher(input);

System.out.println(matcher.find()); // true

final var result = matcher.toMatchResult();
System.out.println(result.start()); // 4
System.out.println(result.end()); // 9
System.out.println(result.group()); // "aaabc"

もしくは、Matcherクラス自身がMatchResultを実装しているので、Matcherから直接取得することもできます。

final var regex = "a+bc";
final var pattern = Pattern.compile(regex);

final var input = "012 aaabc xyz";
final var matcher = pattern.matcher(input);

System.out.println(matcher.find()); // true

System.out.println(matcher.start()); // 4
System.out.println(matcher.end()); // 9
System.out.println(matcher.group()); // "aaabc"

個人的には、後発APIでもある toMatchResult を使うことをおすすめします。

完全一致

public boolean matches()
領域全体をこのパターンとマッチします。

完全一致によるパターンマッチングを行います。

final var pattern = Pattern.compile("a+bc");

{
    final var matcher = pattern.matcher("abc");

    System.out.println(matcher.matches()); // true

    final var result = matcher.toMatchResult();
    System.out.println(result.start()); // 0
    System.out.println(result.end()); // 3
    System.out.println(result.group()); // "abc"
}

{
    final var matcher = pattern.matcher("aaaaabc");

    System.out.println(matcher.matches()); // true

    final var result = matcher.toMatchResult();
    System.out.println(result.start()); // 0
    System.out.println(result.end()); // 7
    System.out.println(result.group()); // "aaaaabc"
}

前方一致や部分一致ではないので、次の例ではマッチングしません。

final var pattern = Pattern.compile("a+bc");

{
    final var matcher = pattern.matcher("012abc");
    System.out.println(matcher.matches()); // false
}

{
    final var matcher = pattern.matcher("abcXYZ");
    System.out.println(matcher.matches()); // false
}

前方一致

public boolean lookingAt()
入力シーケンスとパターンとのマッチを、領域の先頭から始めます。
matchesメソッドと同様に、領域の先頭から開始されます。ただし、領域全体がマッチする必要はありません。

前方一致によるパターンマッチングを行います。

final var pattern = Pattern.compile("a+bc");

{
    final var matcher = pattern.matcher("aaabc");

    System.out.println(matcher.lookingAt()); // true

    final var result = matcher.toMatchResult();
    System.out.println(result.start()); // 0
    System.out.println(result.end()); // 5
    System.out.println(result.group()); // "aaabc"
}

{
    final var matcher = pattern.matcher("abcXYZ");

    System.out.println(matcher.lookingAt()); // true

    final var result = matcher.toMatchResult();
    System.out.println(result.start()); // 0
    System.out.println(result.end()); // 3
    System.out.println(result.group()); // "abc"
}

{
    final var matcher = pattern.matcher("012abc");
    System.out.println(matcher.lookingAt()); // false
}

部分一致

public boolean find()
入力シーケンスからこのパターンとマッチする次の部分シーケンスを検索します。このメソッドは、正規検索エンジンの領域の先頭から開始されます。ただし、前回の呼出しが正常に終了してから正規表現エンジンがリセットされていない場合は、前回のマッチで一致しなかった最初の文字から開始されます。

部分一致によるパターンマッチングを行います。
find を呼び出すたびに、最初に一致するのはどこか? 次に一致するのはどこか? 次に一致するのは…と一致結果が移動していきます。

もし、すべての一致結果を一度に取得したい場合は、後述する results メソッドが便利です。

final var pattern = Pattern.compile("a+bc");
final var matcher = pattern.matcher("abc 012 aaabc XYZ");

{
    // 最初に一致するのはどこか?
    System.out.println(matcher.find()); // true

    final var result = matcher.toMatchResult();
    System.out.println(result.start()); // 0
    System.out.println(result.end()); // 3
    System.out.println(result.group()); // "abc"
}

{
    // 次に一致するのはどこか?
    System.out.println(matcher.find()); // true

    final var result = matcher.toMatchResult();
    System.out.println(result.start()); // 8
    System.out.println(result.end()); // 13
    System.out.println(result.group()); // "aaabc"
}

// もう一致するものがありません。
System.out.println(matcher.find()); // false

public Stream<MatchResult> results()
パターンに一致する入力シーケンスの各サブシーケンスに対する一致結果のストリームを返します。 一致結果は、入力シーケンスの一致するサブシーケンスと同じ順序で発生します。

部分一致によるパターンマッチングを行い、結果を Stream として返します。

final var pattern = Pattern.compile("a+bc");
final var matcher = pattern.matcher("abc 012 aaabc XYZ");

final var stream = matcher.results();

stream.forEach(matchResult -> {

    System.out.println("------");
    System.out.println("start : " + matchResult.start());
    System.out.println("end : " + matchResult.end());
    System.out.println("group : " + matchResult.group());
});

//------
//start : 0
//end : 3
//group : abc
//------
//start : 8
//end : 13
//group : aaabc

文字列の分割

public String[] split​(CharSequence input)
このパターンのマッチに基づいて、指定された入力シーケンスを分割します。

文字列の分割には Pattern クラスを使います。Matcherは使いません。
String.splitと使い方は似ています。

final var regex = "a+bc";
final var pattern = Pattern.compile(regex);

final var array = pattern.split("012abc345aaaaabc678");
System.out.println(Arrays.toString(array)); // [012, 345, 678]
final var regex = "[ABC]";
final var pattern = Pattern.compile(regex);

final var array = pattern.split("vvvAxxxByyyCzzz");
System.out.println(Arrays.toString(array)); // [vvv, xxx, yyy, zzz]

もしくは、Stream を返す splitAsStream​ もおすすめです。

final var regex = "a+bc";
final var pattern = Pattern.compile(regex);

pattern.splitAsStream("012abc345aaaaabc678").forEach(s -> {
    // "012"
    // "345"
    // "678"
    System.out.println(s);
});
final var regex = "[ABC]";
final var pattern = Pattern.compile(regex);

pattern.splitAsStream("vvvAxxxByyyCzzz").forEach(s -> {
    // "vvv"
    // "xxx"
    // "yyy"
    // "zzz"
    System.out.println(s);
});

文字列の置換

public String replaceAll​(String replacement)
パターンとマッチする入力シーケンスの部分シーケンスを、指定された置換文字列に置き換えます。

文字列の置換には Matcher を使います。
String.replaceAllと使い方は似ています。

final var regex = "a+bc";
final var pattern = Pattern.compile(regex);

final var matcher = pattern.matcher("abc-012-aabc-345");

final var result = matcher.replaceAll("Z");
System.out.println(result); // "Z-012-Z-345"
final var regex = "(abc|xyz)";
final var pattern = Pattern.compile(regex);

final var matcher = pattern.matcher("abc-def-xyz-012");

final var result = matcher.replaceAll("ZZZ");
System.out.println(result); // "ZZZ-def-ZZZ-012"

public Matcher appendReplacement​(StringBuilder sb, String replacement)
継続追加置換ステップを実装します。

public StringBuilder appendTail​(StringBuilder sb)
終了追加置換ステップを実装します。

一度にすべてを置換するのではなく、find による部分一致のたびに置換することもできます。
find, appendReplacement, appendTail を使います。

final var regex = "a+bc";
final var pattern = Pattern.compile(regex);

final var matcher = pattern.matcher("abc-012-aabc-345");

final var sb = new StringBuilder();

while (matcher.find()) {
    matcher.appendReplacement(sb, "Z");

    // "Z"
    // "Z-012-Z"
    System.out.println(sb);
}

matcher.appendTail(sb);
System.out.println(sb); // "Z-012-Z-345"
final var regex = "(abc|xyz)";
final var pattern = Pattern.compile(regex);

final var matcher = pattern.matcher("abc-def-xyz-012");

final var sb = new StringBuilder();

while (matcher.find()) {
    matcher.appendReplacement(sb, "ZZZ");

    // "ZZZ"
    // "ZZZ-def-ZZZ"
    System.out.println(sb);
}

matcher.appendTail(sb);
System.out.println(sb); // "ZZZ-def-ZZZ-012"

まとめ

ある文字列の中から、特定のパターンの文字や文字列を検索したいとき、正規表現は非常に強力なツールとなります。

1つ注意したいのは、正規表現はちょっとした記述ミスによる思わぬ不具合が発生しやすい、と個人的には感じています。
特に、複雑な正規表現は…慣れていない人には読みづらく直感的ではない表現になりやすいです。

正規表現を使ったコードは、ユニットテストで品質を確保することも検討してみましょう。

本記事では紹介しきれていないメソッドもまだまだたくさんあります。
興味のあるかたは、関連記事のAPI使用例の記事も参考にしていただけたら幸いです。


関連記事

ページの先頭へ