似ているようで微妙に違う ^ \A と $ \z

正規表現で先頭意味するのは ^ 、末尾は $ というのは、ネットで正規表現と検索すればどのサイトでも当たり前のように書かれています。私もずっとそうだと思ってました。ところがあるとき、徳丸浩さんの「体系的に学ぶ 安全なWebアプリケーションの作り方 第2版」を読んでいて、先頭末尾は ^$ だけじゃないこと、それでは不十分だということを知りました。

先頭なのか行頭なのか、末尾なのか行末なのか、どっちなんだよおおおお〜。どう違うのかイマイチよくわからなかったので、実際にPHPでコードを書いて違いを見比べてみました。今回実験した環境は CentOS 7.5 PHP 5.4.16 です。

対象文字列には、以下のように途中に改行コードが入っています。
$str = “abc123\nXYZ987”;

実験結果(1) ^ $ を使った場合

まずは1行目の文字列 abc123 の検索を行います。

true = ereg “abc123”
true = preg “/abc123/”
true = preg “/abc123/s”
true = preg “/abc123/m”
部分一致。普通にマッチします。

true = ereg “^abc123”
true = preg “/^abc123/”
true = preg “/^abc123/s”
true = preg “/^abc123/m”
^で行頭一致。先頭でもあり行頭でもあるので区別はつきません。

false = ereg “abc123$”
false = preg “/abc123$/”
false = preg “/abc123$/s”
true = preg “/abc123$/m”
$で行末一致。この結果から、改行コードは行末として扱われていないようなので、$は「行末」ではなく「末尾」(文字列の終端)でマッチすると考えられます。(実際はちょっと違う。後述にて。)なお、pregは/mを付けると改行に行末としてマッチします。

false = ereg “^abc123$”
false = preg “/^abc123$/”
false = preg “/^abc123$/s”
true = preg “/^abc123$/m”
^$で行頭行末一致。マッチしません。上記の結果と同じです。

続いて、2行目の文字列 XYZ987 の検索に挑みます。

true = ereg “XYZ987”
true = preg “/XYZ987/”
true = preg “/XYZ987/s”
true = preg “/XYZ987/m”
部分一致。改行のあとの2行目も検索対象として機能していることがわかります。

false = ereg “^XYZ987”
false = preg “/^XYZ987/”
false = preg “/^XYZ987/s”
true = preg “/^XYZ987/m”
^で行頭一致。1行目の場合と結果が変わりました。2行目にあるので「行頭」のはずなのですがマッチしません。この結果からは、^ は「行頭」ではなく「先頭」でマッチすると考えられます。なお、pregの/mをつけると改行を行頭としてマッチします。

true = ereg “XYZ987$”
true = preg “/XYZ987$/”
true = preg “/XYZ987$/s”
true = preg “/XYZ987$/m”
$で行末一致。末尾でもあり行末でもあるので区別はつきません。

false = ereg “^XYZ987$”
false = preg “/^XYZ987$/”
false = preg “/^XYZ987$/s”
true = preg “/^XYZ987$/m”
^$で行頭行末一致。やはり ^ は行頭ではないことがわかります。pregの/mをつけると改行を行頭としてマッチします。

true = ereg “^abc123\nXYZ987$”
true = preg “/^abc123\nXYZ987$/”
true = preg “/^abc123\nXYZ987$/s”
true = preg “/^abc123\nXYZ987$/m”
最後に文字列全体を比較してみました。今までの結果から、^は先頭、$は末尾、であったので上の3つはわかります。pregの/mをつけると、改行は先頭(行頭)としても末尾(行末)としてもマッチするようです。

実験結果(2) \A \z を使った場合

次に、先頭を意味する \A と末尾を意味する \z を使った場合にどうなるか試してみました。

true = preg “/abc123/”
true = preg “/abc123/s”
true = preg “/abc123/m”

true = preg “/\Aabc123/”
true = preg “/\Aabc123/s”
true = preg “/\Aabc123/m”

false = preg “/abc123\z/”
false = preg “/abc123\z/s”
false = preg “/abc123\z/m”
\z は行末にはマッチしません。$ と同じ結果です。
/mの場合、$ではtrueでしたが、\zではfalseになりました。

false = preg “/\Aabc123\z/”
false = preg “/\Aabc123\z/s”
false = preg “/\Aabc123\z/m”
/mの場合、$ではtrueでしたが、\zではfalseになりました。

true = preg “/XYZ987/”
true = preg “/XYZ987/s”
true = preg “/XYZ987/m”

false = preg “/\AXYZ987/”
false = preg “/\AXYZ987/s”
false = preg “/\AXYZ987/m”
\A は行頭にはマッチしません。$ と同じ結果です。
/mの場合、$ではtrueでしたが、\zではfalseになりました。

true = preg “/XYZ987\z/”
true = preg “/XYZ987\z/s”
true = preg “/XYZ987\z/m”

false = preg “/\AXYZ987\z/”
false = preg “/\AXYZ987\z/s”
false = preg “/\AXYZ987\z/m”
/mの場合、$ではtrueでしたが、\zではfalseになりました。

true = preg “/\Aabc123\nXYZ987\z/”
true = preg “/\Aabc123\nXYZ987\z/s”
true = preg “/\Aabc123\nXYZ987\z/m”

まとめ

preg関数の修飾子なしの場合、
^ は行頭ではなく、文字列の先頭にマッチする。
$ は行末ではなく、文字列の末尾にマッチする。

なので、通常は ^ $ を使っていれば問題なさそうに見えます。しかし…

$ は末尾の改行コードを見逃す、とは?

先ほどの本(Webページはこちら)に「データ末尾に改行が含まれている場合を見逃してしまう」と書かれています。本当にそうなのか、試してみました。

末尾に改行コード。 $str = “abc123\n”; の場合
false = ereg “^abc123$”
true = preg “/^abc123$/”
true = preg “/^abc123$/s”
true = preg “/^abc123$/m”
false = preg “/\Aabc123\z/”
false = preg “/\Aabc123\z/s”
false = preg “/\Aabc123\z/m”
pregの場合、末尾は改行コードのはずなのに、$ でマッチしてしまっています!
eregの場合は正常ですね。

先頭に改行コード。 $str = “\nabc123”; の場合
false = ereg “^abc123$”
false = preg “/^abc123$/”
false = preg “/^abc123$/s”
true = preg “/^abc123$/m”
false = preg “/\Aabc123\z/”
false = preg “/\Aabc123\z/s”
false = preg “/\Aabc123\z/m”
^ は改行コードにマッチしません。こちらは正常です。

たしかに $ を使うと、末尾の改行コードがすり抜けてしまうことがわかりました。

結論

^ はかまわないが、$ は使わず代わりに \z を使う方が良い。

LINEで送る
Pocket