Java Tips | 文字列処理:正規表現マッチ

Java Java
スポンサーリンク

正規表現マッチは「文字列の中から“パターン”を見つける」技

文字列分割が「区切りで切る」技だとしたら、正規表現マッチは「ルール(パターン)に合う部分だけを見つける」技です。
「メールアドレスかどうかチェックしたい」「ログから日付だけ抜き出したい」「特定フォーマットのIDだけ拾いたい」――こういうときに真価を発揮します。

ただし、いきなり複雑な正規表現に手を出すと、何を書いているのか自分でも分からなくなります。
だからまずは、「Java で正規表現マッチを扱うための基本パターン」と「それを包むユーティリティ」を押さえるところから始めましょう。


Java の正規表現の基本:Pattern と Matcher

まずは「パターンをコンパイルして、文字列に当てる」

Java で正規表現を扱うときの基本クラスは java.util.regex.PatternMatcher です。

最小構成はこうです。

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

public class RegexBasicExample {

    public static void main(String[] args) {
        String regex = "\\d{3}-\\d{4}"; // 例: 123-4567
        Pattern pattern = Pattern.compile(regex);

        String text = "郵便番号は123-4567です。";
        Matcher matcher = pattern.matcher(text);

        boolean found = matcher.find();
        System.out.println(found); // true
    }
}
Java

ここで押さえたいのは、次の流れです。

正規表現文字列を Pattern.compile で「パターン」にする。
pattern.matcher(text) で、「この文字列に対してマッチングするための Matcher」を作る。
matcher.find()matcher.matches() で、実際にマッチさせる。

この「Pattern と Matcher の二段構え」が、Java の正規表現マッチの基本形です。


業務で使いやすくするためのユーティリティ化

「マッチするかどうか」だけ知りたいときの isMatch

「この文字列がパターンに合っているかどうか」だけ知りたい場面はとても多いです。
毎回 Pattern と Matcher を書くのは冗長なので、ユーティリティにまとめます。

import java.util.regex.Pattern;

public final class Regexes {

    private Regexes() {}

    public static boolean isMatch(String regex, String text) {
        if (regex == null || text == null) {
            return false;
        }
        Pattern pattern = Pattern.compile(regex);
        return pattern.matcher(text).matches();
    }
}
Java

使い方はこうです。

String regex = "\\d{3}-\\d{4}";
System.out.println(Regexes.isMatch(regex, "123-4567")); // true
System.out.println(Regexes.isMatch(regex, "1234-567")); // false
Java

ここで深掘りしたい重要ポイントは、「matches() は“文字列全体がパターンに一致するか”を見る」ということです。
find() は「どこか一部でも一致すれば true」ですが、matches() は「全部がそのパターンであること」を要求します。

郵便番号やIDのように「文字列全体がこの形式であるべき」というチェックでは、matches() を使うのが基本です。


例題:メールアドレス形式の簡易チェック

完璧を目指さず「業務で十分なレベル」をユーティリティにする

メールアドレスの正規表現は、本気でやるととんでもなく長くなります。
業務では、「明らかにおかしいものを弾ければ十分」という割り切りも大事です。

例えば、次のような簡易パターンを使ってみます(あくまで例です)。

public final class EmailValidator {

    private static final Pattern SIMPLE_EMAIL =
            Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");

    private EmailValidator() {}

    public static boolean isValidEmail(String email) {
        if (email == null) {
            return false;
        }
        return SIMPLE_EMAIL.matcher(email).matches();
    }
}
Java

使い方はこうです。

System.out.println(EmailValidator.isValidEmail("user@example.com"));   // true
System.out.println(EmailValidator.isValidEmail("user@@example.com"));  // false
System.out.println(EmailValidator.isValidEmail("user example.com"));   // false
Java

ここでのポイントは二つです。

一つ目は、「Pattern を毎回コンパイルせず、static final にして再利用している」ことです。
正規表現のコンパイルはそれなりにコストがかかるので、同じパターンを何度も使うなら、事前にコンパイルしておくのが定石です。

二つ目は、「“完璧な RFC 準拠”ではなく、“業務で許容できるレベル”のパターンをユーティリティとして固定している」ことです。
これにより、「プロジェクト内でメールアドレスのチェック方法がバラバラ」という状態を防げます。


例題:ログから特定の情報を抜き出す(グルーピング)

キャプチャグループで「欲しい部分だけ」取り出す

正規表現の強みは、「パターンに合うかどうか」だけでなく、「その中の一部を抜き出せる」ことです。
例えば、次のようなログ行があるとします。

2025-01-30 12:34:56 INFO userId=u-001 action=LOGIN

ここから「日付」「ログレベル」「userId」「action」を取り出したいとします。

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

public final class LogLineParser {

    // 日付 時刻 レベル userId=... action=...
    private static final Pattern LOG_PATTERN = Pattern.compile(
            "^(\\d{4}-\\d{2}-\\d{2})\\s+" +   // 1: 日付
            "(\\d{2}:\\d{2}:\\d{2})\\s+" +    // 2: 時刻
            "(\\S+)\\s+" +                    // 3: レベル
            "userId=(\\S+)\\s+" +             // 4: userId
            "action=(\\S+)$"                  // 5: action
    );

    private LogLineParser() {}

