Java正規表現の完全ガイド|基礎から実践まで徹底解説

JavaのPatternクラスを使った正規表現の基本から実践的な活用方法まで網羅的に解説します。PatternとMatcherクラスの使い方、メールアドレスや電話番号などの検証パターンの書き方、キャプチャーや置換といった実務で使える応用テクニック、さらにパフォーマンス上の注意点も学べます。文字列の検証や加工処理に悩むJava開発者必見の内容です。

目次

Javaの正規表現とは?基本概念を理解する

java+regex+code

Javaで文字列処理を効率的に行うためには、正規表現の理解が欠かせません。正規表現を使うことで、複雑な文字列のパターンマッチングや検証、置換といった処理を簡潔なコードで実現できます。ここでは、Javaにおける正規表現の基本概念について、その定義から実際の活用メリットまでを詳しく解説していきます。

正規表現の定義と役割

正規表現(Regular Expression)とは、文字列のパターンを表現するための特殊な記法のことです。文字や記号を組み合わせることで、「3桁の数字」「@を含む文字列」「特定の形式の日付」といった複雑な文字列パターンを定義できます。

正規表現の主な役割は以下の通りです:

  • パターンマッチング:文字列が特定のパターンに合致するかを判定する
  • 文字列検索:テキスト内から特定のパターンに一致する部分を見つけ出す
  • 文字列抽出:パターンに合致した部分を取り出す
  • 文字列置換:パターンに一致する部分を別の文字列に置き換える
  • 入力検証:ユーザー入力が期待する形式に合っているかをチェックする

例えば、メールアドレスの形式を検証する場合、通常のプログラミングでは「@が含まれているか」「ドメイン部分があるか」など複数の条件を個別にチェックする必要がありますが、正規表現を使えば一つのパターン定義で包括的な検証が可能になります。これにより、コードの可読性が向上し、保守性も高まります

Javaにおける正規表現の位置づけ

Javaでは、正規表現機能が標準ライブラリとして提供されており、追加のライブラリをインストールすることなく利用できます。Java 1.4以降、java.util.regexパッケージが導入され、本格的な正規表現のサポートが開始されました。

Javaの正規表現は主に以下のクラスで構成されています:

  • Patternクラス:正規表現パターンをコンパイルして保持するクラス
  • Matcherクラス:実際のマッチング操作を実行するクラス
  • PatternSyntaxExceptionクラス:正規表現の構文エラーを表す例外クラス

これらのクラスを組み合わせることで、高度な文字列処理を実現できます。また、Stringクラスにもmatches()replaceAll()split()といった正規表現を使えるメソッドが用意されており、簡単なパターンマッチングであれば直接Stringクラスのメソッドで処理できる点も特徴です。

Javaの正規表現は、Perlの正規表現をベースにしており、豊富なメタ文字や機能をサポートしています。そのため、他のプログラミング言語で正規表現の経験がある開発者にとっても理解しやすい設計となっています。

正規表現の利用シーンと活用メリット

正規表現は、実務のさまざまな場面で活用されています。具体的な利用シーンとそのメリットを見ていきましょう。

入力フォームのバリデーション

Webアプリケーションやデスクトップアプリケーションでユーザーからの入力を受け取る際、正規表現は強力な検証ツールとなります。メールアドレス、電話番号、郵便番号、クレジットカード番号など、形式が定まっているデータの検証に最適です。一つのパターン定義で包括的な検証ができるため、検証ロジックがシンプルになります

ログファイルの解析

システムログやアクセスログから特定の情報を抽出する際、正規表現が威力を発揮します。タイムスタンプ、IPアドレス、エラーコードなど、ログに含まれる構造化された情報を効率的に取り出せます。大量のログデータから必要な情報だけを抽出する処理も、正規表現を使えば高速に実行できます。

データクレンジングと整形

データベースから取得したデータや外部システムから受け取ったデータを整形する際にも正規表現は有用です。不要な空白の削除、区切り文字の統一、表記ゆれの修正など、複雑な文字列操作を簡潔なコードで実現できます

テキスト処理とパース

CSVファイルやXML、JSONなどの構造化されたテキストデータから情報を抽出する際、正規表現を使うことで柔軟なパース処理が可能になります。特に、フォーマットが完全に固定されていないデータを扱う場合に効果的です。

コード生成とテンプレート処理

テンプレートエンジンやコード生成ツールの実装において、正規表現はプレースホルダーの検出や置換に活用されます。動的にコンテンツを生成する処理を効率化できます。

正規表現を活用することで得られる主なメリットは以下の通りです:

  • コードの簡潔性:複雑な文字列処理を短いコードで表現できる
  • 処理速度:最適化されたエンジンにより高速な処理が可能
  • 保守性の向上:パターンを変更するだけで処理を調整できる
  • 汎用性:一度作成したパターンを様々な場面で再利用できる
  • 可読性:意図が明確なパターン記述により、処理内容が理解しやすくなる

ただし、正規表現は強力な反面、複雑なパターンは読みにくくなる傾向があります。適切なコメントを付けたり、パターンを分割したりすることで、チーム開発でも理解しやすいコードを心がけることが重要です。

