Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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側でエラー出して処理止めてくれ)

参考

kurashicom
「北欧、暮らしの道具店」を運営するクラシコムのエンジニアチーム。
https://hokuohkurashi.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした