クイック スタート
チュートリアル
ツール & 言語
リファレンス
書評
正規表現チュートリアル
はじめに
目次
特殊文字
印字不可能な文字
正規表現エンジンの内部
文字クラス
文字クラスの減算
文字クラスの共通部分
省略形の文字クラス
ドット
アンカー
単語境界
選択
オプション項目
繰り返し
グループ化 & キャプチャ
後方参照
後方参照、パート2
名前付きグループ
相対後方参照
ブランチリセットグループ
フリースペーシング & コメント
Unicode
モード修飾子
アトミックグループ化
所有格量指定子
先読み & 後読み
先読み/後読み、パート2
マッチからテキストを除外する
条件
バランシンググループ
再帰
サブルーチン
無限再帰
再帰 & 量指定子
再帰 & キャプチャ
再帰 & 後方参照
再帰 & バックトラック
POSIXブラケット式
長さ0のマッチ
継続マッチ
このサイトのその他の情報
はじめに
正規表現クイックスタート
正規表現チュートリアル
置換文字列チュートリアル
アプリケーションと言語
正規表現の例
正規表現リファレンス
置換文字列リファレンス
書評
印刷用PDF
このサイトについて
RSSフィード & ブログ
RegexBuddy—Better than a regular expression tutorial!

バランシンググループによるネスト構造のマッチング

.NET の正規表現フレーバーには、バランシンググループと呼ばれる特別な機能があります。バランシンググループの主な目的は、バランスの取れた構造やネストされた構造をマッチさせることです。これが名前の由来です。この機能の技術的により正確な名前は、キャプチャグループの減算でしょう。それがこの機能が実際に行うことです。これは、PerlPCRERuby などの他の正規表現フレーバーが 正規表現の再帰で処理する問題に対する .NET のソリューションです。 JGsoft V2 は、バランシンググループと再帰の両方をサポートしています。

(?<capture-subtract>regex)または(?'capture-subtract'regex)は、バランシンググループの基本的な構文です。.NET の名前付きキャプチャグループに使用される構文と同じですが、2 つのグループ名がマイナス記号で区切られています。このグループの名前は「capture」です。グループ名を省略できます。(?<-subtract>regex)または(?'-subtract'regex)は、非キャプチャのバランシンググループの構文です。

名前「subtract」は、正規表現内の別のグループの名前である必要があります。正規表現エンジンがバランシンググループに入ると、「subtract」グループから1つの一致が差し引かれます。「subtract」グループがまだ一致していない場合、またはすべての一致がすでに差し引かれている場合、バランシンググループはマッチに失敗します。バランシンググループは、「subtract」グループをテストする条件と考えることができます。「regex」が「if」部分であり、「else」部分は常にマッチに失敗します。違いは、バランシンググループには「subtract」グループから1つの一致を差し引くという追加機能があるのに対し、条件ではグループは変更されないことです。

バランシンググループが成功し、名前(この例では「capture」)がある場合、グループは、「subtract」グループから差し引かれたマッチの終わりと、バランシンググループ自体のマッチの開始(この例では「regex」)の間のテキストをキャプチャします。

これが .NET で機能する理由は、.NET のキャプチャグループは、バックトラックまたは差し引かれていないマッチングプロセス中にキャプチャしたすべてのもののスタックを保持するためです。他のほとんどの正規表現エンジンは、各キャプチャグループの最新の一致のみを保存します。以下の場合(\w)+abcとマッチした場合、Match.Groups[1].Valuecを他の正規表現エンジンと同様に返しますが、Match.Groups[1].Capturesは、グループの3回の反復すべてを保存します。a, b、そしてc.

正規表現エンジンの内部を見る

正規表現を適用してみましょう(?'open'o)+(?'between-open'c)+を文字列ooccc. (?'open'o)は最初のoと一致し、それを「open」グループの最初のキャプチャとして保存します。量指定子+はグループを繰り返します。(?'open'o)は2番目のoと一致し、それを2番目のキャプチャとして保存します。もう一度繰り返すと、(?'open'o)は最初のcと一致しません。ただし、+は2回の繰り返しで満足します。