“`html

正規表現で使用する主要な記号とメタ文字

regex+pattern+java

Javaの正規表現を扱う上で、メタ文字と呼ばれる特殊な記号を理解することが不可欠です。メタ文字は通常の文字とは異なり、パターンマッチングにおいて特別な意味を持ちます。これらの記号を適切に組み合わせることで、複雑な文字列パターンを簡潔に表現できます。本章では、正規表現で頻繁に使用される主要な記号とメタ文字について、その種類と具体的な使い方を詳しく解説していきます。

基本的なメタ文字の種類と意味

正規表現におけるメタ文字は、文字列のパターンを定義するための特殊な記号です。Javaの正規表現で基本となるメタ文字には、以下のようなものがあります。

メタ文字意味使用例
.任意の1文字「a.c」は「abc」「a1c」などにマッチ
^行の先頭「^abc」は行頭の「abc」にマッチ
$行の末尾「abc$」は行末の「abc」にマッチ
*直前の文字の0回以上の繰り返し「ab*c」は「ac」「abc」「abbc」にマッチ
+直前の文字の1回以上の繰り返し「ab+c」は「abc」「abbc」にマッチ
?直前の文字の0回または1回の出現「ab?c」は「ac」「abc」にマッチ
[]文字クラス(いずれか1文字)「[abc]」は「a」「b」「c」のいずれかにマッチ
|OR条件「abc|def」は「abc」または「def」にマッチ
()グループ化「(ab)+」は「ab」「abab」にマッチ
\エスケープ文字「\.」は文字としての「.」にマッチ

これらのメタ文字を理解することで、単純な文字列の一致だけでなく、柔軟なパターンマッチングが可能になります。例えば、数字だけでなく「数字が複数桁続くパターン」や「特定の文字で始まり特定の文字で終わるパターン」など、様々な条件を表現できます。

行頭・行末を表す記号の使い方

正規表現で文字列の位置を指定する際に、行頭や行末を表すメタ文字は非常に重要な役割を果たします。これらのアンカーと呼ばれる記号を使うことで、文字列全体の構造を検証したり、特定の位置にある文字列のみを対象にしたりすることができます。

「^」(ハット記号)による行頭指定

ハット記号「^」は、文字列または行の先頭位置を表すメタ文字です。この記号をパターンの最初に配置することで、文字列が特定のパターンで始まることを確認できます。

import java.util.regex.*;

public class StartWithExample {
    public static void main(String[] args) {
        String text1 = "Hello World";
        String text2 = "World Hello";
        
        Pattern pattern = Pattern.compile("^Hello");
        
        Matcher matcher1 = pattern.matcher(text1);
        System.out.println(matcher1.find()); // true
        
        Matcher matcher2 = pattern.matcher(text2);
        System.out.println(matcher2.find()); // false
    }
}

上記の例では、「^Hello」というパターンが「Hello」で始まる文字列のみにマッチします。text1は先頭が「Hello」なのでtrueを返しますが、text2は途中に「Hello」があっても先頭ではないためfalseを返します。

複数行モードでは、「^」は各行の先頭にマッチします。Pattern.MULTILINEフラグを使用することで、この動作を有効にできます。

String multilineText = "Hello World\nGoodbye World\nHello Again";
Pattern pattern = Pattern.compile("^Hello", Pattern.MULTILINE);
Matcher matcher = pattern.matcher(multilineText);

while (matcher.find()) {
    System.out.println("マッチ位置: " + matcher.start());
}
// 出力: マッチ位置: 0
// 出力: マッチ位置: 26

「$」(ドル記号)による行末指定

ドル記号「$」は、文字列または行の末尾位置を表すメタ文字です。パターンの最後に配置することで、文字列が特定のパターンで終わることを検証できます。

import java.util.regex.*;

public class EndWithExample {
    public static void main(String[] args) {
        String text1 = "example.txt";
        String text2 = "example.txt.bak";
        
        Pattern pattern = Pattern.compile("\\.txt$");
        
        Matcher matcher1 = pattern.matcher(text1);
        System.out.println(matcher1.find()); // true
        
        Matcher matcher2 = pattern.matcher(text2);
        System.out.println(matcher2.find()); // false
    }
}

この例では、「\.txt$」というパターンが「.txt」で終わる文字列のみにマッチします。ファイル名の拡張子チェックなど、実用的な場面でよく使用されます。

行頭と行末を組み合わせることで、文字列全体の形式を厳密に検証できます。

// 5桁の数字のみで構成される文字列を検証
Pattern pattern = Pattern.compile("^\\d{5}$");

System.out.println(pattern.matcher("12345").matches());  // true
System.out.println(pattern.matcher("123456").matches()); // false
System.out.println(pattern.matcher("1234").matches());   // false
System.out.println(pattern.matcher("a1234").matches());  // false

繰り返しを表す記号の使い方

正規表現の強力な機能の一つが、文字やパターンの繰り返しを表現できることです。繰り返しを表すメタ文字を使うことで、可変長の文字列パターンを簡潔に記述できます。主な繰り返し記号には「*」「+」「?」があり、それぞれ異なる繰り返し回数を表現します。

「*」による0回以上の繰り返し

アスタリスク「*」は、直前の文字またはパターンが0回以上繰り返されることを表します。つまり、該当する文字が存在しない場合でもマッチします。

import java.util.regex.*;

public class AsteriskExample {
    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("ab*c");
        
        System.out.println(pattern.matcher("ac").matches());     // true (bが0回)
        System.out.println(pattern.matcher("abc").matches());    // true (bが1回)
        System.out.println(pattern.matcher("abbc").matches());   // true (bが2回)
        System.out.println(pattern.matcher("abbbc").matches());  // true (bが3回)
        System.out.println(pattern.matcher("adc").matches());    // false
    }
}

実用例としては、空白文字の扱いに便利です。

// 空白が0個以上あるパターン
Pattern pattern = Pattern.compile("Hello *World");

System.out.println(pattern.matcher("HelloWorld").matches());      // true
System.out.println(pattern.matcher("Hello World").matches());     // true
System.out.println(pattern.matcher("Hello  World").matches());    // true
System.out.println(pattern.matcher("Hello   World").matches());   // true

「+」による1回以上の繰り返し

プラス記号「+」は、直前の文字またはパターンが1回以上繰り返されることを表します。「*」とは異なり、少なくとも1回は出現する必要があります。

import java.util.regex.*;

public class PlusExample {
    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("ab+c");
        
        System.out.println(pattern.matcher("ac").matches());     // false (bが0回)
        System.out.println(pattern.matcher("abc").matches());    // true (bが1回)
        System.out.println(pattern.matcher("abbc").matches());   // true (bが2回)
        System.out.println(pattern.matcher("abbbc").matches());  // true (bが3回)
    }
}

「+」は数値や英字の連続を表現する際に頻繁に使用されます。

// 1文字以上の数字が連続するパターン
Pattern pattern = Pattern.compile("\\d+");

Matcher matcher = pattern.matcher("価格は1000円です");
while (matcher.find()) {
    System.out.println("見つかった数値: " + matcher.group());
}
// 出力: 見つかった数値: 1000

「?」による0回または1回の出現

クエスチョンマーク「?」は、直前の文字またはパターンが0回または1回出現することを表します。つまり、その文字が「あってもなくてもよい」ことを意味し、オプショナルな要素を表現できます。

import java.util.regex.*;

public class QuestionExample {
    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("ab?c");
        
        System.out.println(pattern.matcher("ac").matches());     // true (bが0回)
        System.out.println(pattern.matcher("abc").matches());    // true (bが1回)
        System.out.println(pattern.matcher("abbc").matches());   // false (bが2回)
    }
}

実用的な例として、表記揺れを吸収する場合に便利です。

// 「colour」と「color」の両方にマッチ
Pattern pattern = Pattern.compile("colou?r");

System.out.println(pattern.matcher("color").matches());   // true
System.out.println(pattern.matcher("colour").matches());  // true

// 「https」と「http」の両方にマッチ
Pattern urlPattern = Pattern.compile("https?://");

System.out.println(urlPattern.matcher("http://example.com").find());  // true
System.out.println(urlPattern.matcher("https://example.com").find()); // true

また、繰り返し回数を明示的に指定したい場合は、中括弧「{}」を使用します。

// 繰り返し回数の指定例
Pattern pattern1 = Pattern.compile("a{3}");      // aがちょうど3回
Pattern pattern2 = Pattern.compile("a{2,5}");    // aが2回以上5回以下
Pattern pattern3 = Pattern.compile("a{3,}");     // aが3回以上

System.out.println(pattern1.matcher("aaa").matches());    // true
System.out.println(pattern1.matcher("aa").matches());     // false
System.out.println(pattern2.matcher("aaaa").matches());   // true
System.out.println(pattern3.matcher("aaaaaa").matches()); // true

その他の重要な記号

基本的なメタ文字に加えて、正規表現にはさらに高度なパターンマッチングを可能にする重要な記号があります。これらの記号を組み合わせることで、複雑な文字列パターンを効率的に表現できます。

「.」による任意の1文字表現

ドット「.」は、改行文字を除く任意の1文字にマッチする特殊な記号です。文字の種類を問わず、どのような文字でも受け入れることができます。

import java.util.regex.*;

public class DotExample {
    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("a.c");
        
        System.out.println(pattern.matcher("abc").matches());  // true
        System.out.println(pattern.matcher("a1c").matches());  // true
        System.out.println(pattern.matcher("a@c").matches());  // true
        System.out.println(pattern.matcher("a c").matches());  // true
        System.out.println(pattern.matcher("ac").matches());   // false (文字が足りない)
        System.out.println(pattern.matcher("abbc").matches()); // false (文字が多い)
    }
}

ドットは可変部分のある固定長パターンを表現する際に便利です。

// ファイル名パターン(3文字の拡張子)
Pattern pattern = Pattern.compile("file....\\.txt");

System.out.println(pattern.matcher("file0001.txt").matches()); // true
System.out.println(pattern.matcher("fileABCD.txt").matches()); // true

// 日付形式(YYYY-MM-DD)の簡易チェック
Pattern datePattern = Pattern.compile("....-..-..");

System.out.println(datePattern.matcher("2024-01-15").matches()); // true

注意点として、ドットは改行文字にはマッチしません。改行文字を含めたい場合は、Pattern.DOTALLフラグを使用する必要があります。

String text = "abc\ndef";

// 通常のドット(改行にマッチしない)
Pattern pattern1 = Pattern.compile("abc.def");
System.out.println(pattern1.matcher(text).matches()); // false

// DOTALLフラグを使用(改行にもマッチ)
Pattern pattern2 = Pattern.compile("abc.def", Pattern.DOTALL);
System.out.println(pattern2.matcher(text).matches()); // true

「|」による条件の選択

パイプ記号「|」は、複数のパターンのいずれか一つにマッチすることを表すOR条件です。複数の選択肢から一つを選ぶ場合に使用します。

import java.util.regex.*;

public class OrExample {
    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("cat|dog|bird");
        
        System.out.println(pattern.matcher("cat").matches());    // true
        System.out.println(pattern.matcher("dog").matches());    // true
        System.out.println(pattern.matcher("bird").matches());   // true
        System.out.println(pattern.matcher("fish").matches());   // false
    }
}

OR条件は複数の表記パターンを受け入れる際に有効です。

// 色の名前を検証
Pattern colorPattern = Pattern.compile("red|blue|green|yellow");

String[] colors = {"red", "blue", "purple", "green"};
for (String color : colors) {
    if (colorPattern.matcher(color).matches()) {
        System.out.println(color + "は有効な色です");
    }
}

// ファイル拡張子のチェック
Pattern filePattern = Pattern.compile(".*\\.(jpg|jpeg|png|gif)$");

System.out.println(filePattern.matcher("image.jpg").matches());  // true
System.out.println(filePattern.matcher("image.png").matches());  // true
System.out.println(filePattern.matcher("image.txt").matches());  // false

OR条件は部分的にも使用できます。

// 「Mr」「Ms」「Mrs」「Dr」のいずれかで始まる敬称
Pattern titlePattern = Pattern.compile("(Mr|Ms|Mrs|Dr)\\. [A-Z][a-z]+");

System.out.println(titlePattern.matcher("Mr. Smith").matches());  // true
System.out.println(titlePattern.matcher("Dr. Johnson").matches()); // true
System.out.println(titlePattern.matcher("Prof. Brown").matches()); // false

「()」によるグループ化

丸括弧「()」は、複数の文字やパターンを一つのグループとしてまとめる機能を持ちます。グループ化には主に3つの役割があります。

第一に、繰り返し記号の適用範囲を明確にできます。

import java.util.regex.*;

public class GroupExample {
    public static void main(String[] args) {
        // グループ化なし: 「c」のみが繰り返される
        Pattern pattern1 = Pattern.compile("abc+");
        System.out.println(pattern1.matcher("abcc").matches());    // true
        System.out.println(pattern1.matcher("abcabc").matches());  // false
        
        // グループ化あり: 「abc」全体が繰り返される
        Pattern pattern2 = Pattern.compile("(abc)+");
        System.out.println(pattern2.matcher("abc").matches());     // true
        System.out.println(pattern2.matcher("abcabc").matches());  // true
        System.out.println(pattern2.matcher("abcabcabc").matches()); // true
    }
}

第二に、OR条件の適用範囲を限定できます。

// グループ化なしの場合
Pattern pattern1 = Pattern.compile("apple|orange juice");
// これは「apple」または「orange juice」にマッチ

System.out.println(pattern1.matcher("apple").matches());        // true
System.out.println(pattern1.matcher("orange juice").matches()); // true
System.out.println(pattern1.matcher("apple juice").matches());  // false

// グループ化ありの場合
Pattern pattern2 = Pattern.compile("(apple|orange) juice");
// これは「apple juice」または「orange juice」にマッチ

System.out.println(pattern2.matcher("apple juice").matches());  // true
System.out.println(pattern2.matcher("orange juice").matches()); // true
System.out.println(pattern2.matcher("apple").matches());        // false

第三に、マッチした部分文字列をキャプチャして後から取り出すことができます。

// 日付からの年月日の抽出
Pattern pattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher matcher = pattern.matcher("2024-01-15");

if (matcher.matches()) {
    System.out.println("年: " + matcher.group(1));  // 2024
    System.out.println("月: " + matcher.group(2));  // 01
    System.out.println("日: " + matcher.group(3));  // 15
}

// メールアドレスからユーザー名とドメインを抽出
Pattern emailPattern = Pattern.compile("([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})");
Matcher emailMatcher = emailPattern.matcher("user@example.com");

if (emailMatcher.matches()) {
    System.out.println("ユーザー名: " + emailMatcher.group(1)); // user
    System.out.println("ドメイン: " + emailMatcher.group(2));   // example.com
}

キャプチャが不要な場合は、「(?:)」という非キャプチャグループを使用することで、パフォーマンスを向上させることができます。

// 非キャプチャグループの使用例
Pattern pattern = Pattern.compile("(?:http|https)://([a-z.]+)");
Matcher matcher = pattern.matcher("https://example.com");

if (matcher.matches()) {
    // group(1)は「example.com」を返す
    // 「http」や「https」はキャプチャされない
    System.out.println("ドメイン: " + matcher.group(1));
}

特殊文字のエスケープ処理

正規表現では、ドット「.」やアスタリスク「*」などの記号は特別な意味を持つメタ文字として扱われます。これらの記号を通常の文字として扱いたい場合は、エスケープ処理が必要になります。

Javaの正規表現では、バックスラッシュ「\」を使ってメタ文字をエスケープします。ただし、Javaの文字列リテラル内でバックスラッシュ自体もエスケープが必要なため、実際には二重のバックスラッシュ「\\」を記述する必要があります。

import java.util.regex.*;

public class EscapeExample {
    public static void main(String[] args) {
        // ドット「.」を通常の文字として扱う
        Pattern pattern1 = Pattern.compile("\\.");
        System.out.println(pattern1.matcher(".").matches());    // true
        System.out.println(pattern1.matcher("a").matches());    // false
        
        // 「example.com」という文字列にマッチ
        Pattern pattern2 = Pattern.compile("example\\.com");
        System.out.println(pattern2.matcher("example.com").matches());  // true
        System.out.println(pattern2.matcher("exampleXcom").matches());  // false
    }
}

エスケープが必要な主なメタ文字は以下の通りです。

メタ文字エスケープ後の記述用途例
.\\.ドメイン名、ファイル拡張子
*\\*アスタリスク記号そのもの
+\\+プラス記号、電話番号の「+81」など
?\\?クエスチョンマーク記号
^\\^キャレット記号そのもの
$\\$ドル記号、金額表記
|\\|パイプ記号そのもの
()\\(\\)括弧記号そのもの
[]\\[\\]角括弧記号そのもの
{}\\{\\}波括弧記号そのもの
\\\\\バックスラッシュそのもの

実用的なエスケープの例を見ていきましょう。

// 金額の表記($記号を含む)
Pattern pricePattern = Pattern.compile("\\$\\d+\\.\\d{2}");

System.out.println(pricePattern.matcher("$19.99").matches());  // true
System.out.println(pricePattern.matcher("$100.00").matches()); // true
System.out.println(pricePattern.matcher("19.99").matches());   // false

// ファイルパス(Windowsスタイル)
Pattern pathPattern = Pattern.compile("C:\\\\\\\\Users\\\\\\\\[^\\\\]+\\\\\\\\Documents");

System.out.println(pathPattern.matcher("C:\\Users\\John\\Documents").matches()); // true

// IPアドレス(ドットをエスケープ)
Pattern ipPattern = Pattern.compile("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}");

System.out.println(ipPattern.matcher("192.168.1.1").matches()); // true

// 括弧を含む電話番号
Pattern phonePattern = Pattern.compile("\\(\\d{3}\\) \\d{3}-\\d{4}");

System.out.println(phonePattern.matcher("(03) 1234-5678").matches()); // true

複数のメタ文字をエスケープする必要がある場合、Pattern.quoteメソッドを使うと便利です。このメソッドは、与えられた文字列を正規表現のリテラルパターンとして扱えるようにエスケープしてくれます。

// Pattern.quoteを使った例
String literal = "example.com (test)";
String quotedPattern = Pattern.quote(literal);

Pattern pattern = Pattern.compile(quotedPattern);
System.out.println(pattern.matcher("example.com (test)").matches()); // true

// 手動でエスケープした場合と同等
Pattern manualPattern = Pattern.compile("example\\.com \\(test\\)");
System.out.println(manualPattern.matcher("example.com (test)").matches()); // true

また、文字クラス「[]」の内部では、一部のメタ文字は特別な意味を失います。

// 文字クラス内では「.」はエスケープ不要
Pattern pattern1 = Pattern.compile("[.]");
System.out.println(pattern1.matcher(".").matches()); // true

// ただし「^」「-」「]」は文字クラス内でも特別な意味を持つ
Pattern pattern2 = Pattern.compile("[\\^\\-\\]]");
System.out.println(pattern2.matcher("^").matches()); // true
System.out.println(pattern2.matcher("-").matches()); // true
System.out.println(pattern2.matcher("]").matches()); // true

エスケープ処理を正しく行うことで、メタ文字を含む複雑な文字列パターンでも正確にマッチングできるようになります。特に、URLやファイルパス、数式など、記号を多く含む文字列を扱う際には、適切なエスケープ処理が不可欠です。

“`

