正規表現の先読みを解説
正規表現の先読みを最近になって理解したので解説してみる。
正規表現の先読み
正規表現の先読みとは、簡単に言うと「それがあることを保証するがマッチはさせない文字列」である。
この、「あることを保証してマッチさせない文字列regexp」はRubyでは「(?=regexp)」と書く。
まずは先読みがない例を挙げる。
str = "hoge1huga2" str.match(/hoge.+2/) # => #<MatchData "hoge1huga2"> str.match(/hoge.+3/) # => nil
これは問題ないと思う。次に先読みがある例だ。
str = "hoge1huga2" str.match(/hoge(?=.+2)1/) # => #<MatchData "hoge1"> str.match(/hoge(?=.+3)1/) # => nil
この1つ目のmatchでは、「.+2」が「あることを保証してマッチさせない文字列」となる。
確かに、文字列strの後ろには「1huga2」という「.+2」にマッチする文字列が存在するが、MatchDataにはそれが反映されていない。
以下のように2段階に分けて考えると、もっとわかりやすいかもしれない。
- (?=regexp) の後ろを除去した「hoge(?=.+2)」にマッチするかをまず調べる(これはつまり「hoge.+2」とマッチすることを確認する)
- 上がマッチされることを確認したら (?=regexp) を除去した hoge1 と実際にマッチさせる(上がマッチしなければnilを返す)
いっそのことrubyのプログラムで書いてしまうとこんな感じ。
(str =~ /hoge.+2/) ? str.match(/hoge1/) : nil # => #<MatchData "hoge1">
これだと、先読み例2つめのmatch(/hoge(?=.+3)1/)では、「hoge.+3」とマッチする文字列がstrに存在しないため結果がnilというのもわかるだろう。先ほどのようにプログラムで書いてもnilが返ってくる。
(str =~ /hoge.+3/) ? str.match(/hoge1/) : nil # => nil
否定先読み
さて、実際にはこちらの方が使用頻度が多いかもしれない。
上と同じ言い方をすると、否定先読みは「それがないことを保証する文字列」である。
否定先読みはRubyで「(?!regexp)」と書く。
先ほどと同じ例を先読みではなく否定先読みで実行してみる。
str.match(/hoge(?!.+2)1/) # => nil str.match(/hoge(?!.+3)1/) # => #<MatchData "hoge1">
先読みが理解できていれば、結果が当然逆になることもわかるだろう。2段階に分けた考え方はこのようになる。
- (?=regexp) の後ろを除去した「hoge(?=.+3)」にマッチしないかをまず調べる(これはつまり「hoge.+3」とマッチしないことを確認する)
- 上がマッチしないことを確認したら (?=regexp) を除去した hoge1 と実際にマッチさせる(上がマッチしたらnilを返す)
先読みの逆である。三項演算子を使用したプログラムの例についても、str.match(/hoge1/) と nil を入れ替えることになる。
使用例
例えば、以下のように記号で囲まれた文字列を取得したい場合。
:hoge: :hoge:huga:piyo:
文字列が1つなら「:」で挟み、2つ以上ならその後ろに「文字列:」を書き足していくというものである。
これは2つ以上の場合「/:[^:]+:/」では取得できない。
s1 = ":hoge:" s2 = ":hoge:huga:piyo:" s1.scan(/:[^:]+:/) # => [":hoge:"] s2.scan(/:[^:]+:/) # => [":hoge:", ":piyo:"]
なぜなら、s2の「:hoge:」とマッチが終わった時点での文字列が「huga:piyo:」のように、先頭の「:」が欠けてしまっているからだ。
s2.scan(/:([^:]+)/).flatten # => ["hoge", "huga", "piyo"] s2.scan(/([^:]+):/).flatten # => ["hoge", "huga", "piyo"]
のようにしてもいいが、これは先頭や末尾に別の文字列があるときに上手くいかない。
そこで先読みを使ってみる。先読みを使って書くとこのように書ける。
s2.scan(/:(?=.+:)([^:]+)/).flatten # => ["hoge", "huga", "piyo"]
後ろ「:」があることを保証して「:[^:]+」とマッチさせている。こうすれば、末尾に別の文字列があっても「:」が入っていなければマッチしない。