正規表現エンジンは次に進みます(?'between-open'c)。エンジンがこのバランシンググループに入る前に、差し引かれたグループ「open」が何かをキャプチャしたかどうかを確認する必要があります。2番目のoをキャプチャしました。エンジンはグループに入り、「open」から最新のキャプチャを差し引きます。これにより、「open」グループは最初のoのみをキャプチャとして残します。バランシンググループ内では、cc。エンジンはバランシンググループを終了します。「between」グループは、「open」から差し引かれたマッチ(2番目のo)とcバランシンググループによって一致したばかりの間にあるテキストをキャプチャします。これは空の文字列ですが、とにかくキャプチャされます。

バランシンググループにも+が量指定子として設定されています。エンジンは再び、差し引かれたグループ「open」が何か、つまり最初のoをキャプチャしたことを発見します。正規表現はバランシンググループに入り、「open」グループにはマッチが残っていません。cは2番目のc文字列内にあります。「between」グループはocをキャプチャします。これは、「open」から差し引かれたマッチ(最初のo)と2番目のcバランシンググループによってマッチしたばかりの間にあるテキストです。

バランシンググループは再び繰り返されます。ただし、今回は、正規表現エンジンは「open」グループに一致が残っていないことを発見します。バランシンググループはマッチに失敗します。「between」グループは影響を受けず、最新のキャプチャを保持します。

+2回の反復で満足します。エンジンは正規表現の最後に到達しました。エンジンはooccを全体の一致として返します。Match.Groups['open'].Successfalseを返します。そのグループのキャプチャはすべて差し引かれたからです。Match.Groups['between'].Value"oc".

バランスの取れたペアのマッチング

o と c のバランスの取れた数をマッチさせたい場合は、この正規表現を修正する必要があります。正規表現がoocccとマッチしないようにするには、c の数が o の数より多いので、アンカーを追加できます^(?'open'o)+(?'-open'c)+$。この正規表現は、前のものと同じマッチングプロセスをたどります。ただし、(?'-open'c)+の3回目の反復とのマッチに失敗した後、エンジンは正規表現の最後ではなく$に到達します。これは一致に失敗します。正規表現エンジンは、量指定子のさまざまな順列を試してバックトラックしますが、すべて一致に失敗します。一致は見つかりません。

ただし、正規表現^(?'open'o)+(?'-open'c)+$oocと依然としてマッチします。マッチングプロセスは、バランシンググループが最初のcと一致し、グループ「open」に最初のoのみをキャプチャとして残すまで、再び同じです。量指定子により、エンジンはバランシンググループを再度試みます。エンジンは再び、差し引かれたグループ「open」が何かをキャプチャしたことを発見します。正規表現はバランシンググループに入り、「open」グループにはマッチが残っていません。しかし今、cは、正規表現エンジンが文字列の最後に到達したため、一致に失敗します。

正規表現エンジンは、バランシンググループからバックトラックする必要があります。.NETでは、バランシンググループをバックトラックするとき、減算もバックトラックされます。最初のoのキャプチャがバランシンググループに入るときに「open」から差し引かれたため、このキャプチャは、バランシンググループからバックトラックしているときに復元されます。繰り返しグループ(?'-open'c)+は、1回の反復に削減されます。ただし、量指定子はそれで問題ありません。なぜなら、+は常に「1回以上」を意味するからです。文字列の最後にありながら、正規表現エンジンは$に到達し、これは一致します。文字列全体oocが全体の一致として返されます。Match.Groups['open'].Capturesは、文字列内の最初のoを CaptureCollection 内の唯一の項目として保持します。これは、バックトラック後、2番目のoがグループから差し引かれましたが、最初のoが差し引かれていないからです。