“`html

PatternクラスとMatcherクラスの使い方

java+programming+code

Javaで正規表現を扱う際は、java.util.regexパッケージに含まれるPatternクラスとMatcherクラスを使用します。Patternクラスはコンパイルされた正規表現パターンを表し、Matcherクラスは実際のマッチング処理を実行します。この2つのクラスを組み合わせることで、効率的かつ柔軟な正規表現処理が可能になります。それぞれのクラスには特有の機能とメソッドがあり、用途に応じて使い分けることが重要です。

Patternクラスの基本機能

Patternクラスは正規表現パターンをコンパイルし、再利用可能な形で保持する役割を持ちます。一度コンパイルしたパターンは繰り返し利用できるため、パフォーマンスの向上が期待できます。また、各種フラグを指定することで、マッチング動作を細かく制御できます。

正規表現パターンのコンパイル方法

Patternクラスを使用するには、Pattern.compile()メソッドで正規表現パターンをコンパイルします。この処理により、文字列として記述された正規表現が内部的に最適化され、マッチング処理の効率が向上します。

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

// 基本的なパターンのコンパイル
Pattern pattern = Pattern.compile("[0-9]+");

// コンパイルしたパターンを使ってMatcherを生成
String text = "価格は1000円です";
Matcher matcher = pattern.matcher(text);

if (matcher.find()) {
    System.out.println("数値が見つかりました: " + matcher.group());
}

この例では、数値パターン[0-9]+をコンパイルし、文字列から数値を検索しています。Patternオブジェクトは一度作成すれば何度でも再利用できるため、同じパターンを繰り返し使用する場合に効率的です。

パターンの再利用とパフォーマンス

正規表現のコンパイルは比較的コストの高い処理です。そのため、同じパターンを複数回使用する場合は、Patternオブジェクトを再利用することでパフォーマンスが大幅に向上します。特にループ内で正規表現を使用する場合、この違いは顕著になります。

// 非効率な例:ループごとにコンパイルが発生
for (String text : textList) {
    if (text.matches("[0-9]+")) {  // 毎回コンパイルされる
        System.out.println(text);
    }
}

// 効率的な例:パターンを再利用
Pattern pattern = Pattern.compile("[0-9]+");
for (String text : textList) {
    Matcher matcher = pattern.matcher(text);
    if (matcher.matches()) {  // コンパイル済みパターンを使用
        System.out.println(text);
    }
}

上記の効率的な例では、Patternオブジェクトをループの外で一度だけ作成し、繰り返し使用しています。これにより、不必要なコンパイル処理が削減され、処理速度が向上します。

フラグによる動作制御

Patternクラスでは、コンパイル時にフラグを指定することで、マッチング動作を細かく制御できます。フラグはPattern.compile()メソッドの第2引数として指定します。

// 大文字小文字を区別しないフラグ
Pattern pattern1 = Pattern.compile("java", Pattern.CASE_INSENSITIVE);

// 複数行モード(^と$が各行の先頭と末尾にマッチ)
Pattern pattern2 = Pattern.compile("^start", Pattern.MULTILINE);

// ドットが改行にもマッチするモード
Pattern pattern3 = Pattern.compile("a.b", Pattern.DOTALL);

// 複数のフラグを組み合わせる
Pattern pattern4 = Pattern.compile("java", 
    Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);

主要なフラグには以下のようなものがあります:

  • Pattern.CASE_INSENSITIVE:大文字小文字を区別しないマッチング
  • Pattern.MULTILINE:複数行モードで^と$を行頭・行末として扱う
  • Pattern.DOTALL:ドット(.)が改行文字にもマッチする
  • Pattern.UNICODE_CASE:Unicodeを考慮した大文字小文字の区別
  • Pattern.COMMENTS:パターン内の空白とコメントを無視

Patternクラスの主要メソッド

Patternクラスには、Matcherオブジェクトを生成する以外にも、便利な静的メソッドがいくつか用意されています。これらのメソッドを使用することで、簡潔なコードで一般的な正規表現処理を実行できます。

splitメソッドによる文字列分割

split()メソッドは、正規表現パターンに一致する部分を区切り文字として、文字列を分割します。Stringクラスのsplit()メソッドと同様の機能ですが、Patternオブジェクトを再利用する場合に有効です。

Pattern pattern = Pattern.compile("[,、\\s]+");

String text1 = "りんご,みかん、バナナ ぶどう";
String[] fruits = pattern.split(text1);

// 結果: ["りんご", "みかん", "バナナ", "ぶどう"]
for (String fruit : fruits) {
    System.out.println(fruit);
}

// 分割数を制限する
String text2 = "A,B,C,D,E";
String[] parts = pattern.split(text2, 3);
// 結果: ["A", "B", "C,D,E"]

この例では、カンマ、読点、空白を区切り文字として文字列を分割しています。複数の区切り文字に対応できる点が特徴で、データの前処理やパース処理で頻繁に使用されます。

matchesメソッドによる全体一致判定

Patternクラスの静的メソッドmatches()は、文字列全体がパターンに一致するかを簡単に判定できます。一度だけ判定する場合に便利ですが、繰り返し使用する場合はPatternオブジェクトを作成して再利用する方が効率的です。

// 静的メソッドによる簡潔な記述
boolean result1 = Pattern.matches("[0-9]{3}-[0-9]{4}", "123-4567");
System.out.println(result1);  // true

boolean result2 = Pattern.matches("[0-9]{3}-[0-9]{4}", "12-4567");
System.out.println(result2);  // false

// 複数回使用する場合はPatternオブジェクトを再利用
Pattern zipPattern = Pattern.compile("[0-9]{3}-[0-9]{4}");
System.out.println(zipPattern.matcher("123-4567").matches());  // true
System.out.println(zipPattern.matcher("456-7890").matches());  // true

注意点として、このメソッドは文字列全体の一致を判定するため、部分一致を検索したい場合は後述するMatcherクラスのfind()メソッドを使用します。

asPredicateによる判定用Predicate生成

Java 8以降では、asPredicate()メソッドを使用して、PatternオブジェクトからPredicate関数を生成できます。これにより、StreamAPIと組み合わせた関数型プログラミングが可能になります。

import java.util.List;
import java.util.stream.Collectors;

Pattern emailPattern = Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");

List<String> addresses = List.of(
    "user@example.com",
    "invalid-email",
    "admin@test.co.jp",
    "not_an_email"
);

// Predicateとしてfilterに渡す
List<String> validEmails = addresses.stream()
    .filter(emailPattern.asPredicate())
    .collect(Collectors.toList());

System.out.println(validEmails);
// 結果: [user@example.com, admin@test.co.jp]

この機能は、コレクションのフィルタリングやデータ検証処理をシンプルに記述できるため、モダンなJavaコードでは頻繁に活用されます。

Matcherクラスの基本機能

Matcherクラスは、コンパイルされたパターンを使用して実際のマッチング処理を実行します。Patternオブジェクトから生成され、対象文字列に対する様々なマッチング操作を提供します。マッチング結果の取得や、複数マッチの走査など、柔軟な処理が可能です。

Matcherオブジェクトの生成方法

Matcherオブジェクトは、Patternオブジェクトのmatcher()メソッドに検索対象の文字列を渡すことで生成します。生成されたMatcherは、指定された文字列に対してマッチング処理を実行できます。

// Patternオブジェクトの作成
Pattern pattern = Pattern.compile("[0-9]+");

// Matcherオブジェクトの生成
String text = "商品コード:12345、価格:6789円";
Matcher matcher = pattern.matcher(text);

// 同じPatternから別のMatcherを生成
String text2 = "電話番号:090-1234-5678";
Matcher matcher2 = pattern.matcher(text2);

Matcherオブジェクトは検索対象文字列と紐づいているため、異なる文字列に対してマッチング処理を行う場合は、新しいMatcherオブジェクトを生成する必要があります。ただし、reset()メソッドを使用することで、既存のMatcherオブジェクトを別の文字列で再利用することも可能です。

Pattern pattern = Pattern.compile("[0-9]+");
Matcher matcher = pattern.matcher("最初の文字列123");

// 同じMatcherを別の文字列で再利用
matcher.reset("新しい文字列456");
if (matcher.find()) {
    System.out.println(matcher.group());  // 456
}

Matcherクラスのマッチング操作

Matcherクラスには、マッチング方法の異なる複数のメソッドが用意されています。文字列全体の一致を確認する場合、先頭からの一致を確認する場合、部分一致を検索する場合など、目的に応じて適切なメソッドを選択することが重要です。

matchesによる全体一致の確認

matches()メソッドは、対象文字列全体がパターンに一致するかを判定します。文字列の先頭から末尾まで完全にパターンと一致する場合のみtrueを返します。

Pattern pattern = Pattern.compile("[0-9]{3}-[0-9]{4}");
Matcher matcher1 = pattern.matcher("123-4567");
System.out.println(matcher1.matches());  // true(全体が一致)

Matcher matcher2 = pattern.matcher("郵便番号は123-4567です");
System.out.println(matcher2.matches());  // false(部分一致のみ)

Matcher matcher3 = pattern.matcher("12-4567");
System.out.println(matcher3.matches());  // false(パターンが一致しない)

このメソッドは、入力値の形式検証やバリデーション処理で頻繁に使用されます。例えば、郵便番号や電話番号が正しい形式かどうかを判定する際に便利です。

lookingAtによる先頭一致の確認

lookingAt()メソッドは、文字列の先頭からパターンに一致するかを判定します。matches()と異なり、文字列全体が一致する必要はなく、先頭部分のみが一致すればtrueを返します。

Pattern pattern = Pattern.compile("[0-9]+");

Matcher matcher1 = pattern.matcher("12345abc");
System.out.println(matcher1.lookingAt());  // true(先頭が数値)

Matcher matcher2 = pattern.matcher("abc12345");
System.out.println(matcher2.lookingAt());  // false(先頭が数値でない)

Matcher matcher3 = pattern.matcher("999");
System.out.println(matcher3.lookingAt());  // true(先頭が数値)

このメソッドは、文字列が特定のパターンで始まるかを確認したい場合に有効です。例えば、コマンドラインの入力が特定のプレフィックスで始まるかを判定する際などに使用されます。

findによる部分一致の検索

find()メソッドは、文字列内でパターンに一致する部分を検索します。最も柔軟性が高く、複数のマッチを順次検索できる点が特徴です。メソッドを繰り返し呼び出すことで、すべてのマッチング箇所を取得できます。

Pattern pattern = Pattern.compile("[0-9]+");
String text = "商品A:1000円、商品B:2500円、商品C:500円";
Matcher matcher = pattern.matcher(text);

// すべての数値を検索
while (matcher.find()) {
    System.out.println("見つかった数値: " + matcher.group());
}
// 出力:
// 見つかった数値: 1000
// 見つかった数値: 2500
// 見つかった数値: 500

また、find()メソッドには開始位置を指定できるオーバーロード版も存在します。

Pattern pattern = Pattern.compile("[0-9]+");
String text = "ABC123DEF456GHI789";
Matcher matcher = pattern.matcher(text);

// インデックス10から検索開始
if (matcher.find(10)) {
    System.out.println(matcher.group());  // 456
}

find()メソッドは、ログファイルの解析、データ抽出、テキストマイニングなど、実務で最も頻繁に使用されるメソッドの一つです。部分一致を検索できるため、非構造化データからの情報抽出に適しています。

メソッドマッチング範囲主な用途
matches()文字列全体形式検証、バリデーション
lookingAt()文字列の先頭プレフィックス判定、先頭パターン確認
find()文字列内の任意の位置部分一致検索、データ抽出、複数マッチの走査

“`

