やり直しJava

正規表現

Javaで正規表現を扱うには java.util.regexパッケージを利用します。Javaでの正規表現の用途は 大別すると次の通りになります。

  • ある文字列が正規表現パターンと一致するか、または正規表現パターンが含まれるかどうかの判定(一致判定)。また一致する文字列の抽出
  • 正規表現パターンに一致する文字列を置換
  • ある文字列を正規表現パターンで分割

Javaで正規表現を使うには 大きく分けて2通りの方法があります。

  • 事前に正規表現パターンをコンパイルしておく方法
  • 正規表現パターンをコンパイルせずに、検索・置換・分割を行う際に 正規表現パターンを指定する方法(ユーティリティ)

正規表現パターンによる一致判定等を行う前に、正規表現パターンのチェックをして 正規表現エンジンが解析できるようにするためのデータ構造を構築する処理を行います。同じ正規表現パターンで 繰り返し一致判定等を行う場合は この事前の処理が冗長となるため、事前処理をコンパイルという形で 独立して呼び出せるようになっています。

そのため 同じ正規表現パターンで何度も一致判定等を行う場合は、事前にコンパイルをした方が冗長な処理を省けます。一方で 同じ正規表現パターンを1度しか使わない場合は 事前にコンパイルしないユーティリティを使う方が便利です

ただし、ユーティリティを使う場合は「大文字・小文字の区別をしない」とか「複数行モードを有効にする」といった 細かい制御を行うことができません。また 一致判定では 部分一致の判定ができず、抽出を行うこともできません。そのため 正規表現のフル機能を利用したい場合は、必然的に事前にコンパイルする方法を選択することになります

事前にコンパイルをする使い方

主にPatternクラスとMatcherクラスを使います。Patternクラスで正規表現をコンパイルしMatherクラスがマッチ操作を行い 適用結果を保持します。基本的な流れは次のようになります。

  1. 正規表現のパターンをコンパイルして Patternオブジェクトを作成します。
  2. Patternオブジェクトの正規表現に 適用したい文字列を渡して Matherオブジェクトを作成します。
  3. Matcherオブジェクトにて 一致判定・抽出・置換などを行います。

分割を行う場合は 1ステップ減って次のようになります。

  1. 正規表現のパターンをコンパイルして Patternオブジェクトを作成します。
  2. Patternオブジェクトに 分割したい文字列を渡して 分割を行います。

一致判定

Matherにて一致判定を行う場合、全体一致か部分一致かでメソッドが異なります。

  • 正規表現パターンが 全体と一致するかどうか判定する場合には matches()メソッドを使います。
  • パターンが 先頭から一致するかどうか判定する場合には lookingAt()メソッドを使います。
  • パターンが どこかの部分と一致するかどうか判定する場合には find()メソッドを使います。
    find()の場合は 複数箇所で一致する可能性がありますので、while文などのループと組み合わせて使うことができます。

matches()による一致判定の例です。一部分が一致していても、全体が一致していないと true にはなりません。

String message1 = "HD2100";
String message2 = "HD2100 MP1500 SD4000";
Pattern p = Pattern.compile("[A-Z]+[0-9]+");
System.out.println(p.matcher(message1).matches());  // true
System.out.println(p.matcher(message2).matches());  // false 全体が一致しないと false

続いて lookingAt()による一致判定の例です。先頭が一致していないと trueにはなりません。

String message1 = "HD2100-000";
String message2 = "000-HD2100";
Pattern p = Pattern.compile("[A-Z]+[0-9]+");
System.out.println(p.matcher(message1).lookingAt());  // true
System.out.println(p.matcher(message2).lookingAt());  // false 先頭が一致しないと false

最後に find()による一致判定の例です。二度目のfind()の呼び出しでは 一度目で一致した部分の次から判定を開始します。一致するパターンが見つかる間はtrueを返します。一致するパターンがなくなるとfalseが返ります。

String message1 = "HD-2100";
String message2 = "HD2100 MP1500 SD4000";
Pattern p = Pattern.compile("[A-Z]+[0-9]+");
System.out.println(p.matcher(message1).find());  // false

Matcher m = p.matcher(message2);
System.out.println(m.find());  // true  "HD2100" と一致
System.out.println(m.find());  // true  "MP1500" と一致
System.out.println(m.find());  // true  "SD4000" と一致
System.out.println(m.find());  // false

抽出

一致箇所を抽出したい場合は、正規表現パターンを丸括弧 () で囲みます。Matcherのgroup()メソッドにより 一致箇所を抽出することができます。group()メソッドの引数で 正規表現パターンの丸括弧の位置(左からの出現順)を指定します。ここで注意する点は、位置の開始は1オリジンであることです。引数なしのgroup()と group(0) は一致したパターン全体を返します。

