マルチバイト正規表現関係のメモ

久しぶりに mbstring モジュール(特にマルチバイト正規表現)のソースコードを眺めていたのですが、いくつか面白い(知らなかった)挙動がありましたでメモしておきます。
確認した PHP のバージョンは 5.4.10 ですが、PHP 5.0 以降であれば、あまり変わりません。

パターン修飾子 m と s の解釈

これまでは Perl 互換の正規表現と同じと思っていたのですが、マルチバイト正規表現のパターン修飾子 m と s は Perl 互換の正規表現(preg)と挙動が違います。
mb_ereg_replace() と preg_replace() で、改行を含む文字列置き換えの結果を比較すると以下のようになります。

$ php -r 'echo mb_ereg_replace( ".", "A", "|\n|", "s" );'
A
A

$ php -r 'echo preg_replace( "/./s", "A", "|\n|" );'
AAA
$ php -r 'echo mb_ereg_replace( ".", "A", "|\n|", "m" );'
AAA

$ php -r 'echo preg_replace( "/./m", "A", "|\n|" );'
A
A

上記の挙動に関して、PHPマニュアルのmb_regex_set_options()に以下の記載があります。


オプション意味
m'.' が改行にマッチする
s'^' -> '\A', '$' -> '\Z'

PHPマニュアルのPCRE(Perl互換の正規表現)のパターン修飾子には、以下のように記載されています。


m (PCRE_MULTILINE)
デフォルトで、PCRE は、検索対象文字列を(実際には複数行からなる 場合でも)単一の行からなるとして処理します。 「行頭」メタ文字 (^) は、対象文字列の最初にしかマッチしません。 一方、「行末」メタ文字 ($) は、文字列の最後、または (D 修飾子が設定されていない場合) 最後にある改行記号の前のみにしかマッチしません。 この動作は Perl と同じです。 この修飾子を設定すると、「行頭」および「行末」メタ文字は 対象文字列において、文字列の最初と最後に加えて、各改行の直前と直後にそれぞれマッチします。 この動作は、Perl の /m 修飾子と同じです。 対象文字列の中に "\n" 文字がない場合や、またはパターンに ^ または $ がない場合は、この修飾子を設定しても意味はありません。
s (PCRE_DOTALL)
この修飾子を設定すると、パターン中のドットメタ文字は 改行を含む全ての文字にマッチします。 これを設定しない場合は、改行にはマッチしません。この修飾子は、Perl の /s 修飾子と同じです。 [^a] のような否定の文字クラスは、この修飾子の設定によらず、常に改行文字にマッチします。
ただ、mb_ereg_replace()では、preg_replace()の m 修飾子と同様に、各行(^ と $ を使用)に対して置き換えを実行したい場合、第4引数のオプションを空にする必要があります。

$ php -r 'echo mb_ereg_replace( "^.*$", "A", "|\n|", "" );'
A
A

mb_ereg_replace() の第1引数には数値が入力可能

mb_ereg_replace() の第1引数(パターン文字列)には数値が指定可能です。
数値を指定すると、1文字の文字列に変換して処理されます(0 → NULLバイト, 65 → A)。

$ php -r 'var_dump( mb_ereg_replace( 0, "_", "A\0Z" ) );'
string(3) "A_Z"

$ php -r 'var_dump( mb_ereg_replace( 65, "_", "A\0Z" ) );'
string(3) "_Z"

第1引数に文字列でない型を入力すると、数値変換された結果が適用されます。例えば、配列を与えると以下のようになります(空の配列を数値変換すると 0、空以外の配列を数値変換すると 1)。

$ php -r 'var_dump( mb_ereg_replace( [], "_", "A\0Z" ) );'
string(3) "A_Z"

$ php -r 'var_dump( mb_ereg_replace( [0], "_", "A\0Z" ) );'
string(3) "AZ"

$ と \Z が改行にマッチした場合の文字列置き換えがおかしい?

mb_ereg_replace() の場合、置き換え対象文字列の最後に改行があり、$ や \Z(行末にマッチ) がマッチする場合、改行まで置き換えてしまいます。

$ php -r 'var_dump( mb_ereg_replace( ".\Z", "B", "A\n" ) );'
string(2) "BB"

$ php -r 'var_dump( mb_ereg_replace( ".$", "B", "A\n" ) );'
string(2) "BB"

preg_replace() では以下のようになるので、上記の挙動はバグな気がします。

$ php -r 'var_dump( preg_replace( "/.\Z/", "B", "A\n" ) );'
string(2) "B
"

$ php -r 'var_dump( preg_replace( "/.$/", "B", "A\n" ) );'
string(2) "B
"