“`html

グループとキャプチャリングの活用

java+regex+code

Javaの正規表現では、パターン内の特定部分を括弧「()」で囲むことでグループ化できます。グループ化することで、マッチした文字列の一部だけを抽出したり、後から参照したりできるようになります。これをキャプチャリングと呼び、複雑な文字列処理を効率的に行うための重要な機能です。本セクションでは、グループとキャプチャリングの基本から実践的な活用方法まで詳しく解説します。

グループ化の基本概念

正規表現におけるグループ化は、パターンの一部を括弧「()」で囲むことで実現します。グループ化には主に2つの目的があります。1つ目は、量指定子の適用範囲を明確にすること、2つ目はマッチした部分文字列を後から取り出せるようにすることです。

例えば、「(abc)+」というパターンでは、「abc」という3文字の組み合わせが1回以上繰り返されることを表します。括弧がない場合の「abc+」では「c」だけが繰り返しの対象になりますが、括弧でグループ化することで「abc」全体が繰り返しの対象となります。

import java.util.regex.*;

public class GroupExample {
    public static void main(String[] args) {
        String text = "abcabcabc";
        
        // グループ化した例
        Pattern pattern1 = Pattern.compile("(abc)+");
        Matcher matcher1 = pattern1.matcher(text);
        System.out.println(matcher1.matches()); // true
        
        // グループ化しない例
        Pattern pattern2 = Pattern.compile("abc+");
        Matcher matcher2 = pattern2.matcher(text);
        System.out.println(matcher2.matches()); // false
    }
}

また、グループ化されたパターンにマッチした部分は自動的にキャプチャされ、後からプログラム内で取り出すことができます。この機能により、文字列から必要な情報だけを効率的に抽出できるようになります。

グループ番号によるキャプチャ

正規表現内で括弧を使ってグループ化すると、各グループには左から順に番号が自動的に割り当てられます。この番号はグループ番号と呼ばれ、1から始まります。グループ番号0は特別で、パターン全体にマッチした文字列を表します。

グループ番号の割り当ては、括弧の開始位置「(」が左から出現する順序に基づいて決まります。ネストされたグループがある場合でも、外側の括弧から順に番号が付けられていきます。

import java.util.regex.*;

public class GroupNumberExample {
    public static void main(String[] args) {
        String text = "2024-01-15";
        Pattern pattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
        Matcher matcher = pattern.matcher(text);
        
        if (matcher.matches()) {
            System.out.println("全体: " + matcher.group(0));  // 2024-01-15
            System.out.println("年: " + matcher.group(1));    // 2024
            System.out.println("月: " + matcher.group(2));    // 01
            System.out.println("日: " + matcher.group(3));    // 15
        }
    }
}

上記の例では、日付文字列から年・月・日を個別に抽出しています。3つの括弧がそれぞれグループ1、2、3として認識され、各部分を簡単に取り出せます。

ネストされたグループの場合は、以下のように番号が割り当てられます。

String text = "abc123def";
Pattern pattern = Pattern.compile("((abc)(\\d+)(def))");
Matcher matcher = pattern.matcher(text);

if (matcher.matches()) {
    System.out.println("グループ1: " + matcher.group(1));  // abc123def(外側全体)
    System.out.println("グループ2: " + matcher.group(2));  // abc
    System.out.println("グループ3: " + matcher.group(3));  // 123
    System.out.println("グループ4: " + matcher.group(4));  // def
}

グループ名を使った名前付きキャプチャ

グループ番号による参照は便利ですが、正規表現が複雑になるとどの番号がどの部分を表すか分かりにくくなるという問題があります。この問題を解決するのが名前付きキャプチャです。

名前付きキャプチャは、「(?<名前>パターン)」という構文を使用します。グループに意味のある名前を付けることで、コードの可読性が大幅に向上し、保守性も高まります。

import java.util.regex.*;

public class NamedGroupExample {
    public static void main(String[] args) {
        String text = "田中太郎:tanaka@example.com";
        Pattern pattern = Pattern.compile("(?<name>[^:]+):(?<email>[^@]+@[^@]+)");
        Matcher matcher = pattern.matcher(text);
        
        if (matcher.matches()) {
            System.out.println("名前: " + matcher.group("name"));    // 田中太郎
            System.out.println("メール: " + matcher.group("email"));  // tanaka@example.com
        }
    }
}

名前付きキャプチャを使用すると、グループ番号を覚える必要がなく、直感的に目的のデータを取得できます。特にチームでの開発や、後からコードを見直す際に有効です。

日付パターンの例を名前付きキャプチャで書き直すと、以下のようになります。

String text = "2024-01-15";
Pattern pattern = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher matcher = pattern.matcher(text);

if (matcher.matches()) {
    System.out.println("年: " + matcher.group("year"));    // 2024
    System.out.println("月: " + matcher.group("month"));   // 01
    System.out.println("日: " + matcher.group("day"));     // 15
}

なお、名前付きキャプチャを使用しても、従来のグループ番号による参照も引き続き利用できます。

groupメソッドによるキャプチャ結果の取得

Matcherクラスのgroupメソッドは、キャプチャされた文字列を取得するための中心的なメソッドです。このメソッドには複数のバリエーションがあり、用途に応じて使い分けることができます。

groupメソッドの主な使用形式は以下の通りです。

  • group():引数なしの場合、マッチした文字列全体を返します(group(0)と同じ)
  • group(int group):指定したグループ番号のキャプチャ結果を返します
  • group(String name):指定した名前のキャプチャ結果を返します
  • groupCount():パターン内のキャプチャグループの総数を返します
import java.util.regex.*;

public class GroupMethodExample {
    public static void main(String[] args) {
        String text = "商品コード: ABC-123-XYZ";
        Pattern pattern = Pattern.compile("商品コード: ([A-Z]+)-(\\d+)-([A-Z]+)");
        Matcher matcher = pattern.matcher(text);
        
        if (matcher.find()) {
            // マッチした全体を取得
            System.out.println("全体: " + matcher.group());
            
            // グループ数を確認
            System.out.println("グループ数: " + matcher.groupCount());  // 3
            
            // 各グループを取得
            for (int i = 1; i = matcher.groupCount(); i++) {
                System.out.println("グループ" + i + ": " + matcher.group(i));
            }
        }
    }
}

重要な注意点として、groupメソッドはfind()matches()lookingAt()のいずれかのマッチング操作が成功した後でなければ呼び出せません。マッチング前にgroupメソッドを呼び出すと、IllegalStateExceptionがスローされます。

String text = "test123";
Pattern pattern = Pattern.compile("(test)(\\d+)");
Matcher matcher = pattern.matcher(text);

// NG: マッチング前のgroup呼び出し
// System.out.println(matcher.group(1)); // IllegalStateException

// OK: マッチング後のgroup呼び出し
if (matcher.matches()) {
    System.out.println(matcher.group(1));  // test
    System.out.println(matcher.group(2));  // 123
}

また、findメソッドを繰り返し呼び出す場合、各回のfind実行ごとに新しいマッチ結果がキャプチャされます。これを利用して、文字列内の複数のパターンを順次処理できます。

String text = "電話: 03-1234-5678、FAX: 03-9876-5432";
Pattern pattern = Pattern.compile("(\\d{2})-(\\d{4})-(\\d{4})");
Matcher matcher = pattern.matcher(text);

while (matcher.find()) {
    System.out.println("市外局番: " + matcher.group(1));
    System.out.println("市内局番: " + matcher.group(2));
    System.out.println("加入者番号: " + matcher.group(3));
    System.out.println("---");
}

マッチング位置の取得方法

キャプチャした文字列だけでなく、その文字列が元のテキスト内のどの位置にあるかを知りたい場合があります。Matcherクラスは、マッチした位置を取得するためのメソッドも提供しています。

位置情報を取得する主なメソッドは以下の通りです。

  • start():マッチした文字列全体の開始インデックスを返します
  • end():マッチした文字列全体の終了インデックス(次の文字の位置)を返します
  • start(int group):指定グループの開始インデックスを返します
  • end(int group):指定グループの終了インデックスを返します
  • start(String name):名前付きグループの開始インデックスを返します
  • end(String name):名前付きグループの終了インデックスを返します
import java.util.regex.*;

public class PositionExample {
    public static void main(String[] args) {
        String text = "価格は1000円です";
        Pattern pattern = Pattern.compile("(\\d+)(円)");
        Matcher matcher = pattern.matcher(text);
        
        if (matcher.find()) {
            System.out.println("マッチ全体: " + matcher.group());
            System.out.println("開始位置: " + matcher.start());      // 3
            System.out.println("終了位置: " + matcher.end());        // 8
            
            System.out.println("数値部分: " + matcher.group(1));
            System.out.println("数値開始位置: " + matcher.start(1)); // 3
            System.out.println("数値終了位置: " + matcher.end(1));   // 7
            
            System.out.println("単位部分: " + matcher.group(2));
            System.out.println("単位開始位置: " + matcher.start(2)); // 7
            System.out.println("単位終了位置: " + matcher.end(2));   // 8
        }
    }
}

位置情報は、マッチした部分をハイライト表示したり、前後の文脈を取得したりする際に便利です。例えば、検索結果を表示する際に、マッチした単語の前後数文字を含めて表示する場合などに活用できます。

String text = "Javaは強力なプログラミング言語です。Javaを学ぶことで多くの可能性が広がります。";
Pattern pattern = Pattern.compile("Java");
Matcher matcher = pattern.matcher(text);

while (matcher.find()) {
    int start = Math.max(0, matcher.start() - 5);
    int end = Math.min(text.length(), matcher.end() + 5);
    String context = text.substring(start, end);
    
    System.out.println("位置 " + matcher.start() + ": ..." + context + "...");
}

また、複数のマッチが見つかった場合、各マッチの位置を記録しておくことで、後から元のテキストを加工する際に役立ちます。

String text = "エラー: 404, エラー: 500, エラー: 403";
Pattern pattern = Pattern.compile("エラー: (\\d+)");
Matcher matcher = pattern.matcher(text);

while (matcher.find()) {
    System.out.println(String.format(
        "位置%d-%d: エラーコード%sを検出",
        matcher.start(),
        matcher.end(),
        matcher.group(1)
    ));
}

名前付きキャプチャを使用した場合も、同様に位置情報を取得できます。

String text = "ユーザー名: admin, パスワード: secret";
Pattern pattern = Pattern.compile("ユーザー名: (?<user>\\w+), パスワード: (?<pass>\\w+)");
Matcher matcher = pattern.matcher(text);

if (matcher.find()) {
    System.out.println("ユーザー名の位置: " + matcher.start("user") + "-" + matcher.end("user"));
    System.out.println("パスワードの位置: " + matcher.start("pass") + "-" + matcher.end("pass"));
}

これらの位置取得メソッドも、groupメソッドと同様にマッチング操作が成功した後でなければ呼び出せない点に注意が必要です。

“`

