Regex Notes

基本動作

  • Regex Engine
  • Regexデータ
  • 文字列データ

この三つがメインでEngineがRegexデータと文字列データをひとつずつチェックしていく。マッチというのはRegex全体で表現されるパターンが全体でマッチしている場合のことをいう。(ただ各Regexがマッチしている場合もマッチともいう)。わかりづらいので各Regexがマッチしていることは成功といいダメな時は失敗と呼ぶ。

以下はもっとも基本なマッチ。

Regex基本原則

まずはRegexの基本原則を見てみる。しかし、これらの基本原則はオプションによって変化して、場合ごとに考え方を変えないといけないところにRegexの難しがあると思う。ただ、基本原則をしっかり理解しておけば、変化系にも対応しやすくなる。

左から一文字ずつマッチしていく

1
2
/ab/
ab
  1. Regex: a 文字列: "a" => 成功
  2. Regex: b 文字列: "b" => 成功
  3. 終了
  4. 結果: "ab"にマッチした。

Regex EngineはRegexデータ、文字列ともに現在のステップのポジションを覚えていて、ステップ毎にそれらのポジションをひとつずつずらしていく。ポジションは左から右に動いていく。

マッチした時点で終了

マッチが達成されたら、それ以降の文字列は無視する。これを貪欲でないマッチとか一般的には呼ぶが自分にはどうもわかりずらい。代わりにせっかちマッチと覚えている。せっかちなので一度マッチした時点でマッチしたよ!と言って仕事を終わってしまう。

1
2
/ab/
abab
  1. Regex: a 文字列: "a" => 成功
  2. Regex: b 文字列: "b" => 成功
  3. 主なEngineではデフォルトではLazy、つまりマッチが成功した時点で終わる
  4. 終了
  5. 結果: 最初の"ab"だけにマッチした。

マッチが成功するために全ての可能性を試す

Regexにとって一番大事な仕事はマッチする文字列を探すこと。それを達成するためにRegexは全ての可能性を試す。

1
2
/ab/
acab
  1. Regex: a 文字列: "a" => 成功
  2. Regex: b 文字列: "c" => 失敗
  3. 失敗したのでRegexの最初に戻ってマッチさせようとする。これをBacktrackと呼ぶ。Backtrack == 巻き戻し と考えればいい
  4. Regex: a 文字列: "a" => 成功
  5. Regex: b 文字列: "b" => 成功
  6. 終了
  7. 結果: "ab"にマッチした。

よく使われる応用Regex

ここからは実際によく使うRegexを紹介。中には基本原則で紹介した動作を変えるものもあるので、場合ごとに動作がどう変わるかを把握しないといけない。

g を使うと一度マッチしても続けてマッチさせようとする(貪欲マッチ)

Regexの最後に g をつけると一度マッチしても終了せず続けてマッチさせようとする。gを修飾子と呼びこの動作を貪欲マッチと呼ぶ。せっかちマッチの場合と同じでわかりにくいので熱心マッチと覚える。これは基本動作のせっかちマッチを変える。

1
2
/ab/g
abab
  1. Regex: a 文字列: "a" => 成功
  2. Regex: b 文字列: "b" => 成功
  3. 熱心モードなので文字列がある残っている以上つづける。
  4. Regex: a 文字列: "a" => 成功
  5. Regex: b 文字列: "b" => 成功
  6. 終了
  7. 結果: "abab"にマッチした。

文字だけじゃなくその位置を含めて成功/失敗を判定(アンカー)

今まで見てきたRegexは同じ文字かどうかを判定するものだった。例えば、/ab/はaのあとにbが続く"ab"にマッチする。言い換えれば、aのあとにbが続く限りそれがどこにあろうがマッチする。だから"1ab2はこの場合マッチする。

^$がRegexの前につくと文字のマッチだけじゃなくその文字がある位置まで見る。こうゆう種類のRegexをアンカーと呼ぶ。

1
2
/^a/
1a
  1. Regex: ^a 文字列: "1" => 失敗
  2. BacktrackでRegexが最初に戻る。
  3. Regex: ^a 文字列: "a" => 失敗
  4. "a"にはマッチしているが ^aは一番最初にある"a"なので失敗。
  5. 終了
  6. 結果: マッチなし

改行直後の文字は最初の文字列ではない

^は文字列の最初の位置にマッチするが、改行直後の文字列は最初の文字とはならない。この動作が主なRegexエンジンのデフォルト。

1
2
3
4
5
6
/^a/

# 複数行に渡る文字列
1a
a

  1. Regex: ^a 文字列: "1" => 失敗
  2. BacktrackでRegexが最初に戻る。
  3. Regex: ^a 文字列: "a" => 失敗
  4. BacktrackでRegexが最初に戻る。
  5. Regex: ^a 文字列: "a" => 失敗
  6. ^aはあくまで文字列全体の最初にある"a"。二つ目の"a"は行の最初のaだが文字列全体の最初ではないのでマッチしない。
  7. 終了
  8. 結果: マッチなし

mを使うと改行が入っていても最初の文字とみなす

上で例だと改行直後、二つ目の"a"/^a/にはマッチしなかった。これをマッチさせるにはm修飾子を使う。すると、改行直後の文字でも文字列の最初とみなされる。

1
2
3
4
5
/^a/m