正規表現がocooccとマッチする一方で、oocとはマッチしないようにするには、マッチングプロセスが正規表現の最後に到達したときにグループ「open」にキャプチャが残っていないことを確認する必要があります。これは、条件を使用して行うことができます。(?(open)(?!))は、グループ「open」が何かと一致したかどうかを確認する条件です。.NETでは、何かと一致したとは、バックトラックまたは差し引かれていないスタックにまだキャプチャがあることを意味します。グループが何かをキャプチャした場合、条件の「if」部分が評価されます。この場合は、空の否定先読み(?!)です。この先読み内の空の文字列は常に一致します。先読みが否定であるため、これにより先読みが常に失敗します。したがって、グループが何かをキャプチャした場合、条件は常に失敗します。グループが何もキャプチャしていない場合、条件の「else」部分が評価されます。この場合、「else」部分はありません。これは、グループが何かをキャプチャしていない場合、条件が常に成功することを意味します。これにより、(?(open)(?!))が、グループ「open」にキャプチャが残っていないことを検証するための適切なテストになります。

正規表現^(?'open'o)+(?'-open'c)+(?(open)(?!))$マッチに失敗します。ooc。なぜなら、c正規表現エンジンが文字列の終端に達したため、マッチに失敗します。エンジンはバランシンググループからバックトラックし、"open"には単一のキャプチャが残ります。正規表現エンジンは条件付き部分に到達しますが、これはマッチに失敗します。正規表現エンジンは量指定子の異なる順列を試してバックトラックしますが、それらはすべてマッチに失敗します。マッチは見つかりません。

正規表現^(?'open'o)+(?'-open'c)+(?(open)(?!))$マッチします。oocc。なぜなら、(?'-open'c)+がマッチした後、cc、正規表現エンジンは3回目のバランシンググループに入ることができません。これは、"open"にキャプチャが残っていないためです。エンジンは条件付き部分に進みます。条件付き部分は、"open"にキャプチャが残っておらず、条件付き部分に"else"部分がないため、成功します。これで$が文字列の終端にマッチします。

バランスの取れた構造のマッチング

^(?:(?'open'o)+(?'-open'c)+)+(?(open)(?!))$は、キャプチャグループとバランシンググループを、繰り返される非キャプチャグループで囲みます。この正規表現は、次のような任意の文字列にマッチします。ooocooccocccocこれは、任意の数の完全にバランスの取れたoとcを含み、任意の数のペアが連続して、任意の深さにネストされたものです。バランシンググループは、文字列の任意の時点で、その時点より左にあるoの数よりも多いcを持つ文字列に、正規表現がマッチしないようにします。最後に、反復グループの外にある必要がある条件付き部分は、正規表現がcよりも多いoを持つ文字列にマッチしないようにします。

^(?>(?'open'o)+(?'-open'c)+)+(?(open)(?!))$は、前の正規表現をアトミックグループを非キャプチャグループの代わりに使用することで最適化します。アトミックグループは、非キャプチャグループでもあり、正規表現がマッチを見つけられない場合、ほぼすべてのバックトラックを排除します。これにより、たくさんのoとcがあり、最後に適切にバランスが取れていない長い文字列で使用した場合に、パフォーマンスを大幅に向上させることができます。アトミックグループは、バランスの取れたoとcを持つ文字列に正規表現がマッチする方法を変更しません。

^m*(?>(?>(?'open'o)m*)+(?>(?'-open'c)m*)+)+(?(open)(?!))$は、任意の数の文字を許可します。m文字列内の任意の場所に配置でき、oとcはすべてバランスが取れている必要があります。m*正規表現の先頭では、最初のoの前に任意の数のmを許可します。(?'open'o)+は、次のように変更されました。(?>(?'open'o)m*)+これにより、各oの後に任意の数のmを許可します。同様に、(?'-open'c)+は、次のように変更されました。(?>(?'-open'c)m*)+これにより、各cの後に任意の数のmを許可します。

これは、.NETのバランシンググループまたはキャプチャグループの減算機能を使用して、バランスの取れた構造をマッチングするための一般的なソリューションです。次を置き換えることができます。o, m、そしてcこれらの3つのうち2つが同じテキストにマッチしない限り、任意の正規表現を使用できます。

^[^()]*(?>(?>(?'open'\()[^()]*)+(?>(?'-open'\))[^()]*)+)+(?(open)(?!))$は、この手法を適用して、すべての括弧が完全にバランスが取れている文字列にマッチさせます。

減算されたグループへの後方参照