“`html

正規表現による文字列の置換と操作

java+regex+code

Javaの正規表現では、文字列のマッチングだけでなく、パターンに基づいた置換操作も強力にサポートされています。MatcherクラスやStringクラスが提供するメソッドを活用することで、複雑な文字列変換処理を簡潔に記述できます。このセクションでは、正規表現を使った置換操作の基本から応用まで、実践的なテクニックを解説します。

replaceAllとreplaceFirstによる置換

Javaでは、replaceAllメソッドとreplaceFirstメソッドを使って、正規表現パターンにマッチする部分を別の文字列に置き換えることができます。replaceAllはマッチするすべての箇所を置換し、replaceFirstは最初にマッチした箇所のみを置換します。

これらのメソッドは、Stringクラスに直接実装されているため、簡単に利用できます。基本的な使い方は以下の通りです。

String text = "2024年1月15日、2024年2月20日、2024年3月25日";

// すべての日付形式を統一形式に置換
String result1 = text.replaceAll("(\\d{4})年(\\d{1,2})月(\\d{1,2})日", "$1/$2/$3");
System.out.println(result1);
// 出力: 2024/1/15、2024/2/20、2024/3/25

// 最初にマッチした箇所のみを置換
String result2 = text.replaceFirst("(\\d{4})年(\\d{1,2})月(\\d{1,2})日", "$1/$2/$3");
System.out.println(result2);
// 出力: 2024/1/15、2024年2月20日、2024年3月25日

Matcherクラスを使った置換も可能で、より細かい制御が必要な場合に有効です。

String text = "価格は1000円、特別価格は500円です";
Pattern pattern = Pattern.compile("(\\d+)円");
Matcher matcher = pattern.matcher(text);

// すべての金額を税込価格に変換
String result = matcher.replaceAll("$1円(税込)");
System.out.println(result);
// 出力: 価格は1000円(税込)、特別価格は500円(税込)です

replaceAllとreplaceFirstの使い分けは、処理対象の範囲によって判断します。複数箇所を一括変換したい場合はreplaceAll、最初の1件のみ変更したい場合はreplaceFirstを選択するのが基本です。

キャプチャグループを使った置換

正規表現の強力な機能の一つが、キャプチャグループを使った置換です。パターン内で括弧「()」で囲んだ部分をキャプチャし、置換文字列内で「$1」「$2」といった形式で参照することができます。これにより、マッチした文字列の一部を保持しながら変換処理を行えます。

キャプチャグループを活用すると、文字列の並び替えや部分的な変換が簡単に実現できます。

// 姓名の順序を入れ替える
String name = "山田太郎";
String reversed = name.replaceAll("(..)(..)$", "$2 $1");
System.out.println(reversed);
// 出力: 太郎 山田

// 電話番号の形式を変換
String phone = "03-1234-5678";
String formatted = phone.replaceAll("(\\d{2,4})-(\\d{4})-(\\d{4})", "($1) $2-$3");
System.out.println(formatted);
// 出力: (03) 1234-5678

// メールアドレスのドメイン部分のみを変更
String email = "user@example.com";
String newEmail = email.replaceAll("(.+)@.+", "$1@newdomain.com");
System.out.println(newEmail);
// 出力: user@newdomain.com

キャプチャグループは複数使用できるため、複雑な変換パターンにも対応可能です。

// 日付形式の変換(YYYY-MM-DD → DD/MM/YYYY)
String date = "2024-12-25";
String converted = date.replaceAll("(\\d{4})-(\\d{2})-(\\d{2})", "$3/$2/$1");
System.out.println(converted);
// 出力: 25/12/2024

// HTMLタグの属性を変換
String html = "<img src='old.jpg' width='100'>";
String updated = html.replaceAll("src='([^']+)'", "src='images/$1'");
System.out.println(updated);
// 出力: <img src='images/old.jpg' width='100'>

キャプチャグループの番号は、左括弧の出現順に1から自動的に割り当てられます。ネストした括弧がある場合も、外側から内側へと順番に番号が付けられるため、複雑なパターンでも確実に参照できます。

置換時の後方参照の活用

後方参照は、キャプチャグループで取得した内容を置換文字列内で再利用する技術です。「$1」「$2」といった記法で、キャプチャした部分を参照できます。この機能を活用することで、元の文字列の構造を保ちながら、必要な部分だけを変更する柔軟な置換が可能になります。

後方参照を使った実践的な例を見ていきましょう。

// 重複した単語を1つにまとめる
String text = "これはは重要な重要な情報です";
String fixed = text.replaceAll("(.)\\1+", "$1");
System.out.println(fixed);
// 出力: これは重要な情報です

// URLのプロトコル部分を統一
String url = "http://example.com/page";
String secureUrl = url.replaceAll("^http://(.+)", "https://$1");
System.out.println(secureUrl);
// 出力: https://example.com/page

// カンマ区切りの数値にスペースを追加
String numbers = "100,200,300,400";
String spaced = numbers.replaceAll("(\\d+),(\\d+)", "$1, $2");
System.out.println(spaced);
// 出力: 100, 200, 300, 400

後方参照は、同じパターンの繰り返しを検出する際にも有効です。

// 連続する同じ文字を検出して置換
String text = "あああいいいうううえええおおお";
String result = text.replaceAll("(.)\\1{2,}", "[$1×複数]");
System.out.println(result);
// 出力: [あ×複数][い×複数][う×複数][え×複数][お×複数]

// クォートで囲まれた文字列を抽出して変換
String code = "String name = \"太郎\"; String age = \"25\";";
String escaped = code.replaceAll("\"([^\"]+)\"", "'$1'");
System.out.println(escaped);
// 出力: String name = '太郎'; String age = '25';

複数のキャプチャグループを組み合わせた後方参照により、より複雑な変換も実現できます。

// ファイルパスから拡張子を変更
String path = "/home/user/document.txt";
String newPath = path.replaceAll("(.+)\\.([^.]+)$", "$1.pdf");
System.out.println(newPath);
// 出力: /home/user/document.pdf

// CSSのカラーコードを変換(省略形を完全形に)
String css = "color: #fff; background: #f0f;";
String expanded = css.replaceAll("#([0-9a-f])([0-9a-f])([0-9a-f])(?![0-9a-f])", "#$1$1$2$2$3$3");
System.out.println(expanded);
// 出力: color: #ffffff; background: #f0f;

後方参照を使う際の注意点として、キャプチャグループの番号は必ず存在するものを指定する必要があります。存在しない番号を参照するとエラーになるため、パターン内の括弧の数を確認しながら記述することが重要です。また、「$0」は特別な参照で、マッチした文字列全体を表します。

// $0を使ってマッチした全体を活用
String text = "重要 注意 警告";
String highlighted = text.replaceAll("重要|注意|警告", "【$0】");
System.out.println(highlighted);
// 出力: 【重要】 【注意】 【警告】

“`

“`html

実践的な正規表現パターンのサンプル集

java+regex+programming

Javaで正規表現を活用する際、よく使われる実用的なパターンを理解しておくことで、開発効率を大幅に向上させることができます。このセクションでは、実務でよく遭遇する具体的な検証パターンについて、動作するサンプルコードとともに詳しく解説していきます。

数値・金額のパターン

数値や金額の入力検証は、業務システムやECサイトなどで頻繁に必要となる処理です。整数や小数、カンマ区切りの金額など、用途に応じたパターンを使い分けることが重要です。

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

// 整数のパターン(正・負の両方に対応)
Pattern intPattern = Pattern.compile("^-?\\d+$");
System.out.println(intPattern.matcher("12345").matches());  // true
System.out.println(intPattern.matcher("-678").matches());   // true

// 小数のパターン(小数点以下2桁まで)
Pattern decimalPattern = Pattern.compile("^-?\\d+(\\.\\d{1,2})?$");
System.out.println(decimalPattern.matcher("123.45").matches());  // true
System.out.println(decimalPattern.matcher("100").matches());     // true

// 金額のパターン(3桁カンマ区切り)
Pattern pricePattern = Pattern.compile("^\\d{1,3}(,\\d{3})*$");
System.out.println(pricePattern.matcher("1,234,567").matches());  // true
System.out.println(pricePattern.matcher("500").matches());        // true

// 円マーク付き金額のパターン
Pattern yenPattern = Pattern.compile("^¥\\d{1,3}(,\\d{3})*$");
System.out.println(yenPattern.matcher("¥10,000").matches());  // true

日付・時刻のパターン

日付や時刻の検証は、予約システムやスケジュール管理など多くのアプリケーションで必要になります。基本的な書式チェックとしては正規表現が有効ですが、実際の日付の妥当性(うるう年や月の日数など)を厳密に検証する場合は、正規表現でマッチングした後にjava.time APIを使った追加検証を組み合わせることが推奨されます。

// YYYY/MM/DD形式の日付パターン
Pattern datePattern1 = Pattern.compile("^\\d{4}/(0[1-9]|1[0-2])/(0[1-9]|[12]\\d|3[01])$");
System.out.println(datePattern1.matcher("2024/03/15").matches());  // true

// YYYY-MM-DD形式の日付パターン
Pattern datePattern2 = Pattern.compile("^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$");
System.out.println(datePattern2.matcher("2024-03-15").matches());  // true

// HH:MM:SS形式の時刻パターン(24時間制)
Pattern timePattern = Pattern.compile("^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d$");
System.out.println(timePattern.matcher("14:30:45").matches());  // true

// 日時結合パターン(YYYY-MM-DD HH:MM:SS)
Pattern datetimePattern = Pattern.compile("^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]) ([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d$");
System.out.println(datetimePattern.matcher("2024-03-15 14:30:45").matches());  // true

郵便番号のパターン(日本国内)

日本の郵便番号は「3桁-4桁」の形式が標準です。ハイフンありとハイフンなしの両方に対応できるパターンを用意しておくと便利です。

// ハイフンありの郵便番号パターン(XXX-XXXX)
Pattern zipWithHyphen = Pattern.compile("^\\d{3}-\\d{4}$");
System.out.println(zipWithHyphen.matcher("100-0001").matches());  // true

// ハイフンなしの郵便番号パターン(7桁)
Pattern zipWithoutHyphen = Pattern.compile("^\\d{7}$");
System.out.println(zipWithoutHyphen.matcher("1000001").matches());  // true

// ハイフンあり・なし両対応の郵便番号パターン
Pattern zipFlexible = Pattern.compile("^\\d{3}-?\\d{4}$");
System.out.println(zipFlexible.matcher("100-0001").matches());  // true
System.out.println(zipFlexible.matcher("1000001").matches());   // true

電話番号のパターン(日本国内)

日本の電話番号は、固定電話、携帯電話、フリーダイヤルなど種類が多様で、桁数や形式も異なります。利用シーンに応じて適切なパターンを選択することが大切です。

// 携帯電話番号のパターン(090, 080, 070開始)
Pattern mobilePattern = Pattern.compile("^0[789]0-\\d{4}-\\d{4}$");
System.out.println(mobilePattern.matcher("090-1234-5678").matches());  // true

// ハイフンなし携帯電話番号
Pattern mobileNoHyphen = Pattern.compile("^0[789]0\\d{8}$");
System.out.println(mobileNoHyphen.matcher("09012345678").matches());  // true

// 固定電話番号のパターン(市外局番2~4桁)
Pattern fixedPattern = Pattern.compile("^0\\d{1,3}-\\d{1,4}-\\d{4}$");
System.out.println(fixedPattern.matcher("03-1234-5678").matches());   // true
System.out.println(fixedPattern.matcher("0120-123-456").matches());   // true

// フリーダイヤルのパターン(0120または0800)
Pattern freeDial = Pattern.compile("^0(120|800)-\\d{3}-\\d{3}$");
System.out.println(freeDial.matcher("0120-123-456").matches());  // true

// 電話番号の柔軟なパターン(ハイフンあり・なし両対応)
Pattern phoneFlexible = Pattern.compile("^0\\d{1,3}-?\\d{1,4}-?\\d{4}$");
System.out.println(phoneFlexible.matcher("03-1234-5678").matches());  // true
System.out.println(phoneFlexible.matcher("0312345678").matches());    // true

メールアドレスのパターン

メールアドレスの完全な検証は非常に複雑ですが、実用レベルでは基本的な構造を検証するシンプルなパターンが広く使われています。一般的な形式である「ローカル部@ドメイン部」の構造をチェックするパターンを紹介します。

// 基本的なメールアドレスパターン
Pattern emailBasic = Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
System.out.println(emailBasic.matcher("user@example.com").matches());        // true
System.out.println(emailBasic.matcher("test.user+tag@sub.domain.co.jp").matches());  // true

// より厳密なメールアドレスパターン
Pattern emailStrict = Pattern.compile("^[a-zA-Z0-9_+-]+(\\.[a-zA-Z0-9_+-]+)*@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*\\.[a-zA-Z]{2,}$");
System.out.println(emailStrict.matcher("example@test.com").matches());  // true
パターンの要素説明
[a-zA-Z0-9._%+-]+ローカル部:英数字と一部の記号を許可
@アットマーク(必須)
[a-zA-Z0-9.-]+ドメイン部:英数字とハイフン、ドットを許可
\\.[a-zA-Z]{2,}トップレベルドメイン:2文字以上のアルファベット

IPアドレスのパターン

IPアドレスの検証は、ネットワーク関連の設定や管理画面で頻繁に必要となります。IPv4とIPv6では形式が大きく異なるため、それぞれに適したパターンを使用する必要があります。

IPv4アドレスの検証

IPv4アドレスは「0~255」の数値を4つ、ドットで区切った形式です。単純に「\d{1,3}」を4つ並べただけでは「999.999.999.999」のような不正な値もマッチしてしまうため、各オクテットの範囲を正確に検証する必要があります。

// IPv4アドレスの基本パターン(簡易版)
Pattern ipv4Simple = Pattern.compile("^(\\d{1,3}\\.){3}\\d{1,3}$");
System.out.println(ipv4Simple.matcher("192.168.1.1").matches());  // true

// IPv4アドレスの厳密パターン(0-255の範囲検証)
Pattern ipv4Strict = Pattern.compile(
    "^((25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.){3}" +
    "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)$"
);
System.out.println(ipv4Strict.matcher("192.168.1.1").matches());   // true
System.out.println(ipv4Strict.matcher("255.255.255.255").matches());  // true
System.out.println(ipv4Strict.matcher("256.1.1.1").matches());     // false

厳密パターンでは、各オクテットを以下のように分類して検証しています。

  • 25[0-5]:250~255の範囲
  • 2[0-4]\d:200~249の範囲
  • 1\d{2}:100~199の範囲
  • [1-9]?\d:0~99の範囲

IPv6アドレスの検証

IPv6アドレスは16進数を8グループ、コロンで区切った形式です。省略記法(::)や混在記法(IPv4併記)など複数の表記方法があり、完全な検証は非常に複雑になります。

// IPv6アドレスの基本パターン(完全表記)
Pattern ipv6Full = Pattern.compile("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$");
System.out.println(ipv6Full.matcher("2001:0db8:0000:0000:0000:ff00:0042:8329").matches());  // true

// IPv6アドレスの省略記法対応パターン(簡易版)
Pattern ipv6Simple = Pattern.compile(
    "^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|" +  // 完全表記
    "^::([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}$|" +  // 先頭省略
    "^([0-9a-fA-F]{1,4}:){1,7}:$"  // 末尾省略
);
System.out.println(ipv6Simple.matcher("2001:db8::1").matches());  // true
System.out.println(ipv6Simple.matcher("::1").matches());          // true

URLのパターン

URLの検証は、Webアプリケーションでリンクやリダイレクト先を検証する際に必要です。プロトコル、ドメイン、パス、クエリパラメータなど、URLの構成要素に応じたパターンを使用します。

// 基本的なURLパターン(http/https)
Pattern urlBasic = Pattern.compile("^https?://[a-zA-Z0-9.-]+(:[0-9]+)?(/.*)?$");
System.out.println(urlBasic.matcher("https://example.com").matches());  // true
System.out.println(urlBasic.matcher("http://example.com:8080/path").matches());  // true

// より詳細なURLパターン
Pattern urlDetailed = Pattern.compile(
    "^(https?|ftp)://" +  // プロトコル
    "([a-zA-Z0-9.-]+)" +  // ドメイン
    "(:[0-9]{1,5})?" +    // ポート番号(省略可)
    "(/[^\\s]*)?" +       // パス(省略可)
    "(\\?[^\\s]*)?" +     // クエリパラメータ(省略可)
    "(#[^\\s]*)?$"        // フラグメント(省略可)
);
System.out.println(urlDetailed.matcher("https://example.com/path?param=value#section").matches());  // true

// ドメインのみのパターン
Pattern domainPattern = Pattern.compile("^[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
System.out.println(domainPattern.matcher("example.com").matches());  // true
System.out.println(domainPattern.matcher("sub.example.co.jp").matches());  // true

英数字の検証パターン

ユーザーIDやパスワード、プロダクトコードなど、英数字の組み合わせを検証する場面は非常に多くあります。文字種の制限や文字数の指定など、セキュリティ要件に応じたパターンを使用します。

// 英数字のみ(大文字・小文字・数字)
Pattern alphanumeric = Pattern.compile("^[a-zA-Z0-9]+$");
System.out.println(alphanumeric.matcher("User123").matches());  // true

// 英小文字と数字のみ
Pattern lowerAlphanumeric = Pattern.compile("^[a-z0-9]+$");
System.out.println(lowerAlphanumeric.matcher("user123").matches());  // true

// 英字のみ(大文字・小文字)
Pattern alphabetOnly = Pattern.compile("^[a-zA-Z]+$");
System.out.println(alphabetOnly.matcher("UserName").matches());  // true

// 数字のみ
Pattern digitOnly = Pattern.compile("^\\d+$");
System.out.println(digitOnly.matcher("12345").matches());  // true

// 英数字とアンダースコア(6~20文字)
Pattern username = Pattern.compile("^[a-zA-Z0-9_]{6,20}$");
System.out.println(username.matcher("user_name_123").matches());  // true

// パスワードパターン(英大文字・小文字・数字を各1文字以上含む8文字以上)
Pattern password = Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$");
System.out.println(password.matcher("Password123").matches());  // true
System.out.println(password.matcher("password").matches());     // false

// 英数字と特定の記号を許可(ハイフン、アンダースコア)
Pattern productCode = Pattern.compile("^[a-zA-Z0-9_-]+$");
System.out.println(productCode.matcher("PROD-2024_001").matches());  // true

これらのパターンはあくまでも基本形であり、実際のシステムでは要件に応じてカスタマイズが必要です。特にセキュリティに関わる検証では、正規表現だけでなく専用のバリデーションライブラリと組み合わせることで、より堅牢な検証を実現できます。

“`

