SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

正規表現エンジンを作ろう

正規表現エンジンを作ろう (6)

DFAエンジンとNFAエンジン

  • X ポスト
  • このエントリーをはてなブックマークに追加

NFAエンジンのNFAの表現

 先ほどの実行結果のデバッグ情報を見ると、なにやらアルファベットと数字の羅列が表示されています。実はこれが、NFAエンジンでのNFAの表現です。これは正規表現をバイトコードに変換したもので、各行はオペコードとなっています。

 第1回で述べた通り、NFAエンジンはNFAの非決定性を深さ優先で探索します。失敗するまで突き進み、失敗をしたら処理を巻き戻すと言う動作を、オペコードのBRANCHで表現します。

 試しに、和集合演算をオペコードにするとどうなるかを見てみましょう。まず、和集合演算のNFAの表現は、以下のようなものでした。εにより、状態遷移の可能性が2つに分離しています。

Unionノードを表すNFA(再掲)
Unionノード

 それでは、先ほどのtryコマンドを使い、"a|b"をコンパイルしてできるバイトコードを見てみましょう。

a|bのOPEコード
% ./try 'a|b'
 1:BRANCH(9)
 4:EXACTLY(17)a
 9:BRANCH(17)
12:EXACTLY(17)b
17:END(0)

 BRANCHが、それぞれの分岐の始まりを表しています。この正規表現では、1:と9:のBRANCHから始まる、2つの分岐があります。1:の分岐は'a'、2:の分岐は'b'にマッチします。マッチングが始まると、NFAエンジンはオペコードの順に従って上から下に処理をして行きます。そして、BRANCHが登場すると枝分かれの準備をします。なお、今回参照にしているエンジンでは、枝分かれは関数の再起呼び出しで表現しています。

 例えば、'b'という文字列のマッチングを考えてみましょう。NFAエンジンは、まず1:のBRANCHに入り、'b'が'a'にマッチするかを判定します。ここで、マッチは失敗しますので、再起呼び出しされた関数からreturnで失敗を告げると同時に、1:のBRANCHに書かれている9:のブランチにジャンプします。次に、12:にある通り、'b'と'b'をマッチングさせます。これは成功しますので、そのまま12:に指定されている17:にジャンプし、成功として処理を終了します。

スター演算

 もう一つ枝分かれがある例として、繰り返し演算を見ます。繰り返し演算のNFA表現は以下のようなものでした。

Starノードを表すNFA(再掲)
Starノード

 この状態遷移図だけを見ると、εによる遷移はありますが枝分かれはしていないように見えます。が、受理状態は連結演算の際に、他のフラグメントとεでつなげられます。よって、元のフラグメントの初期状態に戻るε遷移と、この後ろのフラグメントに続くε遷移の2つの遷移可能性があります。

 果たしてそうなっているか、再びtryコマンドで表現を見てみます。ただし、このNFAエンジンではa*のように一文字の繰り返しはBRANCHを利用せずに最適化されてしまいます。そこで、(a)*と言うように括弧で括った正規表現を利用します。

(a)*のOPEコード
% ./try '(a)*'
 1:BRANCH(30)
 4:BRANCH(24)
 7:OPEN1(10)
10:BRANCH(18)
13:EXACTLY(18)a
18:CLOSE1(21)
21:BACK(4)
24:BRANCH(27)
27:NOTHING(30)
30:END(0)

 1:と10:と24:はBRANCH命令となっていますが、実際は枝分かれのない一つのノードです(括弧内に書かれている飛び先(30:や18:や27:)がBRANCHではないことから判断します)。着目すべきは、4:と24:のBRANCHであり、ここで2つに分岐しています。

 4:のBRANCHを見ると、13:に"a"とのマッチが含まれます。つまり、こちらは"(a)"が表すノードへ遷移すると言う可能性に該当します。一方の24:のBRANCHはNOTHINGを経てENDに向かう分岐であり、ノード内に入らずに次に進むと言う遷移を表しています。

NFAエンジンが遅かった理由

 では、先ほどNFAエンジンが"X*X*X*X*X*X*X*X*X*X*XXXXXXXXXX"に苦戦した理由を考えてみましょう。

 まず、X*と言う正規表現ですが、この正規表現は分岐の固まりです。先ほど見たように、繰り返し演算は一つしか枝分かれを誘発しません。しかし、この枝分かれは 21:BACK によってマッチすればするほど繰り返されます。文字列"XXXXXXXXXX"をマッチさせる場合、1つのXに対して、それをX*内のXにマッチさせるか、マッチさせずに次に進めるかの2択の選択肢が用意されます。今回は10個のXがありますので、ここだけで10個の取りうる遷移があるわけです。

 この正規表現の場合、正規表現の後方に10個分のXが全てありますので、「X*には1文字もマッチさせない」のが正解です。ところが、NFAエンジンはなるべくXをマッチさせようとするため、先ほどの10の選択肢全てで、誤った選択肢(次のノードに進まずにXにマッチさせる)を選んでしまいます。

 さらに悪いことに、最初のX*で「1文字もX*にマッチさせない」と言う選択肢をうまく選べても、その後にも次々とX*が待ち構えています。これらの選択肢全てに置いて、NFAエンジンは誤った選択をします。よって、試さなければいけない遷移の数が爆発してしまい、処理が遅くなってしまうのです。

 なお、先ほどのマッチングの場合ですと、X10個を、10個のX*と後続のXの計11パターンのどこかに割り振ることになりますので、その組み合わせの数は11パターンから10個のXの行き先を選ぶ重複組み合わせとなります。よって、20!÷(10!×10!)=184,756通りあります。成功するのは全て後続のXからとる1パターンだけで、前述した通りNFAエンジンは最後まで誤った選択肢を選び続け、分岐をやり直す回数は184,755回ということになります。

 以上、NFAエンジンについて簡単に紹介しました。

まとめ

 今回は、連載のまとめとして以下の内容を説明しました。

  • DFAエンジンの試用
  • NFAとDFAのdump
  • NFAエンジンの仕組み

 今回の連載で、駆け足ではありますが正規表現エンジンの理論の基礎を説明しました。正規表現が用意されてない環境でも、考え方さえ分かってしまえば案外簡単に自分で実装することができます。ツールとしての正規表現は非常に強力ですので、ぜひとも使いこなし、アプリケーションの作成に役立てて頂ければと思います。

参考資料

  1. Henry Spencer『regex - Henry Spencer's regular expression libraries
  2. Russ Cox『Regular Expression Matching Can Be Simple And Fast

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
正規表現エンジンを作ろう連載記事一覧

もっと読む

この記事の著者

hiratara(ヒラタラ)

1977年に苫小牧市で生まれる。北海道大学理学部数学科卒。小学生の頃、両親に買い与えられたMZ-2500でプログラミングを始めた。学生時代、CGIの自作に没頭し、それ以降WEB開発の魅力に憑かれる。社会人になっても数学好きは変わらず、専門書を買い集めるのが最近の趣味。id:hirataraにてblogを執筆...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/3188 2008/12/10 14:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング