Edited at

正規表現の先読み/後読みを「絞り込み」と理解してみる

More than 3 years have passed since last update.

ScalaMatsuriでほんの少しだけ後読みの話が出たようなので先読み/後読みについて解説する。

なお、後方参照とは関係ない。


先読みって?

正規表現技術入門には「与えられた正規表現にマッチする文字列が直後に来る位置」にマッチするとある。これだけだと分かりづらいのでRubyで試してみる。

まず、以下のようなファイルを用意する。


test.txt

1aaa3

2aaa2
3aaa1
1bbb3
2bbb2
3bbb1

まず、基本的なRubyの動きから。普通に先読みなどを使わない、正規表現を使ってマッチする。

$ ruby -ne 'puts $_.scan(/aaa/)' test.txt

aaa
aaa
aaa

マッチした文字列だけを抜き出している。

これに先読みを加えてみる。先読みは(?=)で囲む。

$ ruby -ne 'puts $_.scan(/aaa(?=1)/)' test.txt

aaa

1つしかマッチしなくなる。これは「'a'、'a'、'a'」に続いて「1が直後にくる位置」がある文字列にマッチする。なので


  • aaaの直後に1が来るのは1行しかないので1つ(3aaa1の行)にしかマッチしない

  • 先読みは「位置」にマッチするので抜き出された結果には影響しない

尚、後読みは「直前にくる位置」にマッチする。(?<=)で囲む。

$ ruby -ne 'puts $_.scan(/(?<=1)aaa/)' test.txt

aaa

これは1aaa3の行にのみマッチする

aaa(?=1)を図にするとこんな感じだろうか。

   |-> 1

aaa

後読みの(?<=1)aaaはこうなる。

1 <-|

aaa


「絞り込み」と理解してみる

さて、ご覧の通り、分かりづらい。これを「絞り込み」と理解してみるとどうだろう。

/aaa(?=1)/は「aaaにマッチするもの」の中でも「直後に1が来るもの」とすればどうだろう。少しは分かりやすい気がする。

もう1つ例を出す。これで、単語集の中から7文字の単語を抜き出せる。

$ ruby -ne 'puts $_.scan(/^.{7}$/)' /usr/share/dict/words

# 14,000個くらい

この中でも、母音(aiueo)が3つ以上連続するものに絞り込むことする(英語の母音はaiueoだけじゃないだろとかは突っ込まない)。various、tableauなどがそれにあたる。まず、とある文字列に対して先頭から厳密にマッチするとして母音が3つ以上含まれるかを検査する正規表現は.*[aiueo]{3}になる。それを使って、こう書ける。

$ ruby -ne 'puts $_.scan(/^(?=.*[aiueo]{3}).{7}$/)' /usr/share/dict/words

# 130個くらい

先の図を使うとこうなる。

 |->.*[aiueo]{3}

^ .{7}$

なんとなく正規表現が重ね合わさってるのが分かるだろうか。

さらにもう1つ。シェルスクリプトの中から定義された関数名だけを抜き出してみる。

シェルスクリプトの関数定義は行頭から始まって識別子文字([a-zA-Z_])列が続き、()がくる。例えば

foo() {

echo foo
}

のように。

まずは簡単に試してみる。行頭から始まる識別子文字列を抜き出す。尚、検索対象は拙作のCIMのscriptsディレクトリのファイル群とした。シェルスクリプトで千数百行ある。

$ ruby -ne 'puts $_.scan(/^[a-zA-Z_]+/)' *

...
command
shift
case
esac
case
esac
if
else
fi
cim_set_sbcl_home_for
export
exec
BEGIN

if case commandなど明らかに関数でない、シェルのキーワードも入っている。これを「()が続く」の条件で絞り込む。

$ ruby -ne 'puts $_.scan(/^[a-zA-Z_]+(?=\(\))/)' *

...
cim_aware_system_lisp
cim_make_symlinks
cim_choose_one_version
cim_register_all_impls
cim_set_sbcl_home_for
cim_arch
cim_sbcl_arch
cim_ccl_arch
cim_distribution_archive_type
__sed_i
__which
cim_run_if_installed
run_lisp

ちゃんと関数のみが出てきたようだ。

先の図でいくと、この正規表現はこうなる。

          |->\(\)

[a-zA-Z_]+


まとめ


  • 正規表現の先読み/後読みについて解説した

  • 先読み/後読みの「絞り込み」の解釈を与えた

  • 「正規表現技術入門」読もう