“`html

正規表現の実用的な活用例

java+regex+code

Javaの正規表現は、実務のさまざまな場面で活用されています。ここでは、実際の開発現場で頻繁に利用される具体的なユースケースを紹介します。これらのパターンを理解することで、日常的な文字列処理の課題を効率的に解決できるようになります。

空白や区切り文字の除去・置換

ユーザー入力やファイルから読み込んだデータには、不要な空白文字が含まれていることがよくあります。正規表現を使うことで、これらを効率的に処理できます。

String text = "  Java   正規表現   学習  ";
// 複数の空白を1つに統一
String normalized = text.replaceAll("\\s+", " ");
// 先頭と末尾の空白を除去
String trimmed = text.replaceAll("^\\s+|\\s+$", "");
// 全ての空白を除去
String removed = text.replaceAll("\\s", "");
System.out.println(normalized); // " Java 正規表現 学習 "
System.out.println(trimmed);    // "Java   正規表現   学習"
System.out.println(removed);    // "Java正規表現学習"

この手法は、CSVファイルの処理やフォーム入力のサニタイズにおいて特に有用です。タブ文字や改行文字を含めて処理したい場合は、\sメタ文字が全ての空白文字にマッチするため非常に便利です。

文字列の要素分解とパース処理

構造化されたデータを含む文字列から、必要な情報を抽出する処理は頻繁に発生します。正規表現のグループ機能を活用することで、効率的にパース処理が実現できます。

String logEntry = "2024-01-15 14:30:25 [ERROR] Database connection failed";
Pattern pattern = Pattern.compile("(\\d{4}-\\d{2}-\\d{2}) (\\d{2}:\\d{2}:\\d{2}) \\[([A-Z]+)\\] (.+)");
Matcher matcher = pattern.matcher(logEntry);

if (matcher.find()) {
    String date = matcher.group(1);      // "2024-01-15"
    String time = matcher.group(2);      // "14:30:25"
    String level = matcher.group(3);     // "ERROR"
    String message = matcher.group(4);   // "Database connection failed"
    System.out.println("日付: " + date + ", レベル: " + level);
}

この方法は、ログファイルの解析やAPIレスポンスの処理において威力を発揮します。キャプチャグループを使うことで、複雑な文字列からも必要な部分だけを簡潔に取り出せます。

文字列のマスキング処理

個人情報保護の観点から、クレジットカード番号や電話番号などの機密情報を部分的にマスキングする処理は重要です。正規表現とreplaceAllメソッドを組み合わせることで、柔軟なマスキング処理が可能になります。

// クレジットカード番号のマスキング(最後の4桁以外を隠す)
String creditCard = "1234-5678-9012-3456";
String masked = creditCard.replaceAll("(\\d{4}-)(\\d{4}-)(\\d{4}-)(\\d{4})", "****-****-****-$4");
System.out.println(masked); // "****-****-****-3456"

// 電話番号のマスキング(中間部分を隠す)
String phone = "090-1234-5678";
String maskedPhone = phone.replaceAll("(\\d{3}-)(\\d{4})(-.+)", "$1****$3");
System.out.println(maskedPhone); // "090-****-5678"

// メールアドレスのマスキング(ユーザー名の一部を隠す)
String email = "example@domain.com";
String maskedEmail = email.replaceAll("(.).+(@.+)", "$1***$2");
System.out.println(maskedEmail); // "e***@domain.com"

セキュリティ要件によっては、マスキングだけでなく完全な削除や暗号化が必要になる場合もあります。用途に応じて適切な処理方法を選択してください。

区切り文字の統一

データソースによって異なる区切り文字が使用されている場合、それらを統一することで後続の処理を簡素化できます。正規表現を使えば、複数の区切り文字パターンを一度に変換できます。

// カンマ、スペース、タブなど複数の区切り文字が混在
String data = "apple,banana orange\tgrape;melon";
// 全ての区切り文字をカンマに統一
String unified = data.replaceAll("[,\\s;]+", ",");
System.out.println(unified); // "apple,banana,orange,grape,melon"

// CSVデータの区切り文字を統一(ダブルクォートを考慮)
String csvLike = "item1 , item2,  item3  ,item4";
String cleanCsv = csvLike.replaceAll("\\s*,\\s*", ",");
System.out.println(cleanCsv); // "item1,item2,item3,item4"

この手法は、異なるシステム間でデータ連携を行う際のデータクレンジング処理として非常に有効です。

表記ゆれの統一

ユーザー入力や外部データには、同じ意味でも異なる表記が混在することがあります。正規表現を使って、これらの表記ゆれを標準形式に統一できます。

String text = "JavaとJAVAとjavaは同じ言語です";
// 全角英数字を半角に統一
String normalized = text.replaceAll("[A-Za-z0-9]", m -> {
    char c = m.group().charAt(0);
    return String.valueOf((char)(c - 0xFEE0));
});

// 大文字小文字の統一(Case Insensitiveフラグを活用)
Pattern pattern = Pattern.compile("java", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(text);
String unified = matcher.replaceAll("Java");
System.out.println(unified); // 全ての表記が"Java"に統一される

// カタカナ表記の統一例
String katakana = "コンピュータとコンピューター";
String standardized = katakana.replaceAll("コンピュータ[ー]?", "コンピューター");
System.out.println(standardized); // "コンピューターとコンピューター"

検索機能や集計処理において、表記ゆれの統一は精度向上に直結します。特に日本語データを扱う場合は、全角半角の統一やカナ表記の正規化が重要になります。

HTML・XML内の属性値処理

HTMLやXMLドキュメントから特定の属性値を抽出したり、書き換えたりする処理には正規表現が便利です。ただし、複雑な構造のドキュメントにはパーサーライブラリの使用が推奨されます。

String html = "\"sample\"リンク";

// href属性の値を抽出
Pattern hrefPattern = Pattern.compile("href=\"([^\"]+)\"");
Matcher hrefMatcher = hrefPattern.matcher(html);
while (hrefMatcher.find()) {
    System.out.println("リンク先: " + hrefMatcher.group(1));
}

// src属性の値を書き換え(相対パスを絶対パスに変換)
String updated = html.replaceAll("src=\"([^\"]+)\"", "src=\"https://example.com/$1\"");
System.out.println(updated);

// タグから特定の属性を削除
String cleaned = html.replaceAll("\\s+alt=\"[^\"]*\"", "");
System.out.println(cleaned); // alt属性が削除される

ネストされたタグや特殊な構造を持つHTML/XMLの処理には、正規表現では限界があるため、JSoupやDOMパーサーなどの専用ライブラリの使用を検討してください。

ログファイルの解析

アプリケーションのログファイルから、エラー情報や特定のイベントを抽出する処理は、運用監視やトラブルシューティングにおいて重要です。正規表現を使うことで、大量のログから必要な情報だけを効率的に取得できます。

String logContent = """
2024-01-15 10:23:45 [INFO] Application started
2024-01-15 10:24:12 [ERROR] Failed to connect to database: timeout
2024-01-15 10:25:30 [WARN] Memory usage is high: 85%
2024-01-15 10:26:45 [ERROR] NullPointerException at com.example.Main.run(Main.java:42)
""";

// エラーログのみを抽出
Pattern errorPattern = Pattern.compile("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}) \\[ERROR\\] (.+)$", Pattern.MULTILINE);
Matcher errorMatcher = errorPattern.matcher(logContent);
while (errorMatcher.find()) {
    System.out.println("時刻: " + errorMatcher.group(1));
    System.out.println("メッセージ: " + errorMatcher.group(2));
}

// 特定の例外を検索
Pattern exceptionPattern = Pattern.compile("(\\w+Exception).*?\\((.+?):(\\d+)\\)");
Matcher exMatcher = exceptionPattern.matcher(logContent);
if (exMatcher.find()) {
    System.out.println("例外: " + exMatcher.group(1));
    System.out.println("ファイル: " + exMatcher.group(2));
    System.out.println("行番号: " + exMatcher.group(3));
}

この手法は、ログ監視ツールの開発や障害分析の自動化に活用できます。複数行にまたがるスタックトレースを扱う場合は、Pattern.DOTALLフラグの使用も検討してください。

テンプレート文字列の置換

メール本文やドキュメントの自動生成において、テンプレート内のプレースホルダーを実際の値で置換する処理は一般的です。正規表現を使うことで、柔軟なテンプレートエンジン機能を実装できます。

String template = "こんにちは、{{name}}さん。ご注文番号は{{orderId}}です。合計金額は{{amount}}円となります。";

// プレースホルダーのマップ
Map values = new HashMap>();
values.put("name", "山田太郎");
values.put("orderId", "ORD-12345");
values.put("amount", "5,980");

// プレースホルダーを実際の値に置換
String result = template;
Pattern placeholderPattern = Pattern.compile("\\{\\{(\\w+)\\}\\}");
Matcher matcher = placeholderPattern.matcher(template);
StringBuffer sb = new StringBuffer();

while (matcher.find()) {
    String key = matcher.group(1);
    String value = values.getOrDefault(key, matcher.group(0));
    matcher.appendReplacement(sb, Matcher.quoteReplacement(value));
}
matcher.appendTail(sb);
System.out.println(sb.toString());
// 出力: "こんにちは、山田太郎さん。ご注文番号はORD-12345です。合計金額は5,980円となります。"

appendReplacementメソッドを使うことで、マッチした部分を動的に置換できるため、複雑な条件分岐を含むテンプレート処理も実現可能です。Matcher.quoteReplacementメソッドを使用することで、置換文字列内の特殊文字を適切にエスケープできます。

“`

“`html

正規表現の発展的なテクニック

java+regex+code

Javaの正規表現の基本をマスターしたら、さらに高度なテクニックを活用することで、より複雑なパターンマッチングが可能になります。このセクションでは、実務レベルで役立つ発展的な正規表現のテクニックについて解説します。最短一致や先読み・後読みアサーション、後方参照といった機能を理解することで、正規表現の表現力を大幅に向上させることができます。

最短一致と最長一致の使い分け

正規表現における量指定子は、デフォルトでは最長一致(greedy)で動作します。これは、可能な限り長い文字列にマッチしようとする動作です。一方、最短一致(reluctant)は、条件を満たす最小限の文字列にマッチします。

最長一致と最短一致の違いを理解するために、具体的な例を見てみましょう。HTMLタグを抽出する場合を考えます。

String html = "<div>テキスト1</div><div>テキスト2</div>";

// 最長一致(greedy)
Pattern patternGreedy = Pattern.compile("<div>.*</div>");
Matcher matcherGreedy = patternGreedy.matcher(html);
if (matcherGreedy.find()) {
    System.out.println(matcherGreedy.group());
    // 結果: <div>テキスト1</div><div>テキスト2</div>
}

// 最短一致(reluctant)
Pattern patternReluctant = Pattern.compile("<div>.*?</div>");
Matcher matcherReluctant = patternReluctant.matcher(html);
while (matcherReluctant.find()) {
    System.out.println(matcherReluctant.group());
    // 結果: <div>テキスト1</div>
    //      <div>テキスト2</div>
}

最短一致を利用するには、量指定子の後ろに?を付けます。主な最短一致の記号は以下の通りです。

  • *? – 0回以上の最短一致
  • +? – 1回以上の最短一致
  • ?? – 0回または1回の最短一致
  • {n,m}? – n回からm回の最短一致

最短一致は、タグや括弧で囲まれた文字列を抽出する際に特に有用です。最長一致では意図しない範囲までマッチしてしまうケースで、最短一致を使うことで正確な抽出が可能になります。

先読み・後読みアサーション

先読み(lookahead)と後読み(lookbehind)アサーションは、文字列を消費せずに条件を確認する強力な機能です。これらを使うことで、「特定の文字列の前にある」「特定の文字列の後にある」といった複雑な条件を表現できます。Javaの正規表現では、肯定と否定の両方の先読み・後読みがサポートされています。

肯定先読みの活用

肯定先読み(positive lookahead)は、(?=pattern)の構文で表現され、指定したパターンが後ろに続く位置にマッチします。ただし、パターン自体はマッチ結果に含まれません。

// パスワード検証:英字を含む8文字以上
String password = "Pass1234";
Pattern pattern = Pattern.compile("(?=.*[a-zA-Z]).{8,}");
Matcher matcher = pattern.matcher(password);
System.out.println(matcher.matches()); // true

// 数値の後ろに"円"が続く場合のみ数値を抽出
String text = "価格は1000円です。在庫は50個です。";
Pattern pricePattern = Pattern.compile("\\d+(?=円)");
Matcher priceMatcher = pricePattern.matcher(text);
while (priceMatcher.find()) {
    System.out.println(priceMatcher.group()); // 1000
}

肯定先読みは、複数の条件を同時に満たす必要がある場合に特に有効です。例えば、パスワードの強度チェックで「英字と数字と記号をすべて含む」という条件を表現できます。

// 英字、数字、記号をすべて含む8文字以上のパスワード
Pattern strongPassword = Pattern.compile(
    "(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@#$%^&+=]).{8,}"
);

否定先読みの活用

否定先読み(negative lookahead)は、(?!pattern)の構文で表現され、指定したパターンが後ろに続かない位置にマッチします。特定の文字列を除外したい場合に便利です。

// ".com"で終わらないドメインを抽出
String domains = "example.com example.net example.org";
Pattern pattern = Pattern.compile("\\b\\w+(?!\\.com)\\.(net|org)");
Matcher matcher = pattern.matcher(domains);
while (matcher.find()) {
    System.out.println(matcher.group()); // example.net, example.org
}

// 特定の単語を含まない行を検索
String text = "JavaとPythonとRuby";
Pattern noJava = Pattern.compile("^(?!.*Java).*$");
Matcher noJavaMatcher = noJava.matcher(text);
System.out.println(noJavaMatcher.matches()); // false

否定先読みは、除外条件を簡潔に表現できるため、フィルタリング処理で頻繁に使用されます。

肯定後読みの活用

肯定後読み(positive lookbehind)は、(?<=pattern)の構文で表現され、指定したパターンが前にある位置にマッチします。前方の文脈を確認しながらマッチングを行う際に役立ちます。

// "価格:"の後ろにある数値を抽出
String text = "価格:5000円、定価:8000円";
Pattern pattern = Pattern.compile("(?<=価格:)\\d+");
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
    System.out.println(matcher.group()); // 5000
}

// "$"記号の後ろにある金額を抽出
String prices = "商品A: $100, 商品B: $250";
Pattern dollarPattern = Pattern.compile("(?<=\\$)\\d+");
Matcher dollarMatcher = dollarPattern.matcher(prices);
while (dollarMatcher.find()) {
    System.out.println(dollarMatcher.group()); // 100, 250
}

Javaの肯定後読みでは、可変長のパターンが使用できないという制限があります。つまり、*+などの量指定子は使えず、固定長または有限の選択肢のみが許可されます。

否定後読みの活用

否定後読み(negative lookbehind)は、(?<!pattern)の構文で表現され、指定したパターンが前にな位置にマッチします。特定の前置詞を持たない要素を抽出する際に有用です。

// "$"記号が前にない数値を抽出
String text = "商品A: $100, 商品B: 200個";
Pattern pattern = Pattern.compile("(?<!\\$)\\b\\d+");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
    System.out.println(matcher.group()); // 200
}

// "http://"で始まらないURLっぽい文字列を検出
String urls = "http://example.com example.net ftp://file.com";
Pattern noHttp = Pattern.compile("(?<!http://)\\b\\w+\\.\\w+");
Matcher noHttpMatcher = noHttp.matcher(urls);
while (noHttpMatcher.find()) {
    System.out.println(noHttpMatcher.group()); // example.net
}

否定後読みも肯定後読みと同様に、固定長のパターンのみを指定できます。これらのアサーションを組み合わせることで、非常に精密な条件指定が可能になります。

後方参照による複雑なパターンマッチング

後方参照(backreference)は、以前にキャプチャしたグループと同じ内容を再度マッチさせる機能です。\1\2のような記法で、キャプチャグループ番号を参照します。繰り返しパターンや対称的な構造を持つ文字列のマッチングに非常に効果的です。

// 同じ単語が連続している箇所を検出
String text = "このこの文章には重複重複があります";
Pattern pattern = Pattern.compile("(\\w+)\\1");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
    System.out.println("重複: " + matcher.group(1)); // この, 重複
}

// HTMLタグの開始と終了が一致するかチェック
String html = "<div>内容</div>";
Pattern tagPattern = Pattern.compile("<(\\w+)>.*?</\\1>");
Matcher tagMatcher = tagPattern.matcher(html);
System.out.println(tagMatcher.matches()); // true

// 引用符で囲まれた文字列(同じ種類の引用符)
String quoted = "\"Hello\" and 'World'";
Pattern quotePattern = Pattern.compile("([\"'])(.*?)\\1");
Matcher quoteMatcher = quotePattern.matcher(quoted);
while (quoteMatcher.find()) {
    System.out.println(quoteMatcher.group(2)); // Hello, World
}

後方参照は、構造的な整合性をチェックする際に強力です。例えば、括弧の対応関係や、同じ区切り文字で囲まれた範囲を抽出する場合に活用できます。

// 回文(palindrome)のチェック
String word = "abccba";
Pattern palindrome = Pattern.compile("(\\w)(\\w)(\\w)\\3\\2\\1");
Matcher palindromeMatcher = palindrome.matcher(word);
System.out.println(palindromeMatcher.matches()); // true

名前付きキャプチャグループを使用する場合は、\k<name>の構文で後方参照できます。

// 名前付きグループによる後方参照
Pattern namedPattern = Pattern.compile(
    "<(?<tag>\\w+)>.*?</\\k<tag>>"
);
Matcher namedMatcher = namedPattern.matcher("<span>text</span>");
System.out.println(namedMatcher.matches()); // true

条件の組み合わせパターン

複雑な検証や抽出を行う場合、複数の条件を組み合わせる必要があります。Javaの正規表現では、OR結合とAND結合の両方を表現できますが、それぞれ記述方法が異なります。適切な組み合わせ方を理解することで、より実用的なパターンを構築できます。

複数条件のOR結合

OR結合は、|(パイプ)演算子を使って複数のパターンのいずれかにマッチさせます。この方法は直感的で理解しやすく、選択肢を列挙する場合に適しています。

// 複数のファイル拡張子にマッチ
String filename = "document.pdf";
Pattern extPattern = Pattern.compile(".*\\.(jpg|png|gif|pdf|doc)$");
Matcher extMatcher = extPattern.matcher(filename);
System.out.println(extMatcher.matches()); // true

// 複数のプロトコルにマッチ
String url = "https://example.com";
Pattern protocolPattern = Pattern.compile("^(http|https|ftp)://.*");
Matcher protocolMatcher = protocolPattern.matcher(url);
System.out.println(protocolMatcher.matches()); // true

// 複数の電話番号形式に対応
String phone = "03-1234-5678";
Pattern phonePattern = Pattern.compile(
    "(\\d{2,4}-\\d{2,4}-\\d{4}|\\d{10,11})"
);
Matcher phoneMatcher = phonePattern.matcher(phone);
System.out.println(phoneMatcher.matches()); // true

グループ化を活用することで、部分的なOR条件も表現できます。

// 接頭辞のバリエーション
String text = "Mr.Smith";
Pattern prefixPattern = Pattern.compile("(Mr|Ms|Mrs|Dr)\\.\\w+");
Matcher prefixMatcher = prefixPattern.matcher(text);
System.out.println(prefixMatcher.matches()); // true

複数条件のAND結合

AND結合は、複数の条件をすべて満たす必要がある場合に使用します。正規表現には明示的なAND演算子がないため、肯定先読みを連続して使用することで実現します。

// 英字と数字の両方を含む文字列
String text = "Pass123";
Pattern andPattern = Pattern.compile("(?=.*[a-zA-Z])(?=.*\\d).*");
Matcher andMatcher = andPattern.matcher(text);
System.out.println(andMatcher.matches()); // true

// 大文字、小文字、数字をすべて含む8文字以上
String password = "SecurePass123";
Pattern securePattern = Pattern.compile(
    "(?=.*[A-Z])(?=.*[a-z])(?=.*\\d).{8,}"
);
Matcher secureMatcher = securePattern.matcher(password);
System.out.println(secureMatcher.matches()); // true

// 特定の文字を含み、かつ特定の文字を含まない
String input = "JavaPattern";
Pattern complexPattern = Pattern.compile(
    "(?=.*Java)(?!.*Script).*"
);
Matcher complexMatcher = complexPattern.matcher(input);
System.out.println(complexMatcher.matches()); // true

先読みアサーションを使ったAND結合は、順序に依存しない条件チェックが可能です。例えば、パスワードに必要な要素を任意の順序で検証できます。

// 英大文字、英小文字、数字、記号をすべて含む複雑なパスワード
Pattern strongPattern = Pattern.compile(
    "(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[@#$%^&+=]).{10,}"
);

String weakPassword = "password123";
String strongPassword = "SecureP@ss123";

System.out.println(strongPattern.matcher(weakPassword).matches()); // false
System.out.println(strongPattern.matcher(strongPassword).matches()); // true

このように、OR結合とAND結合を組み合わせることで、実務で求められる複雑な検証ロジックを正規表現で表現できます。ただし、複雑すぎる正規表現は可読性とメンテナンス性を低下させるため、適度な粒度でメソッド分割することも検討してください。

“`

