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
という関数があります。
ここを参考にデバッグしたところ、PREG_JIT_STACKLIMIT_ERROR
の 6
が返されてきました。
そして 定義済定数 のページを見るとこんな文章が。
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側でエラー出して処理止めてくれ)