正規表現で「先読み」というものについて最近知ったので自分の言葉でまとめてみようと思う。
Onigmoとは
Ruby2.0以降ではOnigmoという正規表現エンジンが使用されています。
Onigmoは鬼車のフォークバージョンであり、鬼車はRubyのバージョン1.9やバージョン5以降のPHPの正規表現エンジンとして採用されています。
?= 先読み Look-ahead
「先を読む」とはなんだろう。
「look ahead」には「先のことを考える」「将来に備える」という意味がある。
先読みの正規表現とは、A(?=B)
のようにかき、以下のようにマッチします。
p "When we look at global environmental issues, we have to look ahead to the future".scan(/look (?=ahead)/)
#=>["look "]
(英文だと単語は空白で区切られるので、look(?=ahead)
ではなくlook (?=ahead)
としています)
ここでマッチした"look "は、"look ahead"の"look "です。"look at"の方にはマッチしません。
検索したい単語を「look」とし、その先を読んで「ahead」があるものとマッチする、と考えると、「先読み(Look-ahead)」という名前も認識しやすくなるのではないでしょうか。
先読みは実際には(?=ahead)
で、"ahead"直前の位置とマッチしています。
「aheadの直前を開始地点としている」と言ってもいいかもしれません。
^
や$
の文字列指定版みたいな認識をしておくと考えやすいかもしれない。
アンカーと言うらしい。調べてみると色々あるんですね。
先ほどの例文を(?=ahead)
で検索すると、空文字がマッチします。
(マッチしないのであれば、scanの結果は[]です。matchだとnil/null)
p "When we look at global environmental issues, we have to look ahead to the future".scan(/(?=ahead)/)
#=>[""]
?<= 後読み Look-behind
後読みは先読みの逆です。
p "When we look at global environmental issues, we have to look ahead to the future".scan(/(?<=to) look/)
#=>[" look"]
ここでマッチした" look"は、"to look"の" look"です。"we look"の方にはマッチしません。
?! 否定先読み Negative Look-ahead
否定先読みは、先読みの否定です。
p "When we look at global environmental issues, we have to look ahead to the future".scan(/look (?!ahead)/)
#=>["look "]
ここでマッチした"look "は、"look at"の"look "です。"look ahead"の方にはマッチしません。
"look"の先が"aheadではないlook"とマッチしています。
?<! 否定後読み Negative Look-behind
否定後読みは、後読みの否定です。
p "When we look at global environmental issues, we have to look ahead to the future".scan(/(?<!to) look/)
#=>[" look"]
ここでマッチした" look"は、"we look"の" look"です。"to look"の方にはマッチしません。
"look"の後(?)が"toではないlook"とマッチしています。
「先読み」「後読み」という呼び方について
ここは暇人のみ読むことを推奨します。
「先を見据える」と言えば、確かに先述した通りなのですが、日本語における「先」には正直、前後どちらの意味も文脈によって存在する。
「私が先に行く」「列の先(先頭)に並ぶ」と言えば「前方」「早い方」という意味になるし、
「先を見据える」「この先は行き止まり」と言えば「後ろ」「(時間的に)未来」という意味になる。
そもそも文章における「先」「後」「前」とは?
When we look at global environmental issues
先ほどの例文において、「lookの前はwe」「lookの後はat」は大体の人が同じ感覚だと思う(と信じている)
しかし、
「lookより先に来るのは?」だと「we」と答えるし、
「lookの先にあるのは?」と聞かれたら「at」と答えかねない。
多分こればかりはニュアンスの問題だと思う。
少し助詞がかわるだけで、人によっては逆だったり、どちらも同じ答えかもしれない。
特に、「後読み」って言い回しがどう考えても先読みと同じ意味にしかならないと個人的には思うからで、「後(先)読み」「前読み」と呼びたいと思うほど。
恐らくですが、文字の出現順(並び順)を基準として、「後ろ」「先」と言っているのでしょうけど・・・。
うーん、先読み、後読みって言い方やめない?
ただ、それぞれ「後方一致指定」「前方一致指定」という言い方もある様子。
私は英語はできないが、aheadにもどっちの意味もあるようで、その一方「look ahead」だと「(時間的に)未来を見る」という意味でつかわれているように思える。
でも、「この文字のahead」と言われたら、前方を見る気がする。
正規表現としての位置基準
A(?=B)
と書いたとき、
「Aを基準として、Bがあった場合にマッチする」と考えれば、「Aの先を見据えてBがあればマッチする」という「先読み」「look ahead」的な意味になるが
「Bを基準として、その前にあるAとマッチする」と考えれば「Bの前を探してAがあればマッチする」とも捉えられる。
(?=B)
という特殊な記述部を基準としたくなるが、検索するのはAの方なので、Aを基準にした方がいいようにも思える。
ただ、否定の(?!B)
を基準とすると、Bじゃない部分全ての位置を検索しているようにも思えてしまうので、やはりAを基準とする方が正しいのかもしれない。
とはいえ、以下の検証をすると、位置を基準として検索しているのが正なんだなぁ・・・と思わざるを得ない。
p "When we look at global environmental issues, we have to look ahead to the future".scan(/(?!to)/)
#=> ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]
これは"to"の直前の2か所以外すべての位置(空集合)である。
先読みの応用
名称はともかくとして、先読みは応用が利く。
次のような例を考えてみよう。
p "When we look at global environmental issues".scan(/^.*(?=global)/)
p "When we look at global environmental issues".scan(/^(?=.*global)/)
さてこれらはどうなるだろうか。
・
・
・
1つ目が["When we look at "]
。"global"が現れるまでの文字列です。
2つ目が[""]
です。これは文章の最初の空文字です。
では逆に否定するとどうでしょうか。
p "When we look at global environmental issues,".scan(/^.*(?!global)/)
p "When we look at global environmental issues,".scan(/^(?!.*global)/)
1つ目が["When we look at global environmental issues,"]
。
"global"ではない文字までの文字列・・・なので、「","までの文字列」(=全部)とマッチします。
(これは使い道がなさそう)
2つ目が[]
。行の開始以降にglobalを含まない文字列なので、マッチしません。
ではこれらは一体何を意味するのでしょう。
そう、「文章中に"global"を含むかどうか」 です。
特定の文字列を含む(含まない)行自体をマッチさせたい場合は、以下のようにします。
# "global"を含む行を抽出( /^.*(?=global).*$/ でも可)
p "When we look at global environmental issues,".scan(/^(?=.*global).*$/)
#=> ["When we look at global environmental issues"]
p "we have to look ahead to the future.".scan(/^(?=.*global).*$/)
#=> []
# "global"を含まない行を抽出( /^.*(?!global).*$/ は不可)
p "When we look at global environmental issues,".scan(/^(?!.*global).*$/)
#=> []
p "we have to look ahead to the future.".scan(/^(?!.*global).*$/)
#=> ["we have to look ahead to the future."]
・・・
同様に後読みで書いてみると・・・
p "When we look at global environmental issues,".scan(/^.*(?<=global.*)$/)
#=> invalid pattern in look-behind
これはダメみたいなので、次のように書く。
p "When we look at global environmental issues,".scan(/^.*(?<=global).*$/)
#=> ["When we look at global environmental issues,"]
p "we have to look ahead to the future.".scan(/^.*(?<=global).*$/)
#=> []
後読みは本体がマッチした後で文字どおり遡ってチェックされるはずなので、量指定子の長さが不定だと効率が非常に落ちることは想像がつきます。
仕様までは確認していませんが、そのような理由でサポートされていないのでしょう。
「後読みは長さを不定にできない」ということで認識しておきます。
?~ 非包含演算子
これはおまけですが、非包含演算子というものについて。
文字通り「含まない」を見る正規表現です。
p "When we look at global environmental issues,".scan(/(?~global)/)
#=> ["When we look at globa", "l environmental issues,", ""]
p "we have to look ahead to the future.".scan(/(?~global)/)
#=> ["we have to look ahead to the future.", ""]
見ての通り「globa」は、「globalを含まない文字列」になることに注意します。
(2つ目で空文字がマッチしている理由はなんだろう?)
非包含演算子を単独で使わない方がいいことは、こちらに書いてあります。
prefixやsuffixと組み合わせて使うのがよいでしょう。
p "When we look at global environmental issues,".scan(/we(?~look)/)
#=> ["we loo"]
p "we have to look ahead to the future.".scan(/we(?~look)/)
#=> ["we have to loo"]
ただ、例のように「ある文字列を含まないかどうか」というには違うようです。
文字列で挟んで「途中に特定の文字列を含まない文字列」をチェックする方がよさそう。
p "When we look at global environmental issues,".scan(/we(?~look)at/)
#=> []
p "we have to look ahead to the future.".scan(/we(?~look)have/)
#=> ["we have"]
結論、「特定の文字列を含まない行」をチェックするなら以下が一番簡潔?
p "When we look at global environmental issues,".scan(/^(?~ahead)$/)
#=> ["When we look at global environmental issues,"]
「特定の文字列を含む行」もこれくらい簡潔に書けないものかな。