Edited at

PHPを7.2にバージョンアップしたら正規表現でマッチしない現象に出くわした

PHP5.6からPHP7.2にバージョンを上げた際に出くわした正規表現の制限の話です。


TL;DL


  • 正規表現の検索対象の文字数には気をつけよう

  • 正規表現でうまくいかない現象に出くわしたらエラーコードを調べよう


正規表現でうまくマッチしない現象に出くわす

長めのテキストをDBに保存しているLaravelのプロジェクトがあり、その長めのテキストはテンプレートとして機能していました。

文章の中に、<% tmpl_start %><% tmpl_finish %>があり、その2つの間に<% hogehoge %> みたいな文字列があったら ほげ に置換、<% piyopiyo %> みたいな文字列があったら ぴよ に置換、といった具合です。コードで表すとこんな感じ。

$hoge     = 'ほげ';

$piyo = 'ぴよ';
$longText = '<% tmpl_start %> これは<% hogehoge %><% piyopiyo %>。 <% tmpl_finish %>';
preg_match('/<% tmpl_start %>(.|[\r\n])*?<% tmpl_finish %>/', $longText, $matches);

if (count($matches)) {
$parsedText = str_replace('<% tmpl_start %>', 'スタート:', $matches[0]);
$parsedText = str_replace('<% hogehoge %>', $hoge, $parsedText);
$parsedText = str_replace('<% piyopiyo %>', $piyo, $parsedText);
$parsedText = str_replace('<% tmpl_finish %>', ':終了', $parsedText);
$longText = preg_replace('/<% tmpl_start %>(.|[\r\n])*?<% tmpl_finish %>/', $parsedText, $longText);
}

echo $longText; // `スタート: これはほげぴよ。 :終了` と出力される

そのテンプレートはエンジニア以外でもいじれるようにしており、文章の長さの制限はしていませんでした。

なので、<% tmpl_start %><% tmpl_finish %> の間に数千文字入る可能性がある状態です。

その状態でPHP5.6からPHP7.2にバージョンアップしたところ、PHP7では最初のpreg_matchでマッチせず、置換がなされずテンプレートのまま出力されてしまうという事象が発生していました。

// PHP5.6

`スタート: これはほげぴよ。 :終了` と出力される

// PHP7.2
`<% tmpl_start %> これは<% hogehoge %><% piyopiyo %>。 <% tmpl_finish %>` と出力される

ステージング環境では問題なく、本番環境で突如発生した問題でした。


何がいけなかったのか

よくよく調べてみると、ステージング環境と本番環境で、テキストの中身が若干違うことに気づきました。そこで本番のテキストをステージングで入れてみたところ、再現ができ問題を発見することができました。

しかし、何が違うのか最初はまったくわかりませんでした。diffをとると**************といった文字列が加えられていたので、ワイルドカードを文字列でいくつ以上入れたらおかしくなるんだろうかとか、そんなことを考えていました。実際はそんなことはなく、単純に文字数に問題があるんだということに気づきました。


ちゃんとした原因・調査

今回はたまたま事象を特定できましたが、原因がわからないことには正しい対処をとることができません。

ということで調べたのですが、phpでは正規表現処理でエラーがあった場合にエラーコードを返してくれる preg_last_error という関数があります。

http://php.net/manual/ja/function.preg-last-error.php

ここを参考にデバッグしたところ、PREG_JIT_STACKLIMIT_ERROR6 が返されてきました。

そして 定義済定数 のページを見るとこんな文章が。

The new PREG_JIT_STACKLIMIT_ERROR constant introduced with PHP 7.0.0 has got a value of 6.

I experienced this error code when parsing a 112KB file. preg_match_all failed with this error. Interesting was: The matches array contained some entries, but not all as the command failed (I missed to check the return value).

Unfortunately you can not configure the stack-size of the PCRE JIT. The only way out was - at least for me - to disable the PCRE JIT via php.ini (pcre.jit=0).

なるほど、対象のサイズが大きすぎたんですね。そしてpcre.jit=0をphp.iniに設定すれば解消されるよ、と。

pcre.jitは PCREのjust-in-timeコンパイルを使用するかどうかのフラグの設定で、PHP7で追加されました。デフォルトは1になっています。

実行時設定

pcre.jit=0にすれば確かに解消するかもしれませんが、そもそもたくさんの文字列にマッチしたりするのはパフォーマンス的に非常によろしくない状態です。なので、同じような現象に出くわしたらpcre.jit=0にするより処理の仕方をそもそも見直すのが良いでしょう。


まとめ

正規表現は便利ですが、やはり一歩間違えるときついなーと感じます。今回の現象でデバッグ方法もわかったのはよかったですが、正規表現を使うときは気をつけないといけないですね。(というかPHP側でエラー出して処理止めてくれ)


参考