# 複数行に渡る文字列
1a
a
  1. Regex: ^a 文字列: "1" => 失敗
  2. BacktrackでRegexが最初に戻る。
  3. Regex: ^a 文字列: "a" => 成功
  4. m修飾子があるので改行直後の"a"は文字列の最初とみなされる。
  5. 終了
  6. 結果: 二つ目のaにマッチ

mを使った時の動作をmulti-lineモードと呼ぶ。

(ほぼ)どんな文字にもマッチする . (ドット)

どんな文字にでもマッチするRegexに.がある。

1
2
/.../
1&!
  1. Regex: . 文字列: "1" => 成功
  2. Regex: . 文字列: "&" => 成功
  3. Regex: . 文字列: "!" => 成功
  4. 終了
  5. 結果: 1&!にマッチ

しかし.にも例外はあり改行にはマッチしない。

1
2
3
4
/.../
# 複数行に渡る文字列
1
2
  1. Regex: . 文字列: "1" => 成功
  2. Regex: . 文字列: \n => 失敗
  3. Backtrackで最初の .に戻る
  4. Regex: . 文字列: "2" => 成功
  5. Regex: . 文字列: \n => 失敗
  6. 終了
  7. 結果: マッチなし

s を使うと . (ドット)が改行にもマッチする

s修飾子を使うと.が改行にもマッチするようになる。

1
2
3
4
/.../s
# 複数行に渡る文字列
1
2
  1. Regex: . 文字列: "1" => 成功
  2. Regex: . 文字列: \n => 成功
  3. Regex: . 文字列: "2" => 成功
  4. 終了
  5. 結果: 1\n2にマッチ

sを使った時の動作をsingle-lineモードと呼ぶ。注意するのは single-modeとmulti-lineモードは全く別物だということ。single-lineモードは.が改行も含むようにすることに対して、multi-lineモードはアンカー (^&)が改行を考慮するするようにすることなので、single-lineとmulti-lineは相反するモードではない。(両方指定可能)

パターンを繰り返す

+*を使うとその直前のRegexを繰り返してマッチするようにできる。+は直前のRegexの一回以上の繰り返し。

1
2
/a+/
aaa
  1. Regex: a+ 文字列: "a" => 成功
  2. Regex: a+ 文字列: "a" => 成功
  3. Regex: a+ 文字列: "a" => 成功
  4. 終了
  5. 結果: "aaa"にマッチ

*は直前のRegexの0回以上に繰り返し。つまりマッチする文字がなくても成功になる。

1
2
/a*b/
b
  1. Regex: a* 文字列: "b" => 成功
  2. *は0回以上のaの繰り返せば成功、つまりaがなくても成功とみなす。
  3. Regex: b 文字列: "b" => 成功
  4. 終了
  5. 結果: "b"にマッチ

Regexの最後に行く前に文字が終わってしまったら文字を戻す

.+*を組み合わせると任意の文字の無限の繰り返しを指定できるので文字列のほうが先に終わってしまうことがある。Regexが失敗したときはBacktrackして次の文字からまたやり直したが繰り返しを使うと文字の方を巻き戻す。普通のBacktrackと区別するために文字列Backtrackと呼ぶ。Regexがこうゆう動きをするのは、最初に書いたマッチが成功するために全ての可能性を試すという基本原則があるから。

1
2
/a.+b/
aab
  1. Regex: a 文字列: "a" => 成功
  2. Regex: .+ 文字列: "a" => 成功
  3. .+は任意の文字の繰り返しなので次のRegexには進まない。
  4. Regex: .+ 文字列: "b" => 成功
  5. この時点で文字列が終わってしまったが、Regex側はbがまだ残っている。bまで成功しないとマッチではない。
  6. 文字列をマッチするとわかっているところまでBacktrackする(この場合最初の"aa"/a.+/でマッチするとわかっているのでここまで戻す。)
  7. Regex: b 文字列: "b"=> 成功
  8. 終了
  9. 結果: "aab"にマッチ

もし ~ ならマッチを試みる

ここまで紹介したRegexではプログラミングでつかうIF ~ then ~ ENDのようなことはできない。それをできるようにするのがLookahead。重要なポイントはLookaheadは条件をしていしるだけなのでそれ自体はマッチの結果には含まれない。

もしXにYがつづけばマッチ (Positive Lookahead)

1
2
/a(?=b)/
abd
  1. Regex: a 文字列: "a" => 成功
  2. Regex: (?=b)bの部分 文字列: "b" => 成功
  3. Regex: (?=b)?=の部分 上のステップでbがマッチしたら成功 => 成功
  4. 終了
  5. 結果: "a"にマッチ。bはあくまで条件なのでb自体にはマッチしない

この例ではあまり意味があるように思えないがLookaheadはXが続かないYを探す時に真価を発揮する。

もしXにYがつづかなければマッチ (Negative Lookahead)

1
2
q(?!u)
aq
  1. Regex: q 文字列: "a" => 失敗
  2. Backtrack
  3. Regex: q 文字列: "q" => 成功
  4. Regex: (?!=u)uの部分 文字列: "u" => 失敗
  5. Regex: (?!u)?!の部分 上のステップでuがマッチしていないから成功 => 成功
  6. 終了
  7. 結果: "q"にマッチ

Comments

« CollageとAnimationを使ってElmでアプリを作る