後方参照を使用して、バランシンググループによってマッチが減算されたグループを参照できます。後方参照は、バックトラックまたは減算されなかったグループの最新のマッチにマッチします。正規表現(?'x'[ab]){2}(?'-x')\k'x'aaa, aba, bab、またはbbbにマッチします。次にはマッチしません。aab, abb, baa、またはbba。文字列の最初と3番目の文字は同じである必要があります。

どのように動作するか見てみましょう。(?'x'[ab]){2}(?'-x')\k'x'abaの最初の反復で、(?'x'[ab])aをキャプチャします。2回目の反復でbをキャプチャします。次に、正規表現エンジンはバランシンググループ(?'-x')に到達します。これは、グループ"x"がマッチしたかどうかを確認します。マッチしています。エンジンはバランシンググループに入り、マッチbをグループ"x"のスタックから減算します。バランシンググループ内には正規表現トークンはありません。文字列を進めずにマッチします。これで、正規表現エンジンは後方参照\k'x'に到達します。グループ"x"のスタックの先頭にあるマッチはaです。文字列の次の文字もaであり、後方参照がこれにマッチします。abaは全体的なマッチとして見つかります。

この正規表現をabbに適用すると、マッチングプロセスは同じですが、後方参照が2番目のbにマッチできない点が異なります。正規表現には正規表現エンジンが試せる他の順列がないため、マッチの試みは失敗します。

回文のマッチング

^(?'letter'[a-z])+[a-z]?(?:\k'letter'(?'-letter'))+(?(letter)(?!))$は、任意の長さの回文単語にマッチします。この正規表現は、後方参照とキャプチャグループの減算がうまく連携するという事実を利用しています。また、前のセクションの正規表現と同様に、空のバランシンググループを使用しています。

この正規表現が回文radar. ^にどのようにマッチするかを見てみましょう。(?'letter'[a-z])+は5回反復します。グループ"letter"は、スタックに5つのマッチを残して終了します。r, a, d, a、そしてr。正規表現エンジンは、文字列の終端および正規表現の[a-z]?にいます。これはマッチしませんが、量指定子がオプションにしているため、問題ありません。次に、エンジンは後方参照\k'letter'に到達します。グループ"letter"のスタックの先頭にはrがあります。これは、文字列の終端後のvoidにマッチできません。

正規表現エンジンはバックトラックします。(?'letter'[a-z])+は4回の反復に減らされ、グループ"letter"のスタックにはr, a, d、そしてaが残ります。[a-z]?r。後方参照は再び文字列の終端後のvoidにマッチできません。エンジンはバックトラックし、[a-z]?にマッチをあきらめさせます。これで、"letter"のスタックの先頭にはaがあります。これにより、後方参照がマッチに失敗します。r.

さらなるバックトラックが続きます。(?'letter'[a-z])+は3回の反復に減らされ、グループ"letter"のスタックの先頭にはdが残ります。エンジンは再び[a-z]?を進めます。後方参照にマッチするdがないため、再び失敗します。

もう一度バックトラックすると、グループ"letter"のキャプチャスタックはraに減ります。今、流れが変わります。[a-z]?d。後方参照は、aにマッチします。これは、バックトラックされなかったグループ"letter"の最新のマッチです。エンジンは、空のバランシンググループ(?'-letter')に到達します。グループ"letter"に減算するマッチaがあるため、これはマッチします。

後方参照とバランシンググループは、繰り返される非キャプチャグループ内にあるため、エンジンはそれらをもう一度試行します。後方参照はrそして、バランシンググループはそれを「letter」のスタックから減算し、キャプチャグループは一致するものがない状態になります。もう一度繰り返すと、「letter」グループのスタックには一致するものが残っていないため、後方参照は失敗します。これにより、グループは非参加グループとして機能します。非参加グループへの後方参照は、ほとんどの正規表現フレーバーと同様に、.NETでは常に失敗します。

(?:\k'letter'(?'-letter'))+は、2回の繰り返しに正常に一致しました。次に、条件式(?(letter)(?!))は、「letter」グループには一致するものが残っていないため、成功します。アンカー$も一致します。回文radarがマッチしました。