String message = "HD2100 MP1500 SD4000";
Pattern p = Pattern.compile("([A-Z]+[0-9]+)\\s([A-Z]+[0-9]+)\\s([A-Z]+[0-9]+)");
Matcher m = p.matcher(message);
if(m.matches()) {
    System.out.println(m.group());  // HD2100 MP1500 SD4000
    System.out.println(m.groupCount());  // 3
    for(int i = 1; i <= m.groupCount(); i ++) {  // 位置の指定は 1 から
        System.out.println(i + ":" + m.group(i));  // ex. 1:HD2100
    }
}

丸括弧がネストされている場合は、左から数えて “(” が出現した順番が位置となります。例えば “((AB)((CD)(EF)))” というパターンの場合は 次のようになります。

String message = "ABCDEF";
Pattern p = Pattern.compile("((AB)((CD)(EF)))");
Matcher m = p.matcher(message);
if(m.matches()) {
    for(int i = 1; i <= m.groupCount(); i ++) {
        System.out.println(i + ":" + m.group(i));
    }
}

実行結果は次のようになります。

1:ABCDEF
2:AB
3:CDEF
4:CD
5:EF

位置指定では 分かりづらく 間違いも発生し易いため、名前を付けて指定することもできます正規表現パターンの中で “?<名前>” の形で名前付けをします。group()メソッドでは 正規表現パターンの中で付けた名前を指定します。存在しない名前を指定すると IllegalArgumentExceptionが発生します。

String message = "HD2100 MP1500 SD4000";
Pattern p = Pattern.compile("(?<first>[A-Z]+[0-9]+)\\s(?<second>[A-Z]+[0-9]+)\\s(?<third>[A-Z]+[0-9]+)");
Matcher m = p.matcher(message);
if(m.matches()) {
    System.out.println(m.group("first"));  // HD2100
    System.out.println(m.group("second"));  // MP1500
    System.out.println(m.group("third"));  // SD4000
    System.out.println(m.group("fourth"));  // IllegalArgumentException 発生
}

lookingAt()やfind()を使った場合も 同様に group()メソッドで一致箇所を抽出することができます。find()を使った例を次に挙げます。

String message = "HD2100 MP1500 SD4000";
Pattern p = Pattern.compile("([A-Z]+)([0-9]+)");

Matcher m = p.matcher(message);
while(m.find()) {
    System.out.println(m.group());  // ex. HD2100
    System.out.println(m.group(1) + "-" + m.group(2));  // ex. HD-2100
}

実行結果は次のようになります。

HD2100
HD-2100
MP1500
MP-1500
SD4000
SD-4000

置換

正規表現パターンで一致した箇所を置換するには MatcherクラスのreplaceFirst()、replaceAll()メソッドを使います。

String message = "HD2100 MP1500 SD4000";
Pattern p = Pattern.compile("[A-Z]+");
Matcher m = p.matcher(message);
System.out.println(m.replaceFirst("**"));  // **2100 MP1500 SD4000
System.out.println(m.replaceAll("**"));  // **2100 **1500 **4000

分割

正規表現パターンで指定した区切り文字で分割するには Patternオブジェクトのsplit()メソッドを使います。分割するにはMatcherを使わず Patternを使うことがポイントです。

String message = "HD2100;MP1500;SD4000";
Pattern p = Pattern.compile(";");
String[] array = p.split(message);
System.out.println(Arrays.toString(array));  // [HD2100, MP1500, SD4000]

また、配列ではなく Stringのストリームとして返すsplitAsStream()メソッドも用意されています。

String message = "HD2100;MP1500;SD4000";
Pattern p = Pattern.compile(";");
p.splitAsStream(message).forEach(System.out::println);

事前にコンパイルをしない使い方(ユーティリティ)

同じ正規表現パターンによる一致判定・置換・分割を1度しか行わない場合は、事前にコンパイルをしなくても呼び出せる ユーティリティメソッドを使うのが便利です。それぞれ 次のようなユーティリティが用意されています。

  • 一致判定:Stringクラスのmatches()メソッド、Patternクラスのmatches()クラスメソッド
  • 置換:StringクラスのreplaceAll()、replaceFirst()メソッド
  • 分割:Stringクラスのsplit()メソッド

ただし、一致判定用のユーティリティのmatches()は 全体一致の判定を行うため部分一致には使えません。また 呼び出し側に 一致結果を抽出するためのMatcherオブジェクトが返らないため、抽出を行うことができません。更に、「大文字・小文字の区別をしない」とか「複数行モードを有効にする」といった細かい制御を指定することができません(これらのオプションは 事前にコンパイルをする際に Pattern.compile()の引数で指定するためです)。

Pythonでの正規表現の使方」(Qiita)
PatternとMatcherによる正規表現処理」(Java 好き)