正規表現の先読みについて解説してみる

先読み正規表現について、この前やっと理解できた。Rubyリファレンスマニュアルではこんな説明になっている。

(?= )

  先読み(lookahead)。パターンによる位置指定(幅を持たない)

  (?=re1)re2

  という表現は、re1 と re2 両方にマッチするものにマッチする正規表現です。

  re1(?=re2)

  という表現は、後に re2 とマッチする文字列が続く、正規表現 re1 です。

  p /foo(?=bar)/ =~ "foobar"      # => 0
  p $&    # => "foo"   (bar の部分の情報はない)

(?! )

  否定先読み(negative lookahead)。パターンの否定による位置指定(幅を持たない)

  (?!re1)re2

  という表現は、re1 にマッチしないが re2 にはマッチする正規表現です。

  # 000 を除く 3 桁の数字
  re = /(?!000)\d\d\d/
  p re =~ "000"   # => nil
  p re =~ "012"   # => 0
  p re =~ "123"   # => 0

  # C 言語の識別子 ([A-Za-z_] で始まり、[0-9A-Za-z_] が続く文字列)
  /\b(?![0-9])\w+\b/

あんまり出てこないし、俺もこれまでほとんど使ったことがないのでこの説明だと理解できなかった。なぜ「(?=re1)re2」が「re1とre2にマッチする正規表現」なのか、なぜ「re1(?=re2)」が「後にre2とマッチする文字列が続くre1」なのかが理解できなかった。「幅を持たない」というのもよくわからなかった。だから、これらを表現したいとき、どっちが先だったけと悩んだものだった。

ruby-list:45087から始まるスレッドruby-list:45092から始まるスレッドの質疑応答を見てやっと具体的なイメージができたのだ。

これでやっと先読みについて説明ができる。

先読みというのは、先読み始点から先読み正規表現をマッチさせて、先読み正規表現にマッチするならば先読み以降の正規表現にマッチさせていくものだ。幅を持たないというのは、先読み正規表現のマッチは「元々の正規表現にマッチした文字列」には含まれないということ。あくまで先読みではない正規表現が結果になる。例えてみれば、先読みではない正規表現がmasterで先読み正規表現がslaveになるだろう。

で、具体例を。以下は先読みが含まれないふつーの正規表現だ。正規表現を知っているならば何ら問題はない。

s = "foobar1"
s.match(/foob/)       # => #<MatchData "foob">
s.match(/foo.+1/)     # => #<MatchData "foobar1">

以下は先読みが含まれる正規表現

s.match(/foo(?=.+1)b/)  # => #<MatchData "foob">
s.match(/foo(?=.+2)b/)  # => nil

まず、/foo/までマッチさせる。その次に先読み /(?=先読み正規表現)/ が来ている。先に続く文字列は「bar1」なので、先読み正規表現の/.+1/にマッチする。しかし、それは先読み正規表現なので「1」の後ろ(文字列の末尾)にまでマッチ位置を移動しない。「1」の後ろにまで動かしてから先読み正規表現の直前(「o」の後ろ)にまでマッチ位置を戻すと考えてもよかろう。現在位置はoの後ろなので次にbが来るから/b/にマッチする。よって /foo(?=.+1)b/ にマッチするのだ。先読み正規表現にマッチしないから/foo(?=.+2)b/にはマッチしない。

このように基本的な考え方がわかってしまえば、/foo(?=.+1)/は「/foo/の後に/.+1/とマッチする文字列が続く」し、/(?=.+1)b/は「/.+1/と/b/にマッチする」というリファレンスマニュアルの説明にも合点がいくようになる。

否定先読みの説明も簡単だ。

ruby-list:45087の例「1〜7桁からなる数字にマッチさせたいが、0000000にはマッチさせたくない正規表現」を考えてみる。
「1〜7桁からなる数字にマッチする正規表現」は /\A\d{1,7}\z/ とあっさり書ける。/\d/は/[0-9]/の短縮形。「0000000にマッチする正規表現」も /\A0{7}\z/ だ。/{N}/は直前の表現がN回続くという意味。べつに/\A0000000\z/でもいいんだけど長いので(ry
Ruby正規表現 /\A〜\z/ は文字列の先頭から末尾までのマッチを意味する。他言語の悪いクセで「^〜$」と書くと、改行の後にもマッチしてしまうので注意しよう!!落とし穴。

ここで否定先読みの出番だ。「0000000にマッチしないが、1〜7桁からなる数字にマッチさせる正規表現」を書くには、「0000000にマッチしない正規表現」を否定先読みで書き、1〜7桁からなる数字にマッチする正規表現」を普通の正規表現で書けばよい。よって /\A(?!0{7})\d{1,7}\z/ となる。

re = /\A(?!0{7})\d{1,7}\z/
"12332" =~ re                   # => 0
"0000000" =~ re                 # => nil

まあ先読み正規表現は慣れるまで知らなくてもいいんだけどね。
実際のプログラミングでも

if input =~ /\A\d{1,7}\z/ and input !~ /\A0{7}\z/
  # 
end

if input =~ /\A(?!0{7})\d{1,7}\z/
  # 
end

になるだけなので。