はじめに
Microsoft Docsを見ると、正規表現の(?=subexpression)は、ゼロ幅の肯定先読みアサーションです、って書いてあります。でもこれだとなんのことがさっぱりわからないです。
どんな時に「ゼロ幅の肯定先読みアサーション」を使うのか、具体的な例で説明を試みてみます。
ただ、なかなか正規表現のサンプルで利用する適切な文章を思い浮かべることができません。なので僕の大好きな筒井康隆先生の以下の文章から単語を抜き出すことを考えてみましょう。
ドンドンはドンドコの父なり。ドンドンの子ドンドコ、ドンドコドンを生み、ドンドコドン、ドコドンドンとドンタカタを生む。ドンタカタ、ドカタンタンを生めり。
『バブリング創世記』(筒井康隆著)の冒頭部分を抜粋(改行せずに1文字列にしている)しています。
カタカナの単語を取り出す
まずはゼロ幅の肯定先読みアサーションを必要としない簡単な例をみてみましょう。上記文字列からカタカナの単語(名前)を取り出します。
using System;
using System.Linq;
using System.Text.RegularExpressions;
// 『バブリング創世記』(筒井康隆著)の冒頭部分を抜粋(改行せずに1文字列にしている)
var text = @"ドンドンはドンドコの父なり。ドンドンの子ドンドコ、ドンドコドンを生み、
ドンドコドン、ドコドンドンとドンタカタを生む。ドンタカタ、ドカタンタンを生めり。";
var pattern = @"\p{IsKatakana}+";
var matches = Regex.Matches(text, pattern);
Console.WriteLine(String.Join(",", matches.Cast<Match>().Select(x => x.Value)));
以下のような結果が得られます。
ドンドン,ドンドコ,ドンドン,ドンドコ,ドンドコドン,ドンドコドン,ドコドンドン,ドンタカタ,ドンタカタ,ドカタンタン
正規表現のデフォルトは最長一致なので、@"\p{IsKatakana}+" だけで、うまくカタカナの単語を取り出せています。
「、」の前にあるカタカナの単語を取り出したい
では、「、」の前にあるカタカナの単語を取り出したいとします。以下のようなコードを書いてみました。
var pattern = @"\p{IsKatakana}+、";
var matches = Regex.Matches(text, pattern);
Console.WriteLine(String.Join(",", matches.Cast<Match>().Select(x => x.Value)));
まずは、求めたい結果は以下の通りです。
ドンドコ,ドンドコドン,ドンタカタ
結果です。
ドンドコ、,ドンドコドン、,ドンタカタ、
当然ですがダメですね。「、」も拾ってきてしまいます。
グループ化で解決する
これを解決する一つの方法が、丸括弧()を使ったグループ化です。
var pattern = @"(\p{IsKatakana}+)、";
var matches = Regex.Matches(text, pattern);
Console.WriteLine(String.Join(",", matches.Cast<Match>().Select(x => x.Groups[1].Value)));
結果を取り出す際もGropusプロパティを使うように変更しています。
実行すると下記のように正しく取り出せました。
ドンドコ,ドンドコドン,ドンタカタ、
ゼロ幅の肯定先読みアサーションを使って解決する
やっと本題です。
expression(?=subexpression)
という書式のゼロ幅の肯定先読みアサーションを使っても解決可能です。
こちらの方がスマートかもしれません。Valueプロパティがそのまま使えます。
var pattern = @"\p{IsKatakana}+(?=、)";
var matches = Regex.Matches(text, pattern);
Console.WriteLine(String.Join(",", matches.Cast<Match>().Select(x => x.Value)));
(?=、)
の部分が、「ゼロ幅の肯定先読みアサーション」を使った箇所です。難しい用語ですが、要は
「マッチの条件には含めるけれど、マッチの範囲はexpression
だけで、その後に続くsubexpression
はマッチの範囲には含めないよ」
ということです。
上記例では、"ドンドコ、"とは一致するけど、"、"はマッチした文字列には含めないよ、ということです。
そのため、Valueプロパティには"ドンドコ"だけが入ることになります。
ゼロ幅の正の後読みアサーションの例
では、もうひとつ例をだしましょう。今度は、「と」の前後にあるカタカナの単語を抜き出したいとします。
以下ように3つの単語がつながっている場合も考慮することにしましょう。
パダンパダン、パラパラとパンパンとパンパカパンを生み、
これも、『バブリング創世記』(筒井康隆著)からの引用です。
これを実現するには、ゼロ幅の正の後読みアサーション
(?<=subexpression)expression
も利用します。
こちらは、
「マッチの条件には含めるけれど、マッチの範囲はsubexpression
の後に続くexpression
だけで、subexpression
はマッチの範囲には含めないよ」
という意味になります。
ではコードを示します。
var text = @"ドンドンはドンドコの父なり。ドンドンの子ドンドコ、ドンドコドンを生み、
ドンドコドン、ドコドンドンとドンタカタを生む。ドンタカタ、ドカタンタンを生めり。"
+ "パダンパダン、パラパラとパンパンとパンパカパンを生み、";
var pattern = @"\p{IsKatakana}+(?=と)|(?<=と)\p{IsKatakana}+";
var matches = Regex.Matches(text, pattern);
Console.WriteLine(String.Join(",", matches.Cast<Match>().Select(x => x.Value)));
カタカナ語の前に「と」がくるパターンと、「と」の後にカタカナ語がくるパターンを取り出しています。
結果は、以下のようになります。
ドコドンドン,ドンタカタ,パラパラ,パンパン,パンパカパン
うまく行っているようです。
さいごに
もし「ゼロ幅の肯定先読みアサーションの例ならば、もっと良いのがあるよ」という方はコメントしていただけると嬉しいです。
あるいは上記問題を解くのに「こんな解法もあるよ」と別解を知っている方がいれば、コメントでお教えいただけるとありがたいです。