JavaのPatternとは?正規表現のパターンを表すクラス「Pattern」を解説
JavaでPatternと言えば、正規表現(Regular Expression)で使うクラスのjava.util.regex.Patternを指すのが普通でしょう。このPatternとは、Javaで正規表現のパターンを表すクラスです。
このページをご覧になっているということは、Javaでの正規表現に興味・関心をお持ちなのだと思います。せっかくですので、Patternを通じてJavaでの正規表現の使い方を学びませんか? 絶対に役に立ちますよ!!
正規表現の知識は、文字列検索や文字列操作の作業効率をとても向上させます。正規表現は、Javaに限らず大抵のプログラミング言語で同じように使えますので、どこかできちんと学べば一生ものの知識になります。
この記事では、正規表現の初心者向けに、Javaでの正規表現のサンプルプログラムを通じて、正規表現ではどういうことができるのかをお伝えします。すぐに使える便利なパターンのサンプルもご用意しています。
※この記事のサンプルは、Java 12の環境で動作確認しています
1.Patternは正規表現の「パターン」
1-1.正規表現とは文字列のパターンのこと
そもそも正規表現(Regular Expression)とはナニモノかというと、文字列が持っている「パターン」を共通した言い方で表して、プログラムからまとめて使えればすごく便利じゃない? というものです。
身の回りにあるいろいろな文字列は、ルールやパターンを持つものが多いです。例えば、郵便番号ってどんなもの?と聞かれたら、「3桁の数字と4桁の数字が“-“でつながったもの」とパターンを説明できますよね。
そんな文字列のパターンを、コンピュータがわかる書き方にしたのが正規表現です。パターンを正規表現で書きさえすれば、何かの文字列がそのパターンにマッチするかは、コンピュータはすぐさま確認できます。
人間が持つパターンを見出す能力と、コンピュータが持つ文句ひとつ言わずに大量・高速にガンガン処理できる能力。この二つをあわせれば、大量の文字列を相手にした作業であっても、怖いものなしなのです。
1-2.Javaでの正規表現はPatternとMatcherを使う
正規表現は世の中にあるほとんどのプログラミング言語で使えます。もちろんJavaでも正規表現を使えますが、プログラミング言語の文法には組み込まれていないので、Javaでは正規表現のためのクラスを使います。
Javaの正規表現では、パッケージjava.util.regexにあるPatternとMatcherというクラスを使います。Patternが正規表現の「パターン」を表し、Matcherが検査したい文字列と正規表現との「マッチング」を行います。
java.util.regex (Java SE 11 & JDK 11)
https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/util/regex/package-summary.html
Pattern (Java SE 11 & JDK 11)
https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/util/regex/Pattern.html
Matcher (Java SE 11 & JDK 11)
https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/util/regex/Matcher.html
ちなみに、パッケージ名のregexは、REGular EXpressionの略です。regexとかreは他のプログラミング言語でもよく使われる略称ですので、覚えておくと「ああ、正規表現に関係する何かだね」とすぐわかりますよ。
1-3.Patternは正規表現を「コンパイル」して作る
正規表現はそれだけで一つのプログラミング言語ともいえる高機能・複雑なものです。ですので、正規表現を使う時には、事前に正規表現の「コンパイル」をするプログラミング言語が多いのです。Javaはその仲間です。
Javaで正規表現をコンパイルするには、java.util.regex.Patternのメソッドcompileを呼び出します。Pattern.compileからはPatternのインスタンスが戻ります。正規表現の構文どおりでないと、例外がthrowされます。
// Java 11の日本語版Javadocより抜粋 public static Pattern compile(String regex) 指定された正規表現をパターンにコンパイルします。 パラメータ: regex - コンパイルされる表現 戻り値: パターンにコンパイルする指定された正規表現 例外: PatternSyntaxException - 表現の構文が無効である場合
以下のように正規表現の文字列を引数にcompileを呼び出すと、正規表現のPatternのインスタンスが戻ります。なお、後者は正規表現として正しい構文ではないので、実行時にPatternSyntaxExceptionがthrowされます。
Pattern pattern1 = Pattern.compile("[a-z]"); // 小文字の英字→OK Pattern pattern2 = Pattern.compile("["); // [が閉じていないので、構文としてNG
なお、Patternは外部からnewして作れないので(コンストラクタが公開されていない)、必ずPattern.compileを使うことになります。
1-4.PatternはMatcherと組み合わせて使う
Patternは正規表現を表しているクラスです。でも、Patternだけでは文字列とのマッチングは出来ません。文字列とマッチングをするには、Pattern.matcherで得られるMatcherのインスタンスのメソッドを使います。
// Java 11の日本語版Javadocより抜粋 public Matcher matcher(CharSequence input) 指定された入力とこのパターンをマッチする正規表現エンジンを作成します。 パラメータ: input - マッチされる文字シーケンス 戻り値: このパターンの新しい正規表現エンジン
Patternとは正規表現そのもので、Matcherとは何かの文字列とのマッチングを行うもの(match + er)です。例えば、以下のように使い分けますが、PatternとMatcherの役割の違いがお判りいただけるでしょうか。
// 正規表現をPatternとして作って、 Pattern pattern = Pattern.compile("[a-z]"); // マッチさせたい文字列に対するMatcherをPatternから作る Matcher matcher = pattern.matcher("あいうえお abcdefg"); // Matcherに正規表現にマッチするかを聞いて、マッチしたかで処理を分ける if (matcher.find()) { System.out.println("マッチする部分があります"); } else { System.out.println("マッチする部分がありません"); }
Matcherについては、この後で簡単な使い方をご紹介します。マッチさせるにも、完全一致なのか、部分的な一致なのかなど、いろいろなマッチのさせ方がありますので、使い方は覚えておきたいですね。
1-5.Patternは使いまわしができる
コンパイルして作ったPatternはずっと使えます。つまり、Patternを使えるのは一回こっきりではないのです。むしろ、Patternの使いまわしをするために、PatternとMatcherが別のクラスになっているのです。
例えば、以下のプログラムでは最初に作ったPatternをループの中でずっと使っています。Matcherはループしている行ごとにPatternから作り直していますが、Patternはずっと同じインスタンスを使っていますよね。
// Patternだけは先にcompileして作っておいて、 Pattern pattern = Pattern.compile("[a-z]"); // とあるファイルの内容で行単位にループする for (String line : Files.readAllLines(Paths.get("/path/to/file.txt"))) { // Matcherは現在の行を対象にPatternから生成して、 Matcher matcher = pattern.matcher(line); // Matcherでマッチするか確認する if (matcher.find()) { System.out.println("マッチする部分があります"); } else { System.out.println("マッチする部分がありません"); } }
もちろん以下のようにも書けますし動きもします。でも、同じ正規表現をループごとにコンパイルするのはいろいろと無駄だったりします。Patternは、あらかじめ先に作っておいて、使いまわすのがお勧めです。
// とあるファイルの内容で行単位にループする for (String line : Files.readAllLines(Paths.get("/path/to/file.txt"))) { // Patternをループの度にcompileして作る→無駄!! Pattern pattern = Pattern.compile("[a-z]"); // Matcherは現在の行を対象にPatternから生成して、 Matcher matcher = pattern.matcher(line); // Matcherでマッチするか確認する if (matcher.find()) { System.out.println("マッチする部分があります"); } else { System.out.println("マッチする部分がありません"); } }
Patternが使われる場所により、Patternのインスタンスを保持する場所を、ローカル変数やフィールドのどれにするか選べます。ちなみに、Patternはスレッドセーフなので、マルチスレッドな環境でも安全に使えます。
// Patternのインスタンスを持つところは… class PatternSample { // staticフィールドでもいいし、 static final Pattern PATTERN_FIELD = Pattern.compile("[a-z]"); // インスタンスフィールドでもいいし、 final Pattern patternField = Pattern.compile("[a-z]"); void patternSample() { // ローカル変数としてもいい!! Pattern patternLocal = Pattern.compile("[a-z]"); // よく使うPatternは使いまわすのがお勧め!! Matcher m1 = PATTERN_FIELD.matcher("あいうえお abcdefg"); Matcher m2 = patternField.matcher("あいうえお abcdefg"); Matcher m3 = patternLocal.matcher("あいうえお abcdefg"); } }
1-6.Patternのその他のメソッド
1-6-1.Pattern.splitで文字列を分割する
Stringには、文字列を正規表現で指定した区切り文字で分割する、String.splitというメソッドがあります。Patternには、それと同じ動きをするPattern.splitがあります。
String s = "A-B-C-D"; Pattern p = Pattern.compile("-"); String[] arr = p.split(s); System.out.println(java.util.Arrays.toString(arr)); // → [A, B, C, D]
Pattern.splitのいいところは、複数のStringを同じ正規表現でsplitする場合でも、Patternの生成が1回で済むことです。複雑な正規表現を使う場合などで、プログラムの実行時間を少し短くできるかもしれません。
Pattern p = Pattern.compile("-"); java.util.Arrays.asList("A-B-C-D", "E-F-G-H", "I-J-K-L").forEach(s -> { String[] arr = p.split(s); System.out.println(java.util.Arrays.toString(arr)); });
なお、分割する位置を指定するString.split(String, int)と同じ動作をする、Pattern.split(String, int)もあります。
String s = "A-B-C-D"; Pattern p = Pattern.compile("-"); String[] arr = p.split(s, 2); System.out.println(java.util.Arrays.toString(arr)); // → [A, B-C-D]
String.splitの詳細は、以下の記事でご紹介していますので、よろしければご覧になってください。
関連記事1-6-2.Pattern.asPredicate/asMatchPredicateでマッチ確認用のPredicateを作る
Java 8からはPattern.asPredicate、Java 11からはPattern.asMatchPredicateが使えます。これらは、それぞれMatcher.findとmatchesを実行し、結果を戻すPredicateを生成するメソッドです。
ですので、「この正規表現にマッチするかどうか」をチェックできるPredicateを、お手軽に作れるということです。正規表現での「チェック処理」をインスタンス化して、色々な場所へ持ち運ぶのに使えそうですね。
Pattern p = Pattern.compile("[0-9]+"); Predicate<String> pr1 = p.asPredicate(); Predicate<String> pr2 = p.asMatchPredicate(); // pr1はasPredicateで作ったので、検査対象の文字列に数字が含まれるか(=find)を確認する boolean b1_1 = pr1.test("aaa bbb ccc 123 ddd"); boolean b1_2 = pr1.test("aaa bbb ccc ddd"); System.out.println(b1_1); // → true System.out.println(b1_2); // → false // pr2はasMatchPredicateで作ったので、検査対象の文字列が数字のみか(=matches)を確認する boolean b2_1 = pr2.test("abcd 1234"); boolean b2_2 = pr2.test("1234"); System.out.println(b2_1); // → false System.out.println(b2_2); // → true
2.正規表現の作り方
2-1.【重要】正規表現の基本のキ
正規表現の全てを完璧にマスターするのは難しいです。きちんと解説するなら一冊の分厚い本になるくらいです。しかも、Javaの正規表現には他のプログラミング言語の正規表現と違う部分も、少しだけあったりします。
でも、以下の基本的なものは他のプログラミング言語と同じなので、必ず覚えておきましょう。さらにPatternのJavadocにもJavaの正規表現について書いてありますので、きちんと読んでおけば、いずれ役に立ちますよ。
記号 | 意味 | 例 | マッチするもの |
. | 何か一文字にマッチする | .、..、.+ | a、ab、あいうえお |
? | 直前のパターンが0回あるいは1回続く | a? | a、””(空文字列) |
+ | 直前のパターンが1回以上続く | a+ | a、aa、aaa |
* | 直前のパターンが0回以上続く | a* | a、aa、aaa、””(空文字列) |
[] | []の中にあるどれか一文字、-で範囲指定 | [abcd]、[a-z] | a、ab、abcd |
[^] | []の中にないどれか一文字、-で範囲指定 | [^abcd]、[^a-z] | 1、ABC、あいうえお |
{n} | 直前のパターンがn回続く | a{5} | aaaaa |
{n,} | 直前のパターンがn回以上続く | a{5,} | aaaaa、aaaaaa、aaaaaaa |
{,n} | 直前のパターンがn回以下続く | a{,5} | a、aaa、aaaaa |
{m,n} | 直前のパターンがm回以上m回以下続く | a{2,3} | aa、aaa |
| | どちらかのパターンに一致する | abc|123 | abc、123 |
() | ()の中をグループ化する | (abc|123) | abc、123 |
^ | 行頭 | ^abc | abc defg ※abcから始まる文字列 |
$ | 行末 | xyz$ | ※xyzで終わる文字列 |
実際には、これらを組み合わせます。例えば、“/”区切りでの年月日は“[0-9]{4}/[0-9]{2}/[0-9]{2}”です。つまり、4つの数字、2つの数字、さらに2つの数字が/で区切られているもの、ということです。
でも、これだと実際にはカレンダーにはない13月や40日がOKなので、実用ではさらに工夫が必要です。明らかにおかしくないか程度のチェックを正規表現で行い、追加のチェックは別に書くのもよく見られます。
2-2.正規表現での特殊文字はエスケープが必要
2-2-1.\はJavaの文字列では特別な意味を持つ
正規表現では \ が特別な意味を持ちます。ですが、Javaでは文字列中の \ は正規表現の前に文字列のエスケープを意味するので、正規表現で \ を使いたい場合は \\ として、\の効果を打ち消さなければなりません。
// 正規表現のパターン中で \s や \w や \d を使いたい場合は、\\ とする String regex = "\\s\\w\\d"; System.out.println(regex); // → \s\w\d Pattern pattern = Pattern.compile(regex);
さらに面倒なのは、\ そのものを正規表現で使いたい場合は、\ 自体を正規表現中で特別な意味を持たないよう、さらに打ち消さなければならないことです。ですので、\\\\ と \ が四つ繋がることになります。
// C:\Windows\Sytem32 か Temp を指定する正規表現 // \ そのものを表現するのに \\\\ としなければならない String regex = "C:\\\\Windows\\\\(System32|Temp)"; System.out.println(regex); // → C:\\Windows\\(System32|Temp) Pattern pattern = Pattern.compile(regex);
2-2-2.その他の特殊文字もそのまま使うならエスケープする
そして、正規表現では \ 以外に [] {} | . + * ? () ^ $なども特別な意味を持ちますが、これらもただの文字としたい場合は、\ でエスケープしなければなりません。
// 正規表現上での特殊文字をそのまま使いたいなら、\ でのエスケープが必要 String regex = "\\[\\]\\{\\}\\|\\.\\+\\*\\?\\(\\)\\^\\$"; System.out.println(regex); // → \[\]\{\}\|\.\+\*\?\(\)\^\$ Pattern pattern = Pattern.compile(regex);
ただ、文字列すべての中から正規表現で特別な意味を持つものをぜんぶ手でエスケープするのは大変です。漏れも出てくるでしょう。楽かつ確実に行うには、以下の二つの方法があります。
方法①:Pattern.quote(String)を使う
String regex = "[]{}|.+*?()^$\\"; String regexQuoted = Pattern.quote(regex); System.out.println(regexQuoted); // → \Q[]{}|.+*?()^$\\E Pattern pattern = Pattern.compile(regexQuoted);
方法②:正規表現へ \Q と \E を自分でつける
String regex = "\\Q[]{}|.+*?()^$\\\\E"; System.out.println(regex); // → \Q[]{}|.+*?()^$\\E Pattern pattern = Pattern.compile(regex);
正規表現での \Q と \E は、この二つの文字で囲まれている文字はそのままの意味を持つことを指示するものです。Pattern.quoteの結果も、同じことをしていますよね。ちなみに、\Q と \E は途中に出てきてもOKです。
2-3.正規表現の例
ここでは、プログラミングの現場で使えるかもしれない正規表現の例を、いくつかご紹介します。基本的に、他のプログラミング言語の正規表現でも使えます。
これらの正規表現は、Matcher.mathcesでマッチング確認をすることが前提です。
2-3-1.数値/金額
シンプルな数字の確認(0埋め可、最大桁数 (例では13桁)を明示する場合) | \d{1,13} |
金額(10,000などの3桁区切りも受け付ける) | -?([1-9]\d{0,2}(,\d{3})*|[1-9]\d{0,}) |
金額(100,000.00など、3桁区切りかつ小数点以下も受け付ける) | -?([1-9]\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2})?) |
2-3-2.日付・時刻
YYYY/MM/DD・YYYY-MM-DD・YYYY MM DDのどれか | \d{4}([/\-. ])(0?[1-9]|1[0-2])\1(0?[1-9]|[12][0-9]|3[01]) |
YYYY/MM/DDを、各月でありうる日付のみに制限したもの | \d{4}([/\-. ])((0?[13578]|1[02])\1(0?[1-9]|[12][0-9]|3[01])|0?2\1(0?[1-9]|[12][0-9])|(0?[469]|11)\1(0?[1-9]|[12][0-9]|30)) |
時分秒(HH:MM:SS、24時間制) | (0?[1-9]|1[0-9]|2[0-3]):(0?[0-9]|[12345][0-9]):(0?[0-9]|[12345][0-9]) |
時分秒(HH:MM:SS、12時間制、最後のAM/PMで判断) | (0?[1-9]|1[0-2]):(0?[0-9]|[12345][0-9]):(0?[0-9]|[12345][0-9])(?i)(AM|PM)(?-i) |
時刻に秒以下まで含む場合(ミリ秒まで必須) | (0?[1-9]|1[0-9]|2[0-3]):(0?[0-9]|[12345][0-9]):(0?[0-9]|[12345][0-9])\.\d{3} |
2-3-3.郵便番号(日本国内)
XXX-YYY か XXXYYYY | \d{3}-?\d{4} |
2-3-4.電話番号(日本国内)
携帯電話(IP電話含む) | 0[5789]0-\d{4}-\d{4} |
固定電話(X-Y-Z形式) | 0(?=.{6})\d{2,4}-\d+-\d{4} |
固定電話(X(Y)Z形式) | 0(?=.{6})\d{2,4}\(\d+\)\d{4} |
全て数字の形式(区切り文字、()なし) | 0\d{9} |
市内通話(Y-Z形式) | \d{1,4}-\d{4} |
市内通話((Y)Z形式) | \(\d{1,4}\)\d{4} |
2-3-5.メールアドレス
簡易的なもの | [\w.-]+@[\w-]+\.[\w.-]+ |
HTML5仕様にて推奨されているもの | [a-zA-Z0-9.!#$%&’*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)* |
2-3-6.IPアドレス
IPv4 シンプルなもの(256や999が除外できない) | (\d{1,3}\.){3}.\d{1,3} |
IPv4 各オクテットを0~255まででチェックできるもの | ((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) |
IPv6(参考) | (([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])) |
2-3-7.バーコード・ISBN
※形式的な確認のみ、チェックサムがあるものでも、その確認までは行わない
JAN | \d{8}|\d{13} |
CODE39(可変長) | [\dA-Z-. *$/+%]+ |
NW-7(可変長) | [\dA-Z-.$/+]+ |
ITF(可変長) | \d+ |
ISBN-10 | ISBN(?=.{13})\d{1,5}([- ])\d{1,7}\1\d{1,6}\1(\d|X) |
ISBN-13(2019年時点) | ISBN97[89]([- ])(?=.{13}$)\d{1,5}\1\d{1,7}\1\d{1,6}\1\d |
3.【参考】Matcherの簡単な紹介
さきほどお伝えしたとおり、Javaでの正規表現はPatternだけでは使えず、Matcherがセットになります。ですので、ここでMatcherの基本的な使い方も押さえておきましょう。
どのように文字列とマッチさせるかは、Matcherのメソッドを使い分けます。Matcherにはmatches/lookingAt/findという3つの検索用のメソッドがあり、目的に応じてきちんと使い分ける必要があるのです。
また、Matcher.replaceAllやreplaceFirstも便利に使えるものですので、ここでそれらも少し紹介します。Matcherの知識がないと、この記事の後半がよく分からないということにもなりかねませんからね。
3-1.Matcher.matchesで全体が一致するか調べる
matchesは文字列全体とパターンマッチします。つまり、文字列の最初から最後まで、正規表現と一致するかなので、「文字列がこのパターンを含んでいるかだけ知りたい」というケースでは使いづらいです。
matchesは、String.equalsの正規表現バージョンだと思っていただいてもいいかもしれませんね。
Pattern pattern = Pattern.compile("[a-z]+"); // アルファベットの小文字の連続にマッチするPattern Matcher matcher1 = pattern.matcher("あいうえお abcdefg"); if (matcher1.matches()) { System.out.println("matchesでマッチしました"); } else { System.out.println("matchesでマッチしませんでした"); // → こちら } Matcher matcher2 = pattern.matcher("abcdefg"); if (matcher2.matches()) { System.out.println("matchesでマッチしました"); // → こちら } else { System.out.println("matchesでマッチしませんでした"); }
3-2.Matcher.lookingAtで先頭から一致するか調べる
lookingAtは文字列の先頭からマッチングします。ですので文字列が「このパターンから始まる」かどうかが分かります。lookingAtは、String.startsWithの正規表現版だと考えていただいても、それほどずれていません。
Pattern pattern = Pattern.compile("[a-z]+"); // アルファベットの小文字の連続にマッチするPattern Matcher matcher1 = pattern.matcher("あいうえお abcdefg"); if (matcher1.lookingAt()) { System.out.println("lookingAtでマッチしました"); } else { System.out.println("lookingAtでマッチしませんでした"); // → こちら } Matcher matcher2 = pattern.matcher("abcdefg あいうえお"); if (matcher2.lookingAt()) { System.out.println("lookingAtでマッチしました"); // → こちら } else { System.out.println("lookingAtでマッチしませんでした"); }
3-3.Matcher.findで途中で一致するか調べる
findは文字列中にパターンを一つでも含むかを判断します。このfindもよく使うと思います。そして、文字列中にマッチする箇所が複数あったとしても、findを複数回実行すれば何箇所にマッチしたかが分かります。
ですので、findはString.indexOfに近いイメージです。String.indexOfは開始位置を指定できますが、findを複数回繰り返すということは、indexOfの開始位置を後ろにずらすのと同じです。その意味でも似ていますね。
Pattern pattern = Pattern.compile("[a-z]+"); // アルファベットの小文字の連続にマッチするPattern Matcher matcher1 = pattern.matcher("あいうえお abcdefg"); if (matcher1.find()) { System.out.println("findでマッチしました"); // → こちら } else { System.out.println("findでマッチしませんでした"); } Matcher matcher2 = pattern.matcher("abcdefg あいうえお"); if (matcher2.find()) { System.out.println("findでマッチしました"); // → こちら } else { System.out.println("findでマッチしませんでした"); }
Pattern pattern = Pattern.compile("[0-9]+"); // 数字の連続にマッチするPattern Matcher matcher = pattern.matcher("123 456"); // 1回目のfindは 123 にマッチする if (matcher.find()) { System.out.println("1回目のfindはマッチしました"); // → こちら } else { System.out.println("1回目のfindはマッチしませんでした"); } // 2回目のfindは 456 にマッチする if (matcher.find()) { System.out.println("2回目のfindはマッチしました"); // → こちら } else { System.out.println("2回目のfindはマッチしませんでした"); } // 3回目のfindはもうマッチする部分がないのでfalseになる if (matcher.find()) { System.out.println("3回目のfindはマッチしました"); } else { System.out.println("3回目のfindはマッチしませんでした"); // → こちら }
3-4.Matcher.replaceAll/replaceFirstで一致部分を入れ替える
Matcher.replaceAllとreplaceFirstは、マッチング部分を入れ替えるメソッドです。String.replaceAllとString.replaceFirstの中で行われている処理そのものでもあるのです。
// Java 11の日本語版Javadocより抜粋 public String replaceAll(String replacement) パターンとマッチする入力シーケンスの部分シーケンスを、指定された置換文字列に置き換えます。 パラメータ: replacement - 置換文字列 戻り値: マッチしたすべての部分シーケンスを置換文字列で置き換え、前方参照された部分シーケンスを必要に応じて置換することによって構築された文字列
// Java 11の日本語版Javadocより抜粋 public String replaceFirst(String replacement) パターンとマッチする入力シーケンスの部分シーケンスのうち、最初の部分シーケンスを指定された置換文字列に置き換えます。 パラメータ: replacement - 置換文字列 戻り値: 最初にマッチング部分シーケンスを置換文字列で置き換え、前方参照された部分シーケンスを必要に応じて置換することによって構築された文字列
使い方だけ簡単にお伝えしておきます。replaceAllはマッチング部分すべてを入れ替えて、replaceFirstは最初にマッチング部分のみ入れ替えます。この二つのメソッドのもう少し便利な使い方については後述します。
Pattern pattern = Pattern.compile("[a-z]+"); Matcher matcher1 = pattern.matcher("abcd efgh"); String replaced1 = matcher1.replaceAll("1234"); System.out.println(replaced1); // → 1234 1234、英語部分が全て1234になった Matcher matcher2 = pattern.matcher("abcd efgh"); String replaced2 = matcher2.replaceFirst("1234"); System.out.println(replaced2); // → 1234 efgh、英語部分の最初だけ1234になった
3-5.Matcher.group/groupsでキャプチャー結果を取り出す
Matcherのとても便利なメソッドは、group/groupsです。これらのメソッドは正規表現のキャプチャー(capture)と呼ばれる機能を使うためにあり、Patternとのつながりがとても強いので、後で詳しくお伝えします。
4.正規表現の活用例
ここでは、正規表現の活用例をいくつかご紹介します。
なお、JavaではPatternとMatcherを直接使う以外にも、Stringなどには正規表現が指定できるメソッドがいくつかあります。それらを便利に使うことが、Javaでの正規表現を活用するためのコツだったりするのです。
4-1.空白や区切り文字などの除去、置換
空白や区切り文字の除去や置換は、正規表現の独壇場です。以下に、いくつかのパターンを示します。
これは途中の空白をすべて除去するものです。置換先を“”にすれば、文字の削除と同じ効果があります。
String s = "A B C D"; Pattern p = Pattern.compile(" "); Matcher m = p.matcher(s); String r = m.replaceAll(""); System.out.println(r); // → ABCD
PatternとMatcher.replaceAllを使う代わりに、String.replaceAllでも同じことが出来ます。String.replaceAllは内部的にPatternとMatcherを使っているので、結果は同じになります。
String s = "A B C D"; String r = s.replaceAll(" ", ""); System.out.println(r); // → ABCD
正規表現ですから、空白だけでなく、いくつかの文字を置換対象として同時に指定できます。この例では、空白とタブと改行文字を指定しています。このどれかに該当すればOKです。
String s = "A B\tC\nD"; // タブ(\t)と改行(\n)を含む Pattern p = Pattern.compile("[ \t\n]"); Matcher m = p.matcher(s); String r = m.replaceAll(""); System.out.println(r); // → ABCD
文字列の先頭あるいは末尾の空白文字を削除することも簡単です。String.trimではいわゆる全角空白を削除してくれませんし、先頭だけ・末尾だけの削除もできません。ですが、正規表現を使えば簡単にできます。
String s = " A B C D "; // ←先頭・末尾に全角半角空白が混在しています Pattern p1 = Pattern.compile("^[\\s ]+"); Matcher m1 = p1.matcher(s); String r1 = m1.replaceFirst(""); Pattern p2 = Pattern.compile("[\\s ]+$"); Matcher m2 = p2.matcher(r1); String r2 = m2.replaceFirst(""); System.out.println(r2); // → "A B C D"
以下のように、String.replaceFirstでやっても同じ結果になります。
String s = " A B C D "; // ←先頭・末尾に全角半角空白が混在しています String r = str.replaceFirst("^[\\s ]+", "").replaceFirst("[\\s ]+$", ""); System.out.println(r); // → "A B C D"
\s とは別の、空白文字を表す文字クラス \h を使ってもいいでしょう。こちらには全角空白も含みます。
String s = " A B C D "; // ←先頭・末尾に全角半角空白が混在しています Pattern p1 = Pattern.compile("^\\h+"); Matcher m1 = p1.matcher(s); String r1 = m1.replaceFirst(""); Pattern p2 = Pattern.compile("\\h+$"); Matcher m2 = p2.matcher(r1); String r2 = m2.replaceFirst(""); System.out.println(r2); // → "A B C D"
String.replaceFirstでやる場合は以下のようになります。
String s = " A B C D "; // ←先頭・末尾に全角半角空白が混在しています String r = s.replaceFirst("^\\h+", "").replaceFirst("\\h+$", ""); System.out.println(r); // → "A B C D"
これ以外にも、文字列からのtrimについては以下の記事もあります。いろいろなtrimの仕方について書きましたので、よろしければご覧いただければと思います。
関連記事4-2.文字列の要素分解(空白・カンマ区切りなど)
先ほどは区切り文字の置換などを行いましたが、今度は分割です。文字列の分割はString.splitを使うのが普通だと思いますが、実は内部的にはPatternを使っています。ですから、このようにも書けるのです。
String s = "KEY=VALUE1,VALUE2,VALUE3"; Pattern p = Pattern.compile("[=,]"); String[] split = p.split(s); System.out.println(java.util.Arrays.toString(split)); // → [KEY, VALUE1, VALUE2, VALUE3] System.out.println(split.length); // → 4
String s = "A B C D"; Pattern p = Pattern.compile(" +"); String[] split = p.split(s); System.out.println(java.util.Arrays.toString(split)); // → [A, B, C, D] System.out.println(split.length); // → 4
正規表現を空文字列にすると、文字の境界にマッチするという意味になります。ですので、以下のようにやると文字単位に分割できるのですね。少し裏技チックです。
String s = "ABCDEF"; Pattern p = Pattern.compile(""); String[] split = p.split(s); System.out.println(java.util.Arrays.toString(split)); // → [A, B, C, D, E, F] System.out.println(split.length); // → 6
正規表現は改行文字も指定できるので、複数行を含んだ文字列も行ごとに分解できます。
String s = "A\nBC\r\n\rD"; Pattern p = Pattern.compile("\r\n|\r|\n"); String[] split = p.split(s); System.out.println(java.util.Arrays.toString(split)); // → [A, BC, "", D] System.out.println(split.length); // → 4
区切り文字そのものも、分割した後の文字列に含めたい? それなら、少し特殊な書き方をすればできます。これには、後述する肯定後読みと、肯定先読みという正規表現の機能を使っています。
String s = "A,BC,,D"; Pattern p = Pattern.compile("(?<=,)"); String[] split = p.split(s); System.out.println(java.util.Arrays.toString(split)); // → ["A,", "BC,", ",", "D"] System.out.println(split.length); // → 4
String s = "A,BC,,D"; Pattern p = Pattern.compile("(?=,)"); String[] split = p.split(s); System.out.println(java.util.Arrays.toString(split)); // → ["A", ",BC", ",", ",D"] System.out.println(split.length); // → 4
4-3.文字列のマスク
正規表現は、例えば、何かの文字列をマスクするのにも使えたりします。Matcher.replaceAllの活用例の一つですね。
String s = "The quick brown fox jumps over the lazy dog"; Pattern p = Pattern.compile("fox|dog"); Matcher m = p.matcher(s); String r = m.replaceAll("xxx"); System.out.println(r); // → The quick brown xxx jumps over the lazy xxx
大文字小文字を無視したければ、大文字小文字を無視する埋め込みフラグ (?i) を使うのがいいでしょう。
String s = "The quick brown FoX jumps over the lazy dOg"; Pattern p = Pattern.compile("(?i)fox|dog"); Matcher m = p.matcher(s); String r = m.replaceAll("xxx"); System.out.println(r); // → The quick brown xxx jumps over the lazy xxx
4-4.区切り文字の統一
文字列置換のバリエーションの一つですが、複数の区切り文字が混在した文字列の区切り文字を、何かに統一することもできます。
String s = "A\r\nB\rC\nD"; Pattern p = Pattern.compile("\\r\\n|\\r|\\n"); Matcher m = p.matcher(s); String r = m.replaceAll("\r\n"); System.out.println(r); // → "A\r\nB\r\nC\r\nD" System.out.println(java.util.Arrays.toString(r.getBytes())); // → [65, 13, 10, 66, 13, 10, 67, 13, 10, 68]
String s = "A,B\tC-D"; Pattern p = Pattern.compile("[,\t-]"); Matcher m = p.matcher(s); String r = m.replaceAll(","); System.out.println(r); // → A,B,C,D
4-5.音引きの統一
あまり見ない例かもしれませんが、微妙に違う音引き(“ー“)を一発で統一できたりもします。こんなことはできないかな? ということは、正規表現では実はできてしまったりするのです。
String s = "コンピューターとコンピュータは同じもの?"; Pattern p1 = Pattern.compile("(コンピュータ)([ー])"); Matcher m1 = p1.matcher(s); String r1 = m1.replaceAll("$1"); System.out.println(r1); // → コンピュータとコンピュータは同じもの? Pattern p2 = Pattern.compile("(コンピュータ)([^ー])"); Matcher m2 = p2.matcher(s); String r2 = m2.replaceAll("$1ー$2"); System.out.println(r2); // → コンピューターとコンピューターは同じもの?
4-6.HTML/XML中の属性値の削除
パターンが見つけられさえすれば、対象がHTMLやXMLでも問題ありません。例えば、以下の例ではタグにある不要な属性値(height)について、属性名と値の両方を削除しています。
String s = "<img height=\"100\" class=\"main\" /><img height=\"50\" class=\"footer\" />"; Pattern p = Pattern.compile(" height=\"[^\"]+?\""); Matcher m = p.matcher(s); String r = m.replaceAll(""); System.out.println(r); // → <img class="main" /><img class="footer" />
4-7.ログファイルの解析
ログファイルは決まった形式であることが多いものです。よく見る形式は、日時、ログレベル、ログ出力クラス、ログメッセージが1行にまとまったものです。
そんなログファイルの1行から、それぞれの情報を抜き取るのにも、正規表現は便利に使えます。この例では、後述する正規表現のキャプチャー機能を使っています。String.splitを使ってもいいのですが、少々面倒です。
String s = "2019-05-25 12:34:56.789 INFO - [jp.engineer_club.pattern.Test] Message from program!!"; Pattern p = Pattern.compile("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}) ([A-Z]+) +- \\[([\\w.]+)\\] (.+)$"); Matcher m = p.matcher(s); m.matches(); System.out.println(m.group(1)); // → 2019-05-25 12:34:56.789 System.out.println(m.group(2)); // → INFO System.out.println(m.group(3)); // → jp.engineer_club.pattern.Test System.out.println(m.group(4)); // → Message from program!!
4-8.テンプレートの文字列置換
正規表現でマッチングさせた文字列を、何かのルールで決めた文字列で置換することもできます。以下の例では、正規表現でマッチングさせた文字列に対応する文字列をMapから探して、置換するものです。
これは、ごく簡単なテンプレートエンジンのようなものですね。String.replaceでも同じことはできるのですが、こういうやり方も、やろうとすればできるということです。
Map<String, String> param = new HashMap<>(); param.put("aaaa", "あいうえお"); param.put("bbbb", "かきくけこ"); param.put("cccc", "さしすせそ"); param.put("dddd", "たちつてと"); String s = "{aaaa} {bbbb} {cccc} {dddd}"; String s2 = null; Pattern p = Pattern.compile("(\\{(\\w+?)\\})"); Matcher m = p.matcher(s); while (m.find()) { String key = m.group(2); s2 = m.replaceFirst(param.get(key)); m.reset(s2); } System.out.println(s2); // → あいうえお かきくけこ さしすせそ たちつてと
5.【発展】Patternの便利な機能たち
さて、ここまでがJavaのクラスとしてのPatternとMatcherの基本です。ここからは、PatternとMatcherを256%活用するために必要な知識やプログラミングの仕方をお伝えしていきます。
5-1.【重要】マッチング部分を取り出すキャプチャー
Patternの機能で絶対に使いたいのが、“()”を使ったグループ化とマッチング部分の参照です。正規表現の世界ではキャプチャー(capture)と呼ばれる機能ですが、これを知っているかどうかで作業効率は大違いです。
つまり、正規表現はパターンにマッチするか確認するだけのものではないのです!! マッチング部分をキャプチャーすれば、後から文字列を取得できたり、置換などに活用できることはぜひ知っておきましょう。
5-1-1.Matcher.group(int)を使ってみる
Matcher.groupを使うと、マッチング部分を後から取得できます。つまり、文字列のマッチングと抽出を一度にできるということです。これができれば、面倒なString.substringやindexOfとはもうおさらばです。
Pattern pattern = Pattern.compile("(\\d{4})/(\\d{2})/(\\d{2})"); Matcher matcher = pattern.matcher("今日は2019/04/30です。"); if (matcher.find()) { String ymd = matcher.group(0); System.out.println(ymd); // → 2019/04/30、マッチング部分全体の文字列 String yyyy = matcher.group(1); // → 2019、1番目の()の中の文字列 String mm = matcher.group(2); // → 04、2番目の()の中の文字列 String dd = matcher.group(3); // → 30、2番目の()の中の文字列 System.out.println(yyyy + mm + dd); // → 20190430 }
groupへの数字は、パターン中にある()の1からの連番です。0だとマッチング部分の全体が戻ります。どれだけマッチした箇所があるかは、Matcher.groupCountで分かります。この例では、groupCountは3が戻ります。
Pattern pattern = Pattern.compile("(\\d{4})/(\\d{2})/(\\d{2})"); Matcher matcher = pattern.matcher("今日は2019/04/30です。"); if (matcher.find()) { for (int i = 0; i <= matcher.groupCount(); i++) { System.out.println(matcher.group(i)); // 0:2019/04/30、1:2019、2:04、3:30 } }
もし一つの文字列にマッチする部分が複数あるのなら、findがfalseを戻すまでループをしましょう。findは一つの文字列の中に複数マッチする部分があるなら、複数回実行するとマッチする部分を全部調べてくれます。
Pattern pattern = Pattern.compile("(\\d{4})/(\\d{2})/(\\d{2})"); Matcher matcher = pattern.matcher("今日は2019/04/30です。2019/05/01から令和になります。"); // findがfalseを戻すまでwhile文でループする // findで見つかった単位でgroupができる while (matcher.find()) { String ymd = matcher.group(0); System.out.println(ymd); // → 2019/04/30、2019/05/01 String yyyy = matcher.group(1); String mm = matcher.group(2); String dd = matcher.group(3); System.out.println(yyyy + mm + dd); // → 20190430、20190501 }
5-1-2.Matcher.group(String)を使ってみる
Matcher.group(int)では位置を数字で指定しましたが、グループが多いと分かりづらいですし、数字はずれやすいです。そういう時は?<名前>でグループに名前を付けます。これは「名前付きのキャプチャ」と呼びます。
Pattern pattern = Pattern.compile("(?<year>\\d{4})/(?<month>\\d{2})/(?<day>\\d{2})"); Matcher matcher = pattern.matcher("今日は2019/04/30です。"); if (matcher.find()) { String yyyy = matcher.group("year"); // → 2019 String mm = matcher.group("month"); // → 04 String dd = matcher.group("day"); // → 30 System.out.println(yyyy + mm + dd); // → 20190430 }
あらかじめ名前を付けておけば、その名前でMatcher.group(String)を呼べば、その部分の文字列を取得できます。正規表現内のグループの順番が変わっても文字列取得処理を変えなくてもいいので、なかなか便利ですよ。
5-2.マッチング部分を置換時に$で参照する
()でのグループ化と似ていますが、こちらはMatcher.replaceAllやreplaceFirstで使える機能で、置換先文字列の中で、マッチング部分を $ で参照できるのです。例えば、以下のようなことができます。
Pattern pattern = Pattern.compile("(\\d{4})/(\\d{2})/(\\d{2})"); Matcher matcher = pattern.matcher("今日は2019/04/30です。"); if (matcher.find()) { String replaced = matcher.replaceAll("$1年$2月$3日"); System.out.println(replaced); // → 今日は2019年04月30日です。 }
先ほどのgroupと似ていますね。matcher.group(1)は“2019”ですが、これが$1に対応します。同じようにgroup(2)は“04”で$2、group(3)は“30”で$3です。つまり、$の後ろにgroupの数字を付ければいいのです。
ということは、文字列を入れ替えるだけならgroupをしなくてもいいのです。replaceAll/replaceFirstの置換後文字列に入れ替えたい文字列を指定するだけで、並び替え程度ならサクッと済んでしまうのですね。
さらにグループに名前を付けて、それをreplaceAllでの置換文字列の中で参照させられます。これもMatcher.group(String)と同じ機能です。これらを使えば、文字列置換の処理をより分かりやすくできるのです。
Pattern pattern = Pattern.compile("(?<year>\\d{4})/(?<month>\\d{2})/(?<day>\\d{2})"); Matcher matcher = pattern.matcher("今日は2019/04/30です。"); if (matcher.find()) { String replaced = matcher.replaceAll("${year}年${month}月${day}日"); System.out.println(replaced); // → 今日は2019年04月30日です。 }
ちなみに、Matcher.replaceAllやreplaceFirstは、String.replaceAllとreplaceFirstの内部で行われている処理なので、当然ながら以下のようにもできるのです。これの使い方はいろいろあると思いますよ!!
String str = "今日は2019/04/30です。"; String replaced1 = str.replaceAll("(\\d{4})/(\\d{2})/(\\d{2})", "$1年$2月$3日"); String replaced2 = str.replaceAll("(?<year>\\d{4})/(?<month>\\d{2})/(?<day>\\d{2})", "${year}年${month}月${day}日"); System.out.println(replaced1); // → 今日は2019年04月30日です。 System.out.println(replaced2); // → 今日は2019年04月30日です。
5-3.最短一致をさせてみる
Javaの正規表現は、デフォルトでは最長一致と呼ばれるアルゴリズムでマッチします。つまり、マッチした中でもっとも広い範囲をマッチした範囲とするということなのですが、具体的には以下のようになります。
Pattern pattern = Pattern.compile("あい+"); Matcher matcher = pattern.matcher("あいいいいいいうえお"); // マッチする部分があるかを確認して… if (matcher.find()) { // マッチング部分をprintしてみる System.out.println(matcher.group(0)); // → あいいいいいい }
このパターンでは、“あ“の後ろに“い“があって、さらに“い“が1つ以上連続しているものです。ここで、検索文字列から一番長くマッチする部分を探すので(最長一致)、“い“が連続している最後の部分までマッチしています。
これを一番少ないところまでとするのが「最短一致」です。最短一致にする場合は、繰り返しを指定するパターンの後ろへ“?”を付けます。以下の例だと、マッチする部分が一番短い“あい“になりましたね。
Pattern pattern = Pattern.compile("あい+?"); Matcher matcher = pattern.matcher("あいいいいいいうえお"); if (matcher.find()) { System.out.println(matcher.group(0)); // → あい }
このように、正規表現ではマッチさせたい範囲をコントロールすることもできるのです。
5-3-1.【参考】強欲な数量子を使ってみる
Javaの正規表現には、少々珍しい機能の「強欲な数量子(Possessive Quantifiers)」があります(数量子の後ろにさらに+を付ける)。言葉での説明は少々難しいですが、例えば以下のようなマッチングの差が出てきます。
Pattern pattern1 = Pattern.compile("[a-z]+z"); Pattern pattern2 = Pattern.compile("[a-z]++z"); // 同じ文字列に対して最長一致・強欲な数量子の両方でマッチさせてみる Matcher matcher1 = pattern1.matcher("abcdefgz"); Matcher matcher2 = pattern2.matcher("abcdefgz"); if (matcher1.find()) { System.out.println(matcher1.group(0)); // → abcdefgz } else { System.out.println("matcher1はマッチしませんでした"); } if (matcher2.find()) { System.out.println(matcher2.group(0)); } else { System.out.println("matcher2はマッチしませんでした"); // → こちら }
最初のパターンでは、まず[a-z]+の部分でabcdefgzの全体にマッチできますよね。でもパターンの最後にzがあるので、[a-z]+でマッチする部分をちょっとだけ少なくして、zもマッチするような調整が自動で入ります。
「強欲な数量子」だと、[a-z]+の部分でabcdefgzの全体にマッチした後に、パターン最後のz向けの調整が入りません。マッチング結果を手放さないので「強欲な」なのです。なので、[a-z]++zではマッチしないのです。
この強欲な数量子を使うのは、例えば正規表現のマッチング処理の速度が必要な時です。前述のとおり、強欲な数量子だと再マッチを試みないので、その分処理が速くなります。でも、結果が違ってくるので要注意です。
5-4.【発展】動作を変えるフラグを使ってみる
Patternの動作を変えるには、いくつかのフラグが使えます。これは、オーバーロードされているPattern.compile(String, int)の二つ目のintへ指定できる、Patternのpublic staticな定数です。
// パターンは英語の小文字だけだが、修飾子として大文字小文字無視(CASE_INSENSITIVE)を指定した Pattern pattern = Pattern.compile("[a-z]", Pattern.CASE_INSENSITIVE); // パターンマッチさせる文字列は、全て英語の大文字のみ Matcher matcher = pattern.matcher("ABCDEFG"); // パターンは英語小文字だが、英語大文字の対象文字列にもマッチさせられる if (matcher.find()) { System.out.println("マッチしました"); }
複数の修飾子を同時に有効にする場合は、intの数値を論理和演算子(|)でつなぎ合わせます。
// CASE_INSENSITIVE と UNICODE_CASE と UNIX_LINES を同時に有効にする Pattern pattern = Pattern.compile("[a-z]", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE | Pattern.UNIX_LINES);
あるいは、正規表現中に特別な文字を埋め込むことでも、同じ効果が得られます(埋め込みフラグと呼びます)。埋め込みフラグを解除するには、(?-x)とします。
// CASE_INSENSITIVE を正規表現内で有効にする(?i) Pattern pattern1 = Pattern.compile("(?i)[a-z]"); // CASE_INSENSITIVE と UNICODE_CASE と UNIX_LINES を同時に正規表現内で有効にする Pattern pattern2 = Pattern.compile("(?iud)[a-z]");
どういうフラグがあるかは、PatternのJavadocのフィールド説明部分を参照してください。それぞれのフラグにはどういう意味があるか、埋め込みフラグとしてはどう指定すればいいのかもきちんと書かれています。
5-5.【発展】後方参照をしてみる
正規表現の中で、同じマッチ結果が出てくるか調べたいことがあります。そういうことを正規表現の「後方参照」を使えば一発で出来たりします。例えば、以下の正規表現で申請者と承認者の名前が同じか調べられます。
Pattern pattern = Pattern.compile("申請者=(.+?) 承認者=\\1"); Matcher matcher1 = pattern.matcher("申請者=山田太郎 承認者=山田太郎"); Matcher matcher2 = pattern.matcher("申請者=山田太郎 承認者=鈴木次郎"); if (matcher1.find()) { System.out.println(matcher1.group(1)); // → 山田太郎、()がマッチしている部分が \1 になる } else { System.out.println("申請者と承認者は同じではありません"); } if (matcher2.find()) { System.out.println(matcher2.group(1)); } else { System.out.println("申請者と承認者は同じではありません"); // → こちら }
パターン中の \1 がポイントです。これは先の (.+?) の結果を参照しています。以前のマッチ結果を参照しているので「後方参照」と呼びます。\の後ろの数字は () の数に対応していて、Matcher.group(1)と同じです。
これも数字ではなく文字で名前を付けられます。名前は ?<名前> でつけて、\k<名前> で参照します。これなら ()の位置が変わっても、名前は変わらないので便利ですね。要はMatcher.group(“person”)と同じです。
Pattern pattern = Pattern.compile("申請者=(?<person>.+?) 承認者=\\k<person>"); Matcher matcher = pattern.matcher("申請者=山田太郎 承認者=山田太郎"); if (matcher.find()) { System.out.println(matcher.group("person")); // → 山田太郎 } else { System.out.println("申請者と承認者は同じではありません"); }
5-6.【発展】先読み・後読みをしてみる
正規表現には先読みと後読みと呼ばれる機能があります。さらにパターンがマッチする・しないで肯定と否定の二種類がありますので、先読み・後読み、肯定・否定を組み合わせると以下の四種類のパターンがあります。
- 肯定先読み
- 否定先読み
- 肯定後読み
- 否定後読み
5-6-1.肯定先読み
肯定先読み(?=)とは、何かのパターンの「先」に、指定されたパターンがさらにあるか確認することです。普通のパターンとの違いは、マッチング結果には先にあると指定したパターンが含まれないことです。
Pattern pattern = Pattern.compile("猫は(?=.+かわいい)"); Matcher matcher = pattern.matcher("猫はこの宇宙で最もかわいい生物です"); if (matcher.find()) { System.out.println(matcher.group(0)); // → "猫は"、後ろの部分はマッチしていない }
この例では、“猫は“の後ろに、何かの文字がありつつ“かわいい“があるか肯定先読みで確認しています。実際には条件を満たすのでfindはtrueを戻し、マッチング部分には先読みした部分は入っていないことに要注目です。
5-6-2.否定先読み
否定先読み(?!)とは、肯定先読みの逆で、何かのパターンの「先」に、指定されたパターンがないことを確認することです。では、これも例で見てみましょう。
Pattern pattern = Pattern.compile("猫は(?!.+かわいくない)"); Matcher matcher = pattern.matcher("猫はこの宇宙で最もかわいい生物です"); if (matcher.find()) { System.out.println(matcher.group(0)); // → "猫は"、後ろの部分はマッチしていない }
この例では、“猫は“の後ろには“かわいくない“という文字列は出現していませんよね。ですので、findの結果はtrueになり、マッチング部分も肯定先読みと同じように“猫は“となるのです。
5-6-3.肯定後読み
さて、大体パターンはつかめてきたかもしれませんが、肯定後読み(?<=)とは何かのパターンの「前」に、指定されたパターンがあることを確認することです。これも例で見てみましょう。
Pattern pattern = Pattern.compile("(?<=かわいい生物は)猫です"); Matcher matcher = pattern.matcher("この宇宙で最もかわいい生物は猫です"); if (matcher.find()) { System.out.println(matcher.group(0)); // → "猫です"、前の部分はマッチしていない }
ただし、Javaの後読みでは数量子が使えません。つまり、+と*が使えないということです。{}と?は使えます。これは、後読みの実装難易度が高いからのようで、プログラミング言語によって使えるものが違うのです。
5-6-4.否定後読み
最後の否定後読み(?<!)も例を出します。他のものと同じ流れですね。
Pattern pattern = Pattern.compile("(?<!かわいくない生物は)猫です"); Matcher matcher = pattern.matcher("この宇宙で最もかわいい生物は猫です"); if (matcher.find()) { System.out.println(matcher.group(0)); // → "猫です"、前の部分はマッチしていない }
6.まとめ
この記事では、JavaのPatternを足掛かりにして、正規表現の世界に少し足を踏み入れてみました。
Javaでの正規表現はPattern.compileでPatternのインスタンスを作って使うもので、一旦作ったPatternは流用ができるものです。そして、マッチングではPatternからMatcherを生成して、そのメソッドを呼び出します。
PatternとMatcherの重要な機能は、マッチング結果を後から参照できるキャプチャーです。キャプチャーを使えるかで、文字列処理の効率が大きく変わります。簡単なものでいいので、ぜひ使えるようになってください。
Javaの正規表現は、他のプログラミング言語のものに負けず劣らず強力です。プログラミング言語の組み込み構文ではありませんが、必要な機能は揃っていますので、ガンガン便利に活用しましょう。
コメント