Skip to the content.

05. 扱う正規表現を決める

室内の工作机で材料を選び、道具箱に詰めるAliceとBob

01 章〜04 章 では、a(a|b)*bb という 1 つの例で NFA から DFA までの手順を見た。当然、次に気になるのは「他のパターンでも同じようにできるのか」である。

できる。しかも人間が図を描かなくても、機械が同じ手順を踏むだけでよい。 ここから先は、それを実際に動くスクリプトとして作る。

扱う範囲

01 章〜04 章 で相手にしたのは、次の 3 つだけで 組み立てられる正規表現だった。

演算 意味 書き方 一致する文字列
連接 並べる abb abb
選択 \| どちらか a\|b a、b
繰り返し * 0 回以上 ab* a、ab、abb、…

これに、優先順位を変える括弧 ( ) が付く((a|b)* のように)。 括弧は演算ではなく、どこまでが 1 つのまとまりかを示す記法である。

この 3 つは、実は理論上の正規表現の定義に出てくる演算そのものである。 普段使う記法の多くは、これに書き換えられる略記に過ぎない。

よく使う記法 この 3 つで書くと
a+(1 回以上) aa*
[abc](どれか 1 文字) (a\|b\|c)
a{2,3}(2〜3 回) aa\|aaa

もう 1 つ、? を足す

上の 3 つの組み合わせでは書けないものが 1 つある。a?(0 回か 1 回)である。 これは「a または何も無い」、つまり空文字列 ε との選択 a|ε にあたるが、 ε を書き表す記法をここでは用意していない。そこで a? の方を足す。 02 章 で「⑤ として足せる」と触れたのが、これにあたる。

演算 意味 書き方 一致する文字列
省略 ? 0 回か 1 回 ab?c ac、abc

02 章 で見た 4 つの規則に、5 つめとして 「本体を作り、その入口から出口へ ε を 1 本引く」を加えるだけで済む。

ab?c の機械と、? が用意する 2 本の道

b の矢印を通れば abc、ε の矢印で飛ばせば ac になる。 どちらの道を通っても 2 に着くので、その先は同じである。 繰り返し * が ε を 4 本引くのに対し、? は「飛ばす」1 本だけ—— 戻る道が無いので、本体を通るのは多くても 1 回になる。詳しくは 06 章 で見る。

そしてそれ以外は何も変わらない。 ε 遷移が 1 本増えるだけなので、03 章 の部分集合構成がそのまま吸収する。 DFA 化も最小化も一致判定も、書き換える必要が無い。

規則を 1 つ足すだけで扱える記法が増える——このことが、 01〜04 章で見た手順がその場しのぎではないことを示している。

扱わないもの

.(任意の 1 文字)も文字クラス [ ] も回数指定 {n,m} もアンカー ^ $ も無い。 後方参照(\1)に至っては、DFA では原理的に表せない。

( ) | * ? 以外の文字は、すべてそのまま 1 文字の記号として扱う。 空のパターン('')と空の括弧(())は受け付けない。

そして この範囲のパターンなら、どれが来ても同じ手順で処理できる。 a(a|b)*bb のために特別なことは何もしない。 パターンを読み、NFA を作り、DFA にして、状態数を減らし、表を引く。それだけである。

全体の流れ

ツールの処理の流れ

01〜04 章で見た手順が、そのまま 1 つずつ部品になる。

段階 理屈を見た章 実装を見る章
パターンを読む — 06 章
NFA を作る 02 章 06 章
DFA にする 03 章 07 章
状態数を最小化する 04 章 07 章
一致判定する 04 章 07 章

「パターンを読む」だけは 01〜04 に対応する章が無い。 そこでは正規表現の構造を人間が目で見て取っていたので、説明する必要が無かった。 機械にやらせるとなると、そこも手順にしなければならない。

言語は Python。3.6 以上なら、標準ライブラリだけで動く。

先に動かしてみる

出来上がるものはこれである。パターンとテキストを渡すと、一致するかどうかを答える。

$ python3 -m automaton 'a(a|b)*bb' abb ab aabb ababb bb
match     'abb'
no match  'ab'
match     'aabb'
match     'ababb'
no match  'bb'

判定は文字列全体の一致で、部分一致の検索はしない。

本当に全部扱えるのか

「この範囲なら何でも」と言い切るには根拠がいる。 tests/ では、いくつかのパターンについて短い文字列を総当たりし、 Python の re と同じ判定になることを確かめている。 a(a|b)*bb については長さ 7 以下の全文字列、 a?、ab?、(a|b)?c、(a?b)* なども含めて長さ 5 以下の全文字列である。

さらに、4 つの演算をランダムに組み合わせて作った 400 個のパターン (うち 119 個が ? を含む)について、 長さ 5 以下の全文字列(a、b、c の並び)で re と突き合わせ、 1 件も食い違いが出ないことを確認した。

では 06 章 から、順に作っていく。