    public static LogRecord parse(String line) {
        Matcher m = LOG_PATTERN.matcher(line);
        if (!m.matches()) {
            throw new IllegalArgumentException("ログ形式が不正です: " + line);
        }
        String date   = m.group(1);
        String time   = m.group(2);
        String level  = m.group(3);
        String userId = m.group(4);
        String action = m.group(5);
        return new LogRecord(date, time, level, userId, action);
    }

    public record LogRecord(String date, String time, String level,
                            String userId, String action) {}
}
Java

使い方はこうです。

String line = "2025-01-30 12:34:56 INFO userId=u-001 action=LOGIN";
LogLineParser.LogRecord r = LogLineParser.parse(line);

System.out.println(r.date());   // 2025-01-30
System.out.println(r.time());   // 12:34:56
System.out.println(r.level());  // INFO
System.out.println(r.userId()); // u-001
System.out.println(r.action()); // LOGIN
Java

ここで深掘りしたい重要ポイントは、「キャプチャグループ((...))の順番と group(n) の対応を、コードとして固定している」ことです。
正規表現だけを見ると何番目が何か分かりにくいですが、
LogRecord のフィールド名と group(1)〜group(5) の対応をきちんと書いておくことで、
「このパターンは何を抜き出しているのか」がコードから読み取れるようになります。


例題:文字列中のすべてのマッチを列挙する

find をループして「全部拾う」パターン

matches() は「全体が一致するか」を見るメソッドでしたが、
「文字列の中に何回も出てくるパターンを全部拾いたい」というときは find() を使います。

例えば、「テキスト中のすべてのメールアドレスっぽいものを拾う」例です(パターンは簡易版)。

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

public final class EmailExtractor {

    private static final Pattern SIMPLE_EMAIL =
            Pattern.compile("[^@\\s]+@[^@\\s]+\\.[^@\\s]+");

    private EmailExtractor() {}

    public static List<String> extractAll(String text) {
        List<String> result = new ArrayList<>();
        if (text == null || text.isEmpty()) {
            return result;
        }
        Matcher m = SIMPLE_EMAIL.matcher(text);
        while (m.find()) {
            result.add(m.group());
        }
        return result;
    }
}
Java

使い方はこうです。

String text = "連絡先はuser1@example.comとuser2@test.orgです。";
List<String> emails = EmailExtractor.extractAll(text);

System.out.println(emails); // [user1@example.com, user2@test.org]
Java

ここでのポイントは、「find() をループし、group() で“今マッチした部分”を取り出す」というパターンを覚えることです。
matches() は「全体一致」、find() は「部分一致を順番に拾う」と覚えておくと、使い分けがスムーズになります。


正規表現マッチでつまずきやすいポイント

バックスラッシュと Java 文字列リテラルの二重苦

Java の正規表現は、**「正規表現」と「Java の文字列リテラル」の二重の世界」を同時に扱う必要があります。

例えば、「数字1文字」を表す正規表現は \d ですが、Java の文字列リテラルでは \ 自体がエスケープ文字です。
そのため、コード上では "\\d" と書かなければなりません。

同様に、「バックスラッシュそのもの」を表したいときは、正規表現では \\、Java 文字列では "\\\\" になります。
最初は混乱しやすいので、「正規表現の世界ではこう、Java の世界ではこう」と紙に書き出して整理してみると、だいぶ楽になります。

複雑になりすぎたら「名前をつけて分割する」

正規表現は、欲張るとすぐに「暗号」になります。

例えば、1行で全部書こうとするのではなく、

日付部分のパターン
時刻部分のパターン
ログレベル部分のパターン

のように、意味ごとに文字列定数を分けてから + でつなぐと、かなり読みやすくなります。

private static final String DATE = "(\\d{4}-\\d{2}-\\d{2})";
private static final String TIME = "(\\d{2}:\\d{2}:\\d{2})";
private static final String LEVEL = "(\\S+)";

private static final Pattern LOG_PATTERN = Pattern.compile(
        "^" + DATE + "\\s+" + TIME + "\\s+" + LEVEL + ".*$"
);
Java

「正規表現を分割して名前をつける」というだけで、
「何をマッチさせたいのか」が一気にクリアになります。


まとめ:正規表現マッチユーティリティで身につけたい感覚

正規表現マッチは、「文字列の中から“ルールに合う部分”を見つけたり抜き出したりする」ための強力な技です。

押さえておきたい感覚は、まず「Pattern と Matcher の二段構えを理解し、matches()(全体一致)と find()(部分一致)の役割を区別する」こと。
次に、「よく使うパターン(メールアドレス、ログ行、ID など)は static final Pattern としてユーティリティに閉じ込め、プロジェクト全体で同じチェック・抽出ロジックを共有する」こと。
そして、「複雑な正規表現は意味ごとに分割して名前をつけ、キャプチャグループとオブジェクトのフィールドをきちんと対応づける」ことです。

もしあなたのコードのどこかに、text.matches("...")Pattern.compile("...") が生で散らばっているなら、
その一つを題材にして、ここで作った Regexes.isMatchEmailValidatorLogLineParser のようなユーティリティに置き換えてみてください。
それだけで、「読めて、直せて、再利用しやすい正規表現マッチ」に、一段レベルアップできます。

タイトルとURLをコピーしました