Java : 正規表現の基本
正規表現の構文は知っているけど、Javaで使うにはどうしたらよいのかな…
そんなかたを対象に、本記事ではJavaにおける正規表現の基本的な使い方を解説していきます。
概要
プログラミングにおける正規表現とは…
- 検索対象となる文字列に対して、指定した文字列のパターンと一致するか判定する
この 文字列のパターン の表記方法を正規表現といいます。
また、正規表現のパターンと一致するかどうか判定することをパターンマッチングといいます。
本記事では、正規表現自体の詳しい解説は割愛します。
どのようにJavaで正規表現を使うのか、ということを主眼として解説していきます。
Javaでは、
を使いパターンマッチングを行います。
また、Stringクラスにはいくつか正規表現を使うメソッドがあります。
ただし、PatternやMatcherに比べて簡易的なものとなっています。
API | 完全一致 | 前方一致 | 部分一致 | 一致した文字列の取得 | 文字列の分割 | 文字列の置換 | パフォーマンス | 手軽さ |
---|---|---|---|---|---|---|---|---|
String | 〇 | × | × | × | 〇 | 〇 | △ | 〇 |
Pattern, Matcher | 〇 | 〇 | 〇 | 〇 | 〇 | 〇 | 〇 | △ |
できること、できないことを簡単にまとめてみました。
ちょっとしたことならString、
パターンマッチングによる大量の処理を必要とする場合は Pattern, Matcherを使うのがよいかなと思います。
Stringを使う方法
Stringクラスには、正規表現を使うメソッドとして、
- 完全一致 (matches)
- 文字列の分割 (split)
- 文字列の置換 (replaceAll, replaceFirst)
があります。
完全一致
文字列が、指定した正規表現に完全一致しているか判定します。
前方一致や部分一致ではないのでご注意ください。
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
文字列の分割
文字列を、指定した正規表現の位置で分割します。
(正規表現に関係なく、単純な文字で分割したいときにもよくお世話になるメソッドです)
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]
問題なく正規表現に一致した部分で分割できていますね。
文字列の置換
ある文字列に対して、指定した正規表現に一致する文字列を、指定した文字列へ置き換えます。
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を使いパターンマッチングを行う流れは、
- 正規表現の文字列をコンパイル → Patternを生成
- Patternにマッチングの対象となる文字列を指定 → Matcherを生成
- Matcherでパターンマッチングを行う。
- 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クラス
Patternクラスは、コンパイル済みの正規表現です。
ここでいうコンパイルとは、プログラムが正規表現を扱いやすい形式に変換することをいいます。
おそらく文字列表現のままだと扱いにくいのでしょう。
コンパイルには Pattern.compile を使います。
コンパイルの結果は Patternオブジェクトとなります。
// 文字列で表現した正規表現。
final var regex = "a+bc";
// コンパイル済みの正規表現。
final var 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-19.0.1
public final class String
...
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
さらに Patternクラスの matches メソッドは次のようになっています。
// openjdk-19.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クラス
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 を使うことをおすすめします。
完全一致
完全一致によるパターンマッチングを行います。
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
}
前方一致
前方一致によるパターンマッチングを行います。
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
}
部分一致
部分一致によるパターンマッチングを行います。
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
部分一致によるパターンマッチングを行い、結果を 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
文字列の分割
文字列の分割には 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 -> {
System.out.println(s);
});
// 結果
// ↓
//012
//345
//678
final var regex = "[ABC]";
final var pattern = Pattern.compile(regex);
pattern.splitAsStream("vvvAxxxByyyCzzz").forEach(s -> {
System.out.println(s);
});
// 結果
// ↓
//vvv
//xxx
//yyy
//zzz
文字列の置換
文字列の置換には 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"
一度にすべてを置換するのではなく、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");
System.out.println("appendReplacement : " + sb);
}
matcher.appendTail(sb);
System.out.println("appendTail : " + sb);
// 結果
// ↓
//appendReplacement : Z
//appendReplacement : Z-012-Z
//appendTail : 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");
System.out.println("appendReplacement : " + sb);
}
matcher.appendTail(sb);
System.out.println("appendTail : " + sb);
// 結果
// ↓
//appendReplacement : ZZZ
//appendReplacement : ZZZ-def-ZZZ
//appendTail : ZZZ-def-ZZZ-012
まとめ
ある文字列の中から、特定のパターンの文字や文字列を検索したいとき、正規表現は非常に強力なツールとなります。
1つ注意したいのは、正規表現はちょっとした記述ミスによる思わぬ不具合が発生しやすい、と個人的には感じています。
特に、複雑な正規表現は…慣れていない人には読みづらく直感的ではない表現になりやすいです。
正規表現を使ったコードは、ユニットテストで品質を確保することも検討してみましょう。
本記事では紹介しきれていないメソッドもまだまだたくさんあります。
興味のあるかたは、関連記事のAPI使用例の記事も参考にしていただけたら幸いです。