はじめての正規表現とベストプラクティス#9: `.*`や`.+`がバックトラックで不利な理由

主にRubyを中心としながらも、なるべく一般的な形で正規表現を解説しています。誤りやお気づきの点がありましたら@hachi8833までどうぞ🙇。

.*のバックトラックを視覚的に理解する

百聞は一見にしかずということで、.*のバックトラックを見てみることにしましょう。ひらがなの「いろはに…ひもせすん」に対して/.*ろ/という正規表現を適用したときのバックトラックの様子をregexp101.comの正規表現デバッグ機能で表示しました。最初のものに限り、regexp101を開かなくてもアニメーションGIFで見られるようにしてあります。

  • 例: /.*ろ/というパターン(regex101

/.*ろ/というシンプルなパターンなのに、マッチが完了するまでのステップが大きいことにお気づきかと思います。ステップ数は画像にもあるとおり「50」です。ここに表れている動作が正規表現のバックトラックです。

バックトラックとは

バックトラックを説明するために、本連載記事で説明したことの中から思い出していただきたいことが2つあります。

以前「参考: |正規表現エンジンのタイプによって挙動が変わる」で、正規表現エンジンに「テキスト制御型」と「正規表現制御型」の2種類があるということを簡単に説明しました。

バックトラック(backtrack)は、後者である「正規表現制御型」エンジンの中核をなす動作です。前者の「テキスト制御型」エンジンではバックトラックは行われません。

なお、後方参照(back reference)は言葉は似ていますが、まったく別の概念です。後方参照については今後説明します。

もうひとつ思い出していただきたいのが、「正規表現はじめの五歩: .」で説明した「+(および*)量指定子はデフォルトで最長一致する」という性質です。

この最長一致(別名: 欲張り一致)という動作は、バックトラックによって実現されます。

上の例を再録します。探索がどのように進むかじっくりご覧ください。リンク先ならマウスやキーボードで1ステップずつ実行できますので、より実感しやすいと思います。

  • 例: /.*ろ/というパターン(regex101

このバックトラックの動作を順に説明します。

  • .+というパターンがあると、対象文字列のその地点から末尾まで一気に進む
  • .+の直後にという文字があるので、対象文字列の末尾から1文字ずつさかのぼる
  • 対象文字列の中にという文字があれば、対象文字列の.*を開始した地点からまでをマッチ文字とする

こうすることで、「いろはに…ひもせすん」の中にという文字が複数あったとしても、末尾に最も近いまでマッチします。つまり最長一致します。

.+.*は、使う前に考えよう

私は、本連載記事を通して.+.*といった正規表現をなるべく避けていますが、その理由のひとつがこれです。つまり無駄なバックトラックにつながる可能性があるからです。常にではありませんが。

.+.*が単独だったり、正規表現の末尾に置かれている分にはさほど効率は落ちませんが、.*ろなどのように.+.*の後ろにさらに正規表現が続くと、長い文字列を対象とする正規表現でパフォーマンスが落ちることがあります。

上の例は、バックトラックで効率が落ちるところがわかりやすいよう、極端な正規表現と対象文字列を用いたエッジケースであることにご注意ください。

もちろん.+.*を使うしかないこともあると思いますので、私のように毛嫌いしなくてもよいと思います。それでも、.+.*のような正規表現を使う前に、このことをたまに思い出していただければ幸いです。

余分なバックトラックを回避するには

以下はほんの一例であり、他にもいろいろ方法が考えられると思います。.+.*最後の手段としてとっておき、原則として避けるのが楽だと思います。

1. .*?で最短一致にする

ここまでの動作を見れば、.*?のように量指定子に最短一致(別名: ものぐさ一致)の?を追加して最長一致をやめれば、バックトラックを抑制できそうです。やってみましょう。

成功です。50ステップが3ステップにまで激減しました❤️。

2. .をもっと絞り込む

+*といった量指定子ばかりではなく、.という任意の一文字にも改善の余地があります。あまり実用的な例ではありませんが、.を以下のように[^ろ]に変えてみました。

これも成功です。やはり50ステップが3ステップにまで激減しました❤️。

1. 先読み

うすうす予想はついていましたが、先読みを使ったらどうなるかをやってみました。

やはり効きませんでした。ステップ数は50のままです👎。

まとめ

  • バックトラックが余分に発生すると正規表現のパフォーマンスが落ちることがある
  • 正規表現の途中に.+.*を使う前に検討しよう
  • 最小一致の?はバックトラックの抑制に効果的な場合がある
  • .を絞り込んで、より詳細な別のパターンにすることでバックトラックを抑制できることがある
  • regex101.comの正規表現デバッグは便利

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

夏のTechRachoフェア2019

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