Hatena::ブログ(Diary)

A Day in Serenity @ kenjis

2013-02-21

PHP の正規表現があまりに複雑なのでまとめてみた

できるだけ正確な記述を目指していますが、誤りがありましたら、お知らせ願います。

(最終更新: 2013/3/29 11:22)

正規表現の種類

まず、PHP には以下の 3種類の正規表現があります。

  1. Perl 互換の正規表現 (pcre)
  2. mbstring の正規表現 (mbregex)
  3. POSIX 拡張正規表現 (regex)

このうち、regex は

  • バイナリセーフでない
  • 日本語は扱えない
  • PHP 5.3 で非推奨

なので使わない方がいいでしょう。見つけたら、随時 pcre か mbregex で書き直しましょう。

Perl 互換の正規表現 (pcre)

mbstring の正規表現 (mbregex)

パターンの書き方

正規表現パターンは文字列として記述します。PHP には正規表現リテラルがありません。

  • 文字列なので「'」で囲む (「"」だと変数展開されるなどがありややこしい)
  • 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
var_dump(mb_ereg('\w', ''));
var_dump(preg_match('/\w/u', '')); // 全角の1
var_dump(mb_ereg('\w', ''));

この結果は、以下のようになります。

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
var_dump(mb_ereg('\d', ''));
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(
    // 全角漢字、英数、ひらがな、カタカタ
    '', '', '', '', '',
    // 全角記号
    '', '_',
    // 全角スペース
    ' ',
    // 半角カナ
    '',
);

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(
    // 全角漢字、英数、ひらがな、カタカタ
    '', '', '', '', '',
    // 全角記号
    '', '_',
    // 全角スペース
    ' ',
    // 半角カナ
    '',
);

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 でしかマッチしません。

結局、どうすればいいのか?

  • ロケール、文字エンコーディング、オプションを正しく設定する
  • 日本語がマッチすることを期待しない場合は、\w, \d, [:alnum:] などの文字クラスを使わない
  • 必ず、ユニットテストを書く

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 の仕様が突然変わった場合も発見できます。

関連

投稿したコメントは管理者が承認するまで公開されません。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/Kenji_s/20130221/php_regexp