“`html

正規表現のパフォーマンスと注意点

java+regex+performance

Javaで正規表現を活用する際には、パフォーマンスへの影響を理解しておくことが重要です。正規表現は非常に強力なツールですが、使い方によっては処理速度が大幅に低下する可能性があります。このセクションでは、正規表現のパフォーマンス問題と、効率的な実装方法について詳しく解説します。

正規表現のパフォーマンス問題

正規表現処理は、文字列操作の中でも比較的負荷が高い処理です。特に複雑なパターンや長い文字列に対して正規表現を適用すると、予想以上に実行時間がかかることがあります。

正規表現エンジンは、マッチングを行う際にバックトラッキング(後戻り)という処理を行います。これは、パターンが一致しない場合に前の状態に戻って別の可能性を試す仕組みです。このバックトラッキングが過剰に発生すると、指数関数的に処理時間が増加する「破滅的バックトラッキング」という現象が起こる可能性があります。

また、Patternクラスのコンパイル処理自体にもコストがかかります。正規表現パターンをコンパイルする際には、パターン文字列を解析して内部的な状態機械を構築する必要があるため、この処理を繰り返し実行すると無駄なオーバーヘッドが発生します。

処理速度が遅くなる原因と対策

正規表現の処理速度が遅くなる主な原因とその対策について、具体的に見ていきましょう。

原因1:繰り返しのPatternコンパイル

最も一般的な問題は、同じ正規表現パターンを何度もコンパイルしてしまうことです。例えば、ループ内でPattern.compile()を呼び出すと、毎回コンパイル処理が実行されます。

// 悪い例:ループ内でコンパイル
for (String text : textList) {
    Pattern pattern = Pattern.compile("\\d{3}-\\d{4}");
    Matcher matcher = pattern.matcher(text);
    if (matcher.find()) {
        // 処理
    }
}

// 良い例:事前にコンパイル
Pattern pattern = Pattern.compile("\\d{3}-\\d{4}");
for (String text : textList) {
    Matcher matcher = pattern.matcher(text);
    if (matcher.find()) {
        // 処理
    }
}

対策としては、Patternオブジェクトを再利用することが有効です。可能であればクラスのstatic final変数として定義しておくことで、アプリケーション全体でコンパイル済みのパターンを共有できます。

原因2:過剰なバックトラッキング

入れ子になった量指定子(*, +, ?)や複雑な選択肢(|)を持つパターンは、バックトラッキングを大量に発生させる可能性があります。

// 危険な例:破滅的バックトラッキングの可能性
Pattern pattern = Pattern.compile("(a+)+b");
// "aaaaaaaaaaaaaaaaaac"のような文字列で極端に遅くなる

// 改善例:原子グループや所有量指定子を使用
Pattern pattern = Pattern.compile("(?>a+)+b");  // 原子グループ
Pattern pattern2 = Pattern.compile("a++b");     // 所有量指定子

原因3:不要な全文検索

matches()メソッドは文字列全体とのマッチングを試みるため、部分一致を探す場合には非効率です。

// 非効率な例
if (text.matches(".*keyword.*")) {
    // 処理
}

// 効率的な例
if (text.contains("keyword")) {  // 正規表現が不要なら通常のメソッドを使用
    // 処理
}

// 正規表現が必要な場合
Pattern pattern = Pattern.compile("keyword");
if (pattern.matcher(text).find()) {
    // 処理
}

効率的な正規表現の書き方

パフォーマンスを向上させるための効率的な正規表現の記述方法をいくつか紹介します。

1. 具体的なパターンを優先する

「.」(任意の文字)よりも、具体的な文字クラスを使用する方が効率的です。マッチング対象が明確になることで、正規表現エンジンの探索範囲が絞られます。

// 改善前
Pattern pattern = Pattern.compile(".*@.*\\..*");

// 改善後:具体的な文字クラスを使用
Pattern pattern = Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}");

2. アンカーを活用する

行頭(^)や行末($)のアンカーを使用することで、不要な探索を減らせます。

// 文字列全体をチェックする場合
Pattern pattern = Pattern.compile("^\\d{3}-\\d{4}$");

// 先頭から始まることが分かっている場合
Pattern pattern2 = Pattern.compile("^ERROR:");

3. 非キャプチャグループを使用する

キャプチャが不要な場合は、非キャプチャグループ((?:…))を使用することでメモリとCPUの使用量を削減できます。

// キャプチャが必要な場合
Pattern pattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");

// キャプチャが不要な部分は非キャプチャグループに
Pattern pattern2 = Pattern.compile("(?:\\d{4})-(?:\\d{2})-(\\d{2})");

4. 選択肢の順序を最適化する

「|」で複数の選択肢を指定する場合、出現頻度が高いものや、マッチしやすいものを先に配置します。

// 頻度の高いパターンを先に配置
Pattern pattern = Pattern.compile("https?|ftp|file");

5. 量指定子の適切な使用

必要最小限の範囲で量指定子を使用し、明確な上限がある場合は{n,m}形式で指定します。

// 改善前:上限なし
Pattern pattern = Pattern.compile("\\d+");

// 改善後:上限を明示
Pattern pattern2 = Pattern.compile("\\d{1,10}");  // 最大10桁と分かっている場合

正規表現使用時の注意事項

正規表現を実装する際には、パフォーマンス以外にも注意すべき点があります。これらを理解しておくことで、バグの少ない堅牢なコードを書くことができます。

エスケープ処理の注意

Javaの文字列リテラルでは、バックスラッシュ自体をエスケープする必要があります。正規表現で「\d」を使いたい場合、Javaのコードでは「\\d」と記述しなければなりません。この二重エスケープを忘れると、意図しない動作になります。

// 誤り:コンパイルエラーになる
// Pattern pattern = Pattern.compile("\d+");

// 正しい記述
Pattern pattern = Pattern.compile("\\d+");

// メタ文字のエスケープも同様
Pattern pattern2 = Pattern.compile("\\.");  // ドットをリテラルとして扱う
Pattern pattern3 = Pattern.compile("\\\\"); // バックスラッシュ自体をマッチ

PatternSyntaxExceptionへの対処

不正な正規表現パターンを指定すると、PatternSyntaxExceptionが発生します。ユーザー入力から正規表現を構築する場合は、必ず例外処理を実装しましょう。

try {
    Pattern pattern = Pattern.compile(userInput);
} catch (PatternSyntaxException e) {
    System.err.println("無効な正規表現: " + e.getMessage());
    // エラーハンドリング
}

マルチスレッド環境での使用

PatternクラスはスレッドセーフですがMatcherクラスはスレッドセーフではありません。複数のスレッドで正規表現を使用する場合、Patternオブジェクトは共有できますが、Matcherオブジェクトは各スレッドで個別に生成する必要があります。

// static変数として共有可能
private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}");

public void processEmail(String email) {
    // Matcherは毎回新規作成
    Matcher matcher = EMAIL_PATTERN.matcher(email);
    if (matcher.matches()) {
        // 処理
    }
}

文字コードとUnicodeの扱い

日本語などのマルチバイト文字を扱う場合、適切なフラグを指定する必要があります。Pattern.UNICODE_CHARACTER_CLASSフラグを使用することで、\wや\dなどがUnicode文字を正しく認識します。

// Unicode対応の正規表現
Pattern pattern = Pattern.compile("\\w+", Pattern.UNICODE_CHARACTER_CLASS);

// Java 7以降では(?U)フラグも使用可能
Pattern pattern2 = Pattern.compile("(?U)\\w+");

正規表現が本当に必要か検討する

最後に重要な注意点として、正規表現を使わない方が良いケースもあるということを理解しておきましょう。単純な文字列検索や置換であれば、Stringクラスのcontains()、startsWith()、replace()などのメソッドの方が高速で読みやすいコードになります。

// 正規表現が不要な例
if (text.contains("ERROR")) { }           // contains()で十分
if (text.startsWith("http://")) { }       // startsWith()で十分
String result = text.replace("a", "b");   // replace()で十分

// 正規表現が必要な例
if (text.matches("\\d{3}-\\d{4}")) { }    // パターンマッチングが必要
String result = text.replaceAll("\\s+", " "); // 複数の空白を1つに

正規表現は強力なツールですが、適切に使用しないとパフォーマンス問題やバグの原因となります。パターンの再利用、効率的な記述方法、そして正規表現が本当に必要かどうかの判断を適切に行うことで、保守性が高く高速なコードを実装できます。

“`

まとめ

java+programming+code

本記事では、Javaにおける正規表現の基本から実践的な活用方法まで、幅広く解説してきました。正規表現は文字列の検索、置換、検証など、さまざまな場面で威力を発揮する強力なツールです。

Java の正規表現を扱う際には、PatternクラスとMatcherクラスの2つが中心となります。Patternクラスで正規表現パターンをコンパイルし、Matcherクラスで実際のマッチング操作を行うという基本的な流れを理解することが重要です。

メタ文字や記号を適切に組み合わせることで、単純な文字列マッチングから複雑なパターンマッチングまで柔軟に対応できます。特にグループ化やキャプチャリング機能を活用すると、マッチした部分文字列を効率的に抽出・再利用できるため、実務での開発効率が大きく向上します。

実践的な活用場面としては、以下のようなケースが挙げられます。

  • ユーザー入力の検証(メールアドレス、電話番号、郵便番号など)
  • ログファイルの解析と情報抽出
  • 文字列の整形や表記ゆれの統一
  • HTML/XMLのパース処理
  • テキストデータのクレンジング

一方で、正規表現のパフォーマンスには注意が必要です。複雑すぎるパターンや不適切な記述は処理速度の低下を招くため、効率的な書き方を意識することが大切です。パターンの再利用やコンパイル済みPatternオブジェクトの活用は、パフォーマンス改善の基本テクニックとなります。

正規表現は最初は難しく感じるかもしれませんが、基本的なメタ文字から段階的に学習し、実際のコードで試しながら習得していくことで、確実にスキルアップできます。本記事で紹介したサンプルパターンや実用例を参考に、ぜひ実際のJava開発プロジェクトで正規表現を活用してみてください。

正規表現を適切に使いこなせるようになると、文字列処理の実装がシンプルかつ保守性の高いものになり、開発者としての生産性が大きく向上するでしょう。