PHP の正規表現があまりに複雑なのでまとめてみた
できるだけ正確な記述を目指していますが、誤りがありましたら、お知らせ願います。
(最終更新: 2013/3/29 11:22)
正規表現の種類
このうち、regex は
- バイナリセーフでない
- 日本語は扱えない
- PHP 5.3 で非推奨
なので使わない方がいいでしょう。見つけたら、随時 pcre か mbregex で書き直しましょう。
Perl 互換の正規表現 (pcre)
- 正規表現エンジンは Perl の「PCRE」
- 日本語は UTF-8 のみ扱える
- UTF-8 を使う場合は、パターン修飾子に u を指定する
- 文字クラスはロケールの影響を受ける
- 処理の制限値 (pcre.backtrack_limit, pcre.recursion_limit) を超えるとマッチしなかったことになる
- マッチするはずの正規表現がマッチしない現象 参照
- preg_last_error() で確認できる
- UTF-8 では PHP 5.3.4 (Windows ではもう少し後?) から \w, \d, [:alnum:] などの文字クラスが日本語文字にもマッチするように変わっている
- 「.」はデフォルトでは改行にマッチしない
- パターン修飾子「e」は危険なので使わない方がよい
- PHP 5.5.0 で非推奨。PHP: 正規表現パターンに使用可能な修飾子 - Manual 参照
- 2010-08-15
mbstring の正規表現 (mbregex)
- 正規表現エンジンは Ruby の「鬼車」
- マルチバイト文字列が扱えるが、mbstring 関数で使える文字エンコーディングでも使えないものがある
- オプションは mb_regex_set_options() で変更できる
- デフォルトのオプションは pr = msr
- m → '.' が改行にマッチする
- s → '^' -> '\A', '$' -> '\Z'
- r → Ruby 構文
- 文字エンコーディングは mbstring.internal_encoding か mb_regex_encoding() で指定する
- mb_internal_encoding() で文字エンコーディングを変更しても影響しない
- Perl 形式の文字クラス (\w など) は、文字エンコーディングにより範囲が異なる
- ロケールの影響を受けない
- 「.」はデフォルトで改行にマッチする
- mb_ereg_match() は、文字列の先頭からのみマッチする
- mb_ereg_search() という少し変わった関数がある
- 2011-02-08 参照
- mb_ereg_replace() の e 修飾子は危険なので使わない方がよい
- 2011-02-06 参照
パターンの書き方
正規表現パターンは文字列として記述します。PHP には正規表現リテラルがありません。
ややこしいですが、パターンの文字列を echo して表示された結果が、正規表現エンジンに渡されるパターンになります。
$ cat test.php <?php echo '/\\\\/', PHP_EOL; $ php test.php /\\/
- pcre ではパターンをデリミタ (通常は「/」) で囲む必要がある
pcre での日本語の処理
pcre で日本語を処理する場合は、文字エンコーディングは UTF-8 にし、パターン修飾子 u を付ける必要があります。付けないとマルチバイト文字が適切に処理されず、意図した結果を得られない場合があります。
<?php if (PHP_OS === 'WIN32' || PHP_OS === 'WINNT') { setlocale(LC_ALL, 'C'); } else { if (setlocale(LC_ALL, 'ja_JP.UTF-8') === false) { exit('Can\'t set locale to UTF-8'); } } $str = 'aあc'; var_dump(preg_match('/a.c/', $str)); var_dump(preg_match('/a.c/u', $str)); $str = 'いう'; var_dump(preg_match('/あ?いう/', $str)); var_dump(preg_match('/あ?いう/u', $str));
上記の結果は、以下のようになります。
int(0) int(1) int(0) int(1)
文字クラス
\w, \d, \s について確認してみましょう (PHP 5.3.21)。
まず、\w の全角の漢字、かな、英数。
<?php if (PHP_OS === 'WIN32' || PHP_OS === 'WINNT') { setlocale(LC_ALL, 'C'); } else { if (setlocale(LC_ALL, 'ja_JP.UTF-8') === false) { exit('Can\'t set locale to UTF-8'); } } mb_regex_encoding('UTF-8'); var_dump(preg_match('/\w/u', '亜')); var_dump(mb_ereg('\w', '亜')); var_dump(preg_match('/\w/u', 'あ')); var_dump(mb_ereg('\w', 'あ')); var_dump(preg_match('/\w/u', 'ア')); var_dump(mb_ereg('\w', 'ア')); var_dump(preg_match('/\w/u', 'A')); // 全角のA var_dump(mb_ereg('\w', 'A')); var_dump(preg_match('/\w/u', '1')); // 全角の1 var_dump(mb_ereg('\w', '1'));
この結果は、以下のようになります。
int(1) int(1) int(1) int(1) int(1) int(1) int(1) int(1) int(1) int(1)
全角の記号。
<?php if (PHP_OS === 'WIN32' || PHP_OS === 'WINNT') { setlocale(LC_ALL, 'C'); } else { if (setlocale(LC_ALL, 'ja_JP.UTF-8') === false) { exit('Can\'t set locale to UTF-8'); } } mb_regex_encoding('UTF-8'); var_dump(preg_match('/\w/u', ' ')); // 全角スペース var_dump(mb_ereg('\w', ' ')); var_dump(preg_match('/\w/u', '!')); // 全角の感嘆符 var_dump(mb_ereg('\w', '!')); var_dump(preg_match('/\w/u', '。')); var_dump(mb_ereg('\w', '。')); var_dump(preg_match('/\w/u', '_')); // 全角のアンダースコア var_dump(mb_ereg('\w', '_'));
この結果は、以下のようになります。
int(0) bool(false) int(0) bool(false) int(0) bool(false) int(0) int(1)
全角のアンダースコアは、pcre では \w にマッチしませんが、mbregex ではマッチします。
半角カナ。
<?php if (PHP_OS === 'WIN32' || PHP_OS === 'WINNT') { setlocale(LC_ALL, 'C'); } else { if (setlocale(LC_ALL, 'ja_JP.UTF-8') === false) { exit('Can\'t set locale to UTF-8'); } } mb_regex_encoding('UTF-8'); var_dump(preg_match('/\w/u', 'ア')); // 半角カナ var_dump(mb_ereg('\w', 'ア'));
この結果は、以下のようになります。
int(1) int(1)
次に \d です。
<?php if (PHP_OS === 'WIN32' || PHP_OS === 'WINNT') { setlocale(LC_ALL, 'C'); } else { if (setlocale(LC_ALL, 'ja_JP.UTF-8') === false) { exit('Can\'t set locale to UTF-8'); } } mb_regex_encoding('UTF-8'); var_dump(preg_match('/\d/u', '1')); // 全角の1 var_dump(mb_ereg('\d', '1')); var_dump(preg_match('/\d/u', '一')); // 漢数字の1 var_dump(mb_ereg('\d', '一'));
この結果は、以下のようになります。
int(1) int(1) int(0) bool(false)
全角数字は \d にマッチしますが、漢数字はマッチしません。
最後に \s です。
<?php if (PHP_OS === 'WIN32' || PHP_OS === 'WINNT') { setlocale(LC_ALL, 'C'); } else { if (setlocale(LC_ALL, 'ja_JP.UTF-8') === false) { exit('Can\'t set locale to UTF-8'); } } mb_regex_encoding('UTF-8'); var_dump(preg_match('/\s/u', ' ')); // 全角スペース var_dump(mb_ereg('\s', ' '));
この結果は、以下のようになります。
int(1) int(1)
続いて、mbregex での \w の文字エンコーディングの違いについてもみておきましょう。
<?php $provider = array( // 全角漢字、英数、ひらがな、カタカタ '亜', 'A', '1', 'あ', 'ア', // 全角記号 '!', '_', // 全角スペース ' ', // 半角カナ 'ア', ); echo 'mbregex: UTF-8 SJIS EUC-JP' . PHP_EOL; foreach ($provider as $str) { echo $str . ': '; mb_regex_encoding('UTF-8'); $test = mb_ereg('\w', $str); if ($test === false) $test = '0'; echo $test . ' '; mb_regex_encoding('SJIS'); $str_s = mb_convert_encoding($str, 'SJIS', 'UTF-8'); $test = mb_ereg('\w', $str_s); if ($test === false) $test = '0'; echo $test . ' '; mb_regex_encoding('EUC-JP'); $str_e = mb_convert_encoding($str, 'EUC-JP', 'UTF-8'); $test = mb_ereg('\w', $str_e); if ($test === false) $test = '0'; echo $test . ' '; echo PHP_EOL; }
この結果は、以下のようになります。
mbregex: UTF-8 SJIS EUC-JP 亜: 1 1 1 A: 1 1 1 1: 1 1 1 あ: 1 1 1 ア: 1 1 1 !: 0 1 1 _: 1 1 1 : 0 1 1 ア: 1 0 1
SJIS と EUC-JP では、全角記号も \w にマッチします。半角カナは SJIS では \w にマッチしません。
\d はどうでしょうか?
<?php $provider = array( // 全角漢字、英数、ひらがな、カタカタ '亜', 'A', '1', 'あ', 'ア', // 全角記号 '!', '_', // 全角スペース ' ', // 半角カナ 'ア', ); echo 'mbregex: UTF-8 SJIS EUC-JP' . PHP_EOL; foreach ($provider as $str) { echo $str . ': '; mb_regex_encoding('UTF-8'); $test = mb_ereg('\d', $str); if ($test === false) $test = '0'; echo $test . ' '; mb_regex_encoding('SJIS'); $str_s = mb_convert_encoding($str, 'SJIS', 'UTF-8'); $test = mb_ereg('\d', $str_s); if ($test === false) $test = '0'; echo $test . ' '; mb_regex_encoding('EUC-JP'); $str_e = mb_convert_encoding($str, 'EUC-JP', 'UTF-8'); $test = mb_ereg('\d', $str_e); if ($test === false) $test = '0'; echo $test . ' '; echo PHP_EOL; }
この結果は、以下のようになります。
mbregex: UTF-8 SJIS EUC-JP 亜: 0 0 0 A: 0 0 0 1: 1 0 0 あ: 0 0 0 ア: 0 0 0 !: 0 0 0 _: 0 0 0 : 0 0 0 ア: 0 0 0
\d の場合は、全角数字は UTF-8 でしかマッチしません。
結局、どうすればいいのか?
setlocale() でロケールを設定します。
<?php if (setlocale(LC_ALL, 'ja_JP.UTF-8') === false) { exit('Can\'t set locale to UTF-8'); }
Windows ではロケールを UTF-8 にする方法がないようなので、どうするのが正しいのかはわかりません。知ってる人がいましたら、お教え願いたいです。
pcre では、UTF-8 の場合は、'/abc/u' のように必ず「u」を指定します。
mbregex では、mb_regex_encoding() で文字エンコーディングを指定し、必要があれば mb_regex_set_options() でオプションを変更します。
例えば、半角数字を期待するなら \d ではなく [0-9] を使います。
最後に、正常系、異常系についてユニットテストを書いておきます。そうすれば、自分の思い違い、正規表現エンジンの違いによる微妙な違い、万一 PHP の仕様が突然変わった場合も発見できます。