正規表現を使って、ある文字列にいくつかの特定の文字列が含まれているかを調べる
http://blogs.wankuma.com/trapemiya/archive/2009/04/18/171635.aspx
にコメントをいただいて、正規表現の先読みに関して理解が足りていないと痛感しましたので、自分なりに調査してまとめてみました。間違ってるというご指摘があれば是非コメントを下さい。よろしくお願いします。
「謎は全て解けた!」と、金田一少年のように言い切れればいいんですけど・・・ 。でも自分ではほぼ満足いくレベルに達したんで公開してみました。添削してもらいたいということもありますがw
では、解説を始めます。
以下の検査対象の文字列があるとします。
abuser unknown? bbbbb unknown user1aaaaaa No such user here a
この時、「user unknown」、「unknown user」、「No such user here」の3つの文字列全てを含むかどうかを検査する正規表現は以下のように書けます。(ただしベストな書き方ではありません。より良い書き方は後述します。)
(?=.*user unknown)(?=.*unknown user)(?=.*No such user here)
ここで(?=)は先読みと呼ばれるものです。いきなりですが先読みという訳は誤解を招きやすく、訳としては適当ではないと思います。英語ではlookaheadやlook-aheadやlook aheadと書かれます。直訳すれば「先を見ろ」ということになります。つまり先は前後の前という意味の先です。先読みとは前方を読みなさいということです。順序的に先に読むということではありません。
(#日本語訳だとわかりにくい場合があるので、その場合には英語の原文で確認されることをお勧めします。これは一般的に言えることです。)
さて、意味がわかったところで先読みの動作を見てみましょう。先読みは、マッチした部分があるとその直前にポインタを置きます。例えば、abcdefという文字列に対する(?=cd)はcdの部分にマッチし、ポインタはbとcの間に置かれます。従って、bcdeにマッチさせるにはb(?=cd)cdeとなります。このように先読みはマッチした部分をマッチの対象として記憶しません。これをキャプチャしないとも言います。言いかればキャプチャした文字列長が0ということであり、幅0とも言われます。
上記のことは次のように言うこともできます。
abcdefという文字列に対してcdという正規表現があった場合、abcdefのcdの部分にマッチし、次の検査開始のポインタはそのマッチした部分の直後に置かれます。したがって例えばbcdeにマッチさせる正規表現はb(cd)eになります。もちろんbcdeと書いてもかまいません。一方、先読みは幅0ですので、このポインタが移動しないということなのです。したがって前述した通り、先読みでは同じ正規表現を b(?=cd)cdeと書く必要があります。lookaheadですので、その場に留まって前方を見ろということなのですしょう。
ところで、最初に書いたように先読みを連続で書いた場合はどうなるでしょうか? この場合は、全ての先読みについて、それぞれが一致した位置が全て合致している場合にマッチしたということになります。
つまり、
(?=.*user unknown)(?=.*unknown user)(?=.*No such user here)
に関して言えば、3つの先読みがそれぞれ一致した位置の全てが合致している場合にマッチしたということになります。
上のことは論理積ですので、3つの先読みをどのような順序で記述しても良いということになります。
ではここで問題です。
以下の検査対象の文字列があるとします。
abcdefg
(?=.*cd)(?=bc)はマッチするでしょうか?
(?=.*cd)(?=de)はマッチするでしょうか?
答えは(?=.*cd)(?=bc)はマッチしますが、(?=.*cd)(?=de)はマッチしません。
なぜなら(?=.*cd)が一致する位置は先頭からcdの直前までですから、先頭、aとbの間、bとcの間の三ヵ所あります。(?=bc)はaとbの間のみ、(?=de)はcとdの間のみになります。
したがって(?=.*cd)(?=bc)はaとbの間が合致しますからマッチしますが、(?=.*cd)(?=de)は合致する位置がないためマッチしないということになります。
同様に考えると、
(?=.*user unknown)(?=.*unknown user)(?=.*No such user here)
も比較する位置がたくさんあって効率悪そうです。上記の3つの先読みはいずれも.*を含むため、先読みが一致すれば一致する位置に必ず先頭が含まれるということです。そこで先頭の位置だけ比較すれば済むように、マッチ条件に先頭の位置を追加しましょう。先頭の位置は^で表します。位置情報なので^も幅0です。
^
(?=.*user unknown)
(?=.*unknown user)
(?=.*No such user here)
の4つの位置の論理積になりますから、
^(?=.*user unknown)(?=.*unknown user)(?=.*No such user here)
のように書くことができます。4つの論理積なので、ちょっとびっくりされるかもしれませんが、
(?=.*user unknown)^(?=.*unknown user)(?=.*No such user here)
や
(?=.*user unknown)(?=.*unknown user)(?=.*No such user here)^
でもいいんです。
これで4つの先頭の位置が合致したらマッチしたと条件を絞ることができます。
ちなみに(?=.*user unknown)の合致する位置は複数あり得ますが、その場合でもポインタは先頭に置かれます。したがって、以下のような先読みである
(?=.*user unknown).*
にマッチした場合、マッチする文字列は検索対象文字列全体になります。
さて、次に先読みについてもう少し解説します。論理積なので、そのうちの一つである(?=.*user unknown)の動作について説明すれば十分でしょう。なぜなら他の2つもこれと同様の動作になるだけだからです。
では、以下のように先読みが含まれる場合のキャプチャされる文字列について考えてみます。
^.*(?=.*user unknown)
検索対象は既出と同じく
abuser unknown? bbbbb unknown user1aaaaaa No such user here a
です。
先読みで.*user unknownがマッチするところを調べます。とりあえず一致するところが見つかりました。abuser unknownの部分ですね。次に.*について調べます。.*はマッチする最長に一致しようとします。そうすると、先読みの条件を満たす.*の最長はabになります。(?=.*user unknown)は幅0なので、結局この正規表現でキャプチャされたフレーズはab + 幅0 = abということになります。
先読みはバックトラックしないため、つまり複数マッチする部分を見つけようとしないため、正規表現にマッチした部分は一箇所であり、グループ化もされていないのでキャプチャされた部分も一箇所になりますが、通常の正規表現ですとマッチする部分が複数、それぞれのマッチした部分でのキャプチャも複数ということもあり得ます。
上記を理解されれば、
^(?=.*unknown user)や
(?=.*user unknown)や
^
のキャプチャが空白になることは明らかでしょう。^も(?=.*unknown user)などの位置情報は幅0だからです。
^.(?=.*user unknown)
のキャプチャがaになるのも明白ですね。
では、
^.*(?=.*user unknown).
はどうなるでしょうか?(判りにくいですが文末にピリオドがあります)
ab + 幅0 までは良いでしょう。この時、abuser unknownにマッチしていますが、先読み部分はポインタが動きませんから、ポインタはabの後にあることになります。
したがって、.はuになり、abuがキャプチャされることになります。ちなみにマッチしている部分はabuser unknown? bbbbb unknown userです。
^.*(?=.*unknown XXXuser).
としたら、userの前にXXXが付いているため先読み部分がマッチしませんから、正規表現全体としてもマッチしません。したがって何もキャプチャされません。
^.*(?=.*user unknown).*
はもう簡単ですね。検索対象全てになります。ab以下.*ですから、一致する最長の部分となり、すなわちキャプチャは検索対象全てということになります。ちなみに
^(?=.*user unknown).*や
^(?=.*user unknown).*$も
同様の理由で、キャプチャは検索対象全てになります。
以上のように、マッチするかどうかと何がキャプチャされるかとは別物なのでご注意下さい。すなわち、最初に書いた3つの検索フレーズ全てが含まれるかどうかは以下のように書いてもOKです。 ただし冗長になります。
^.*(?=.*user unknown)(?=.*unknown user)(?=.*No such user here).*$
や
^(?=.*user unknown)(?=.*unknown user)(?=.*No such user here).*$
問題とすべきは、3つの先読みがそれぞれマッチした位置が3つとも合致するかどうかなのです。そういう意味で後ろにある.*や.*$は何の意味もありません。
上記の動作は、Regexクラスから得られるMatchオブジェクトやCaptureコレクションなどで調査しました。
(参考文献) 順不同
▼正規表現の問題集1(基本編)
http://codezine.jp/article/detail/1573
▼■[ruby]正規表現の先読みについて解説してみる
http://d.hatena.ne.jp/rubikitch/20080622/1214080482
▼標準添付ライブラリ紹介 【第 12 回】 正規表現 (1)
http://jp.rubyist.net/magazine/?0019-BundledLibraries
▼正規表現
http://lukewarm.s101.xrea.com/RegEx.html
▼正規表現(先読み)
http://blatoma.blog10.fc2.com/blog-entry-7.html
▼先読み(lookahead)
http://www4.ocn.ne.jp/~kaerume/k2e/regex_3.html