これは一連の教育的正規表現の記事の2番目の部分です。これは、先読みとネストされた参照を使用して、通常でない言語と一致させる方法を示しています。んbん。ネストされた参照は最初に導入されます: この正規表現はどのようにして三角形の数を見つけるのですか?
典型的な非 通常の言語 の1つは:
L = { a
んb
ん: n > 0 }
これは、いくつかのa
の後に同数のb
が続く空でないすべての文字列の言語です。この言語の文字列の例は、ab
、aabb
、aaabbb
です。
この言語は pumping lemma によって非正規であることを示すことができます。これは実際には典型的な context-free language であり、 context-free grammarS → aSb | ab
によって生成できます。
それにもかかわらず、現代の正規表現の実装は、通常の言語だけではなく、それ以上のものを明確に認識します。つまり、それらは正式な言語理論の定義では「通常」ではありません。 PCREとPerlは再帰正規表現をサポートし、.NETはバランスグループ定義をサポートします。 「ファンシー」な機能はさらに少ない。後方参照マッチングは、正規表現が規則的でないことを意味します。
しかし、この「基本」機能はどれほど強力なのでしょうか。 Java regexなど)でL
を認識できますか?ルックアラウンドとネストされた参照を組み合わせて、たとえば String.matches
ab
、aabb
、aaabbb
などの文字列に一致させるには?
Java.util.regex.Pattern
答えは言うまでもありませんが、 YES! aに一致するJava正規表現パターンを書くことができますんbん。アサーションには正の先読みを使用し、「カウント」には1つのネストされた参照を使用します。
この回答は、パターンをすぐに示すのではなく、それを導き出すプロセスを通して読者をガイドします。ソリューションがゆっくりと構築されるにつれて、さまざまなヒントが与えられます。この側面では、うまくいけば、この回答には、単なる別のきちんとした正規表現パターンよりもはるかに多くのものが含まれます。読者が「正規表現で考える」方法、およびさまざまな構成要素を調和のとれた方法で組み合わせて、将来自分自身でより多くのパターンを導き出す方法を学ぶこともできれば幸いです。
ソリューションの開発に使用される言語は、簡潔にするためにPHPです。パターンが完成したら、最後のテストはJavaで行われます。
簡単な問題から始めましょう。文字列の先頭で_a+
_を照合しますが、直後に_b+
_が続く場合のみです。 _^
_を使用して アンカー に一致させることができます。_a+
_なしで_b+
_のみを一致させたいため、 先読み アサーション_(?=…)
_。
以下は、単純なテストハーネスを使用したパターンです。
_function testAll($r, $tests) {
foreach ($tests as $test) {
$isMatch = preg_match($r, $test, $groups);
$groupsJoined = join('|', $groups);
print("$test $isMatch $groupsJoined\n");
}
}
$tests = array('aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb');
$r1 = '/^a+(?=b+)/';
# └────┘
# lookahead
testAll($r1, $tests);
_
出力は( ideone.comで見られるように ):
_aaa 0
aaab 1 aaa
aaaxb 0
xaaab 0
b 0
abbb 1 a
_
これはまさに必要な出力です。文字列の先頭にある場合と、直後に_a+
_が続く場合に限り、_b+
_に一致します。
レッスン:ルックアラウンドでパターンを使用してアサーションを作成できます。
ここで、_b+
_を一致の一部にしたくないとしても、グループ1に キャプチャ したいとします。また、より複雑なパターンがあると予想しているため、 free-spacing にx
修飾子を使用して、正規表現を読みやすくします。
以前のPHPスニペットを基にすると、次のパターンになります。
_$r2 = '/ ^ a+ (?= (b+) ) /x';
# │ └──┘ │
# │ 1 │
# └────────┘
# lookahead
testAll($r2, $tests);
_
出力は次のようになります( ideone.comで見られるように ):
_aaa 0
aaab 1 aaa|b
aaaxb 0
xaaab 0
b 0
abbb 1 a|bbb
_
たとえば、 _aaa|b
_は、各グループが_'|'
_でキャプチャしたものをjoin
- ingした結果です。この場合、グループ0(つまり、パターンが一致したもの)はaaa
をキャプチャし、グループ1はb
をキャプチャしました。
レッスン:ルックアラウンド内でキャプチャできます。読みやすさを向上させるためにフリースペースを使用できます。
カウントメカニズムを紹介する前に、パターンに1つの変更を加える必要があります。現在、先読みは_+
_繰り返し「ループ」の外にあります。 _b+
_の後に_a+
_があると断言したかっただけですが、 really が最終的にやりたいことは、 「ループ」内で一致する各a
、それに対応するb
があります。
ここでは、カウントメカニズムについて心配せず、次のようにリファクタリングを実行します。
a+
_を_(?: a )+
_にリファクタリングします(_(?:…)
_は非キャプチャグループであることに注意してください)a*
_を「見る」前に_b+
_を「スキップ」する必要があることに注意してください。したがって、それに応じてパターンを変更しますしたがって、次のようになります。
_$r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x';
# │ │ └──┘ │ │
# │ │ 1 │ │
# │ └───────────┘ │
# │ lookahead │
# └───────────────────┘
# non-capturing group
_
出力は以前と同じです( ideone.comで見られるように )、その点で変更はありません。重要なことは、_+
_ "ループ"の毎の反復でアサーションを作成していることです。現在のパターンではこれは必要ありませんが、次に自己参照を使用してグループ1を「カウント」します。
レッスン:非キャプチャグループ内でキャプチャできます。ルックアラウンドは繰り返すことができます。
ここでは、次のようにします。グループ1を次のように書き換えます。
+
_の最初の反復の最後に、最初のa
が一致すると、b
をキャプチャする必要がありますa
が一致すると、bb
をキャプチャする必要がありますbbb
をキャプチャする必要がありますb
がない場合、アサーションは単に失敗しますしたがって、グループ1は_(b+)
_になりましたが、_(\1 b)
_のように書き直す必要があります。つまり、前の反復でグループ1がキャプチャしたものにb
を「追加」しようとします。
このパターンには「基本ケース」が欠けている、つまり自己参照なしで一致する可能性があるという点で、ここにわずかな問題があります。グループ1は「初期化されていない」状態で開始されるため、基本ケースが必要です。まだ何も(空の文字列でさえ)キャプチャしていないため、自己参照の試みは常に失敗します。
これには多くの方法がありますが、今のところは、自己参照マッチングを オプション 、つまり_\1?
_にしてみましょう。これは完全に機能する場合と機能しない場合がありますが、それが何をするかを見てみましょう。問題が発生した場合は、その橋を渡ったときにその橋を渡ります。また、テストケースを追加します。
_$tests = array(
'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb'
);
$r4 = '/ ^ (?: a (?= a* (\1? b) ) )+ /x';
# │ │ └─────┘ | │
# │ │ 1 | │
# │ └──────────────┘ │
# │ lookahead │
# └──────────────────────┘
# non-capturing group
_
出力は次のようになります( ideone.comで見られるように ):
_aaa 0
aaab 1 aaa|b # (*gasp!*)
aaaxb 0
xaaab 0
b 0
abbb 1 a|b # yes!
aabb 1 aa|bb # YES!!
aaabbbbb 1 aaa|bbb # YESS!!!
aaaaabbb 1 aaaaa|bb # NOOOOOoooooo....
_
あはは!私たちは今、本当に解決策に近いようです!自己参照を使用してグループ1を「カウント」することができました!しかし、ちょっと待ってください... 2番目と最後のテストケースに問題があります!! b
sが足りません。どういうわけかそれは間違っていました!これが次のステップで発生した理由を調べます。
レッスン:自己参照グループを「初期化」する1つの方法は、自己参照を一致させることですオプション。
問題は、自己参照マッチングをオプションにしたので、b
が足りない場合、「カウンター」が0に「リセット」できるということです。入力としてaaaaabbb
を使用して、パターンの各反復で何が起こるかを詳しく調べてみましょう。
_ a a a a a b b b
↑
# Initial state: Group 1 is "uninitialized".
_
a a a a a b b b
↑
# 1st iteration: Group 1 couldn't match \1 since it was "uninitialized",
# so it matched and captured just b
___
a a a a a b b b
↑
# 2nd iteration: Group 1 matched \1b and captured bb
_____
a a a a a b b b
↑
# 3rd iteration: Group 1 matched \1b and captured bbb
_
a a a a a b b b
↑
# 4th iteration: Group 1 could still match \1, but not \1b,
# (!!!) so it matched and captured just b
___
a a a a a b b b
↑
# 5th iteration: Group 1 matched \1b and captured bb
#
# No more a, + "loop" terminates
_
あはは! 4回目の繰り返しでも、_\1
_とは一致しましたが、_\1b
_とは一致しませんでした。 _\1?
_を使用して自己参照マッチングをオプションにすることを許可しているため、エンジンはバックトラックして「ノーサンキュー」オプションを選択しました。これにより、b
!
ただし、最初の反復を除いて、常に自己参照_\1
_のみに一致させることができることに注意してください。もちろん、これは前のイテレーションでキャプチャしたものであり、セットアップではいつでも再び一致させることができるため、明らかです(たとえば、前回bbb
をキャプチャした場合でも、まだ存在することが保証されます) bbb
になりますが、今回はbbbb
が存在する場合と存在しない場合があります)。
レッスン:バックトラックに注意してください。正規表現エンジンは、指定されたパターンが一致するまで、可能な限り多くのバックトラックを行います。これは、パフォーマンス( 破滅的なバックトラック )や正確さに影響を与える可能性があります。
「修正」は明白になりました。オプションの繰り返しを 所有格 数量詞と組み合わせます。つまり、単純に_?
_の代わりに_?+
_を使用します(所有格として定量化される繰り返しは、そのような「協調」が全体的なパターンの一致をもたらす可能性がある場合でも、バックトラックしないことに注意してください)。
非常に非公式な言葉で言えば、これは_?+
_、_?
_および_??
_が言うことです:
_
?+
_
- (オプション)「そこにある必要はありません」
- (所有している)「しかし、もしそこにあるなら、それを手放さず、手放さないでください!」
_
?
_
- (オプション)「そこにある必要はありません」
- (貪欲)「でも、もしそうなら今のところはそれでいい」
- (バックトラック)「しかし、後でそれを手放すように求められるかもしれません!」
_
??
_
- (オプション)「そこにある必要はありません」
- (しぶしぶ)「そして、たとえそうであっても、まだ取る必要はない」
- (バックトラッキング)「しかし、後でそれをとるように求められるかもしれません!」
私たちのセットアップでは、_\1
_は最初はありませんが、 always 以降はいつでもあり、 always次に一致させたい。したがって、_\1?+
_は、まさに私たちが望むものを実現します。
_$r5 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ /x';
# │ │ └──────┘ │ │
# │ │ 1 │ │
# │ └───────────────┘ │
# │ lookahead │
# └───────────────────────┘
# non-capturing group
_
出力は( ideone.comで見られるように )です:
_aaa 0
aaab 1 a|b # Yay! Fixed!
aaaxb 0
xaaab 0
b 0
abbb 1 a|b
aabb 1 aa|bb
aaabbbbb 1 aaa|bbb
aaaaabbb 1 aaa|bbb # Hurrahh!!!
_
ほら!問題が解決しました!!!これで、希望どおりに正しくカウントされます。
レッスン:貪欲、しぶしぶ、所有格の繰り返しの違いを学びます。 Optional-Posessiveは強力な組み合わせになります。
したがって、今のところはa
に繰り返し一致するパターンであり、一致したa
ごとに、対応するb
がグループ1にキャプチャされます。_+
_は、a
がなくなるか、b
に対応するa
がないためにアサーションが失敗した場合に終了します。
ジョブを完了するには、パターンに_\1 $
_を追加するだけです。これは、グループ1が一致したものへの後方参照になり、その後に行アンカーの終わりが続きます。アンカーは、文字列に余分なb
がないことを保証します。つまり、実際には aんbん。
これが最終的なパターンで、10,000文字の長さのテストケースを含む、追加のテストケースがあります。
_$tests = array(
'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb',
'', 'ab', 'abb', 'aab', 'aaaabb', 'aaabbb', 'bbbaaa', 'ababab', 'abc',
str_repeat('a', 5000).str_repeat('b', 5000)
);
$r6 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ \1 $ /x';
# │ │ └──────┘ │ │
# │ │ 1 │ │
# │ └───────────────┘ │
# │ lookahead │
# └───────────────────────┘
# non-capturing group
_
4つの一致が検出されます:ab
、aabb
、aaabbb
、および a5000b5000。 ideone.comでの実行には0.06秒しかかかりません 。
したがって、パターンはPHPで機能しますが、最終的な目標は、Javaで機能するパターンを記述することです。
_public static void main(String[] args) {
String aNbN = "(?x) (?: a (?= a* (\\1?+ b)) )+ \\1";
String[] tests = {
"", // false
"ab", // true
"abb", // false
"aab", // false
"aabb", // true
"abab", // false
"abc", // false
repeat('a', 5000) + repeat('b', 4999), // false
repeat('a', 5000) + repeat('b', 5000), // true
repeat('a', 5000) + repeat('b', 5001), // false
};
for (String test : tests) {
System.out.printf("[%s]%n %s%n%n", test, test.matches(aNbN));
}
}
static String repeat(char ch, int n) {
return new String(new char[n]).replace('\0', ch);
}
_
パターンは期待どおりに機能します( ideone.comで見られるように )。
先読みの_a*
_、および実際の「メイン_+
_ループ」は、どちらもバックトラックを許可していることに注意する必要があります。読者は、これが正確性の点で問題ではない理由、および同時に両方の所有格にすることも機能する理由を確認することをお勧めします(おそらく、同じパターンで必須と非必須の所有格指定子を混在させると誤解が生じる可能性があります)。
また、 aに一致する正規表現パターンがあることはきちんとしていることにも注意してください。んbん、これは実際には常に「最良の」ソリューションとは限りません。より良い解決策は、単純に^(a+)(b+)$
を照合し、ホスティングプログラミング言語でグループ1と2によってキャプチャされた文字列の長さを比較することです。
PHPでは、次のようになります( ideone.comで見られるように ):
_function is_anbn($s) {
return (preg_match('/^(a+)(b+)$/', $s, $groups)) &&
(strlen($groups[1]) == strlen($groups[2]));
}
_
この記事の目的は、正規表現がほとんど何でもできることを読者に納得させるために [〜#〜]ではありません[〜#〜] です。それができないことは明らかであり、それができることであっても、より簡単なソリューションにつながる場合は、ホスティング言語への少なくとも部分的な委任を検討する必要があります。
冒頭で述べたように、この記事には必ずstackoverflowの_[regex]
_のタグが付けられますが、それ以上のものである可能性があります。アサーション、ネストされた参照、所有格指定子などについて学ぶことには確かに価値がありますが、おそらく、ここでのより大きなレッスンは、問題を解決するための創造的なプロセス、決断、そして困難に直面するときにしばしば必要となるハードワークです。さまざまな制約、実用的なソリューションを構築するためのさまざまなパーツからの体系的な構成など。
PHPを起動したので、PCREは再帰的なパターンとサブルーチンをサポートしていると言う必要があります。したがって、次のパターンは_preg_match
_で機能します( ideone.comで見られるように ):
_$rRecursive = '/ ^ (a (?1)? b) $ /x';
_
現在、Javaの正規表現は再帰的なパターンをサポートしていません。
したがって、 aを照合する方法を見てきました。んbんこれは通常ではありませんが、コンテキストに依存しませんが、 aにも一致させることができますんbんcん、これもコンテキストフリーではありませんか?
もちろん、答えは YES!です。読者は自分でこれを解決することをお勧めしますが、解決策を以下に示します( での実装Java on ideone.com )。
^ (?: a (?= a* (\1?+ b) b* (\2?+ c) ) )+ \1 \2 $
再帰パターンをサポートするPCREについて言及されていないことを考えると、問題の言語を記述するPCREの最も単純で最も効率的な例を指摘したいと思います。
/^(a(?1)?b)$/
質問で述べたように、.NET平衡化グループでは、タイプのパターンんbんcんdん…zん簡単に一致させることができます
^
(?<A>a)+
(?<B-A>b)+ (?(A)(?!))
(?<C-B>c)+ (?(B)(?!))
...
(?<Z-Y>z)+ (?(Y)(?!))
$
例: http://www.ideone.com/usuOE
編集:
再帰パターンを使用した一般化された言語用のPCREパターンもありますが、先読みが必要です。これは上記の直訳ではないと思います。
^
(?=(a(?-1)?b)) a+
(?=(b(?-1)?c)) b+
...
(?=(x(?-1)?y)) x+
(y(?-1)?z)
$