次の方法で共有


.NET での正規表現のベスト プラクティス

.NET の正規表現エンジンは、リテラル テキストの比較と照合ではなく、パターンの一致に基づいてテキストを処理する、強力なフル機能を備えたツールです。 ほとんどの場合、パターン マッチングは迅速かつ効率的に実行されます。 ただし、正規表現エンジンが遅いように見える場合があります。 極端なケースでは、数時間または数日にわたって比較的小さな入力を処理するため、応答を停止するように見える場合もあります。

この記事では、正規表現が最適なパフォーマンスを実現するために開発者が採用できるベスト プラクティスの一部について説明します。

Warnung

System.Text.RegularExpressions を使用して信頼できない入力を処理するときは、タイムアウトを渡します。 悪意のあるユーザーが RegularExpressionsに入力を提供し、 サービス拒否攻撃を引き起こす可能性があります。 RegularExpressions を使用する ASP.NET Core フレームワーク API は、タイムアウトを渡します。

入力ソースについて考える

一般に、正規表現は、制約付きまたは制約なしの 2 種類の入力を受け取ることができます。 制約付き入力は、既知または信頼できるソースから生成され、定義済みの形式に従うテキストです。 制約のない入力は、Web ユーザーなどの信頼性の低いソースから生成されたテキストであり、定義済みまたは予期された形式に従わない可能性があります。

正規表現パターンは、多くの場合、有効な入力に一致するように記述されます。 つまり、開発者は一致するテキストを調べ、それに一致する正規表現パターンを記述します。 その後、開発者は、複数の有効な入力項目を使用してテストすることで、このパターンに修正が必要か、さらに詳しく調べる必要があるかを判断します。 パターンが推定されたすべての有効な入力と一致する場合、そのパターンは実稼働可能であると宣言され、リリースされたアプリケーションに含めることができます。 この方法により、正規表現パターンは制約付き入力の照合に適しています。 ただし、制約のない入力の照合には適していません。

制約のない入力と一致させるには、正規表現で 3 種類のテキストを効率的に処理する必要があります。

  • 正規表現パターンに一致するテキスト。
  • 正規表現パターンと一致しないテキスト。
  • 正規表現パターンとほぼ一致するテキスト。

最後のテキスト型は、制約付き入力を処理するために書き込まれた正規表現では特に問題になります。 その正規表現が広範な バックトラッキングにも依存している場合、正規表現エンジンは、一見無害なテキストの処理 (場合によっては数時間または数日) の膨大な時間を費やすことができます。

Warnung

次の例では、過度のバックトラッキングが発生しやすく、有効なメール アドレスを拒否する可能性がある正規表現を使用しています。 電子メール検証ルーチンでは使用しないでください。 電子メール アドレスを検証する正規表現を使用する場合は、「 方法: 文字列が有効な電子メール形式であることを確認する」を参照してください。

たとえば、電子メール アドレスのエイリアスを検証するために一般的に使用されるが問題のある正規表現を考えてみましょう。 正規表現 ^[0-9A-Z]([-.\w]*[0-9A-Z])*$ は、有効な電子メール アドレスと見なされるものを処理するために記述されます。 有効なメール アドレスは、英数字で構成され、その後に英数字、ピリオド、またはハイフンを使用できる 0 個以上の文字が続きます。 正規表現は英数字で終わる必要があります。 ただし、次の例に示すように、この正規表現は有効な入力を簡単に処理しますが、ほぼ有効な入力を処理する場合、パフォーマンスは非効率的です。

using System;
using System.Diagnostics;
using System.Text.RegularExpressions;

public class DesignExample
{
    public static void Main()
    {
        Stopwatch sw;
        string[] addresses = { "AAAAAAAAAAA@contoso.com",
                             "AAAAAAAAAAaaaaaaaaaa!@contoso.com" };
        // The following regular expression should not actually be used to
        // validate an email address.
        string pattern = @"^[0-9A-Z]([-.\w]*[0-9A-Z])*$";
        string input;

        foreach (var address in addresses)
        {
            string mailBox = address.Substring(0, address.IndexOf("@"));
            int index = 0;
            for (int ctr = mailBox.Length - 1; ctr >= 0; ctr--)
            {
                index++;

                input = mailBox.Substring(ctr, index);
                sw = Stopwatch.StartNew();
                Match m = Regex.Match(input, pattern, RegexOptions.IgnoreCase);
                sw.Stop();
                if (m.Success)
                    Console.WriteLine("{0,2}. Matched '{1,25}' in {2}",
                                      index, m.Value, sw.Elapsed);
                else
                    Console.WriteLine("{0,2}. Failed  '{1,25}' in {2}",
                                      index, input, sw.Elapsed);
            }
            Console.WriteLine();
        }
    }
}

// The example displays output similar to the following:
//     1. Matched '                        A' in 00:00:00.0007122
//     2. Matched '                       AA' in 00:00:00.0000282
//     3. Matched '                      AAA' in 00:00:00.0000042
//     4. Matched '                     AAAA' in 00:00:00.0000038
//     5. Matched '                    AAAAA' in 00:00:00.0000042
//     6. Matched '                   AAAAAA' in 00:00:00.0000042
//     7. Matched '                  AAAAAAA' in 00:00:00.0000042
//     8. Matched '                 AAAAAAAA' in 00:00:00.0000087
//     9. Matched '                AAAAAAAAA' in 00:00:00.0000045
//    10. Matched '               AAAAAAAAAA' in 00:00:00.0000045
//    11. Matched '              AAAAAAAAAAA' in 00:00:00.0000045
//
//     1. Failed  '                        !' in 00:00:00.0000447
//     2. Failed  '                       a!' in 00:00:00.0000071
//     3. Failed  '                      aa!' in 00:00:00.0000071
//     4. Failed  '                     aaa!' in 00:00:00.0000061
//     5. Failed  '                    aaaa!' in 00:00:00.0000081
//     6. Failed  '                   aaaaa!' in 00:00:00.0000126
//     7. Failed  '                  aaaaaa!' in 00:00:00.0000359
//     8. Failed  '                 aaaaaaa!' in 00:00:00.0000414
//     9. Failed  '                aaaaaaaa!' in 00:00:00.0000758
//    10. Failed  '               aaaaaaaaa!' in 00:00:00.0001462
//    11. Failed  '              aaaaaaaaaa!' in 00:00:00.0002885
//    12. Failed  '             Aaaaaaaaaaa!' in 00:00:00.0005780
//    13. Failed  '            AAaaaaaaaaaa!' in 00:00:00.0011628
//    14. Failed  '           AAAaaaaaaaaaa!' in 00:00:00.0022851
//    15. Failed  '          AAAAaaaaaaaaaa!' in 00:00:00.0045864
//    16. Failed  '         AAAAAaaaaaaaaaa!' in 00:00:00.0093168
//    17. Failed  '        AAAAAAaaaaaaaaaa!' in 00:00:00.0185993
//    18. Failed  '       AAAAAAAaaaaaaaaaa!' in 00:00:00.0366723
//    19. Failed  '      AAAAAAAAaaaaaaaaaa!' in 00:00:00.1370108
//    20. Failed  '     AAAAAAAAAaaaaaaaaaa!' in 00:00:00.1553966
//    21. Failed  '    AAAAAAAAAAaaaaaaaaaa!' in 00:00:00.3223372
Imports System.Diagnostics
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim sw As Stopwatch
        Dim addresses() As String = {"AAAAAAAAAAA@contoso.com",
                                   "AAAAAAAAAAaaaaaaaaaa!@contoso.com"}
        ' The following regular expression should not actually be used to 
        ' validate an email address.
        Dim pattern As String = "^[0-9A-Z]([-.\w]*[0-9A-Z])*$"
        Dim input As String

        For Each address In addresses
            Dim mailBox As String = address.Substring(0, address.IndexOf("@"))
            Dim index As Integer = 0
            For ctr As Integer = mailBox.Length - 1 To 0 Step -1
                index += 1
                input = mailBox.Substring(ctr, index)
                sw = Stopwatch.StartNew()
                Dim m As Match = Regex.Match(input, pattern, RegexOptions.IgnoreCase)
                sw.Stop()
                if m.Success Then
                    Console.WriteLine("{0,2}. Matched '{1,25}' in {2}",
                                      index, m.Value, sw.Elapsed)
                Else
                    Console.WriteLine("{0,2}. Failed  '{1,25}' in {2}",
                                      index, input, sw.Elapsed)
                End If
            Next
            Console.WriteLine()
        Next
    End Sub
End Module
' The example displays output similar to the following:
'     1. Matched '                        A' in 00:00:00.0007122
'     2. Matched '                       AA' in 00:00:00.0000282
'     3. Matched '                      AAA' in 00:00:00.0000042
'     4. Matched '                     AAAA' in 00:00:00.0000038
'     5. Matched '                    AAAAA' in 00:00:00.0000042
'     6. Matched '                   AAAAAA' in 00:00:00.0000042
'     7. Matched '                  AAAAAAA' in 00:00:00.0000042
'     8. Matched '                 AAAAAAAA' in 00:00:00.0000087
'     9. Matched '                AAAAAAAAA' in 00:00:00.0000045
'    10. Matched '               AAAAAAAAAA' in 00:00:00.0000045
'    11. Matched '              AAAAAAAAAAA' in 00:00:00.0000045
'    
'     1. Failed  '                        !' in 00:00:00.0000447
'     2. Failed  '                       a!' in 00:00:00.0000071
'     3. Failed  '                      aa!' in 00:00:00.0000071
'     4. Failed  '                     aaa!' in 00:00:00.0000061
'     5. Failed  '                    aaaa!' in 00:00:00.0000081
'     6. Failed  '                   aaaaa!' in 00:00:00.0000126
'     7. Failed  '                  aaaaaa!' in 00:00:00.0000359
'     8. Failed  '                 aaaaaaa!' in 00:00:00.0000414
'     9. Failed  '                aaaaaaaa!' in 00:00:00.0000758
'    10. Failed  '               aaaaaaaaa!' in 00:00:00.0001462
'    11. Failed  '              aaaaaaaaaa!' in 00:00:00.0002885
'    12. Failed  '             Aaaaaaaaaaa!' in 00:00:00.0005780
'    13. Failed  '            AAaaaaaaaaaa!' in 00:00:00.0011628
'    14. Failed  '           AAAaaaaaaaaaa!' in 00:00:00.0022851
'    15. Failed  '          AAAAaaaaaaaaaa!' in 00:00:00.0045864
'    16. Failed  '         AAAAAaaaaaaaaaa!' in 00:00:00.0093168
'    17. Failed  '        AAAAAAaaaaaaaaaa!' in 00:00:00.0185993
'    18. Failed  '       AAAAAAAaaaaaaaaaa!' in 00:00:00.0366723
'    19. Failed  '      AAAAAAAAaaaaaaaaaa!' in 00:00:00.1370108
'    20. Failed  '     AAAAAAAAAaaaaaaaaaa!' in 00:00:00.1553966
'    21. Failed  '    AAAAAAAAAAaaaaaaaaaa!' in 00:00:00.3223372

前の例の出力に示すように、正規表現エンジンは、長さに関係なく、有効な電子メール エイリアスを約同じ時間間隔で処理します。 一方、ほぼ有効な電子メール アドレスの文字数が 5 文字を超えると、文字列内の余分な文字ごとに処理時間が約 2 倍になります。 したがって、ほぼ有効な 28 文字の文字列の処理には 1 時間以上かかっており、ほぼ有効な 33 文字の文字列の処理には 1 日近くかかります。

この正規表現は一致する入力の形式のみを考慮して開発されたため、パターンに一致しない入力を考慮できません。 この監視により、正規表現パターンとほぼ一致する制約のない入力によって、パフォーマンスが大幅に低下する可能性があります。

この問題を解決するには、次の操作を行います。

  • パターンを開発するときは、特に正規表現が制約のない入力を処理するように設計されている場合に、バックトラッキングが正規表現エンジンのパフォーマンスに与える影響を考慮する必要があります。 詳細については、「 バックトラッキングを担当する 」セクションを参照してください。

  • 無効、ほぼ有効、有効な入力を使用して、正規表現を十分にテストします。 Rex を使用して、特定の正規表現の入力をランダムに生成できます。 Rex は、Microsoft Research の正規表現探索ツールです。

オブジェクトのインスタンス化を適切に処理する

の中心に.NET の正規表現オブジェクト モデルは、正規表現エンジンを表す System.Text.RegularExpressions.Regex クラスです。 多くの場合、正規表現のパフォーマンスに影響する最大の要因は、 Regex エンジンの使用方法です。 正規表現を定義するには、正規表現エンジンと正規表現パターンを厳密に結合する必要があります。 その結合プロセスは、コンストラクターに正規表現パターンを渡して Regex オブジェクトをインスタンス化するか、または正規表現パターンと分析対象の文字列を渡して静的メソッドを呼び出すかに関係なく、コストがかかります。

解釈およびコンパイルされた正規表現の使用によるパフォーマンスへの影響の詳細については、ブログ記事「 正規表現のパフォーマンスの最適化」パート II: バックトラッキングを担当しています

正規表現エンジンと特定の正規表現パターンを組み合わせてから、エンジンを使用していくつかの方法でテキストと一致させることができます。

  • Regex.Match(String, String)などの静的パターン マッチング メソッドを呼び出すことができます。 このメソッドでは、正規表現オブジェクトのインスタンス化は必要ありません。

  • Regex オブジェクトをインスタンス化し、解釈された正規表現のインスタンス パターン マッチング メソッドを呼び出すことができます。これは、正規表現エンジンを正規表現パターンにバインドするための既定のメソッドです。 Regex オブジェクトが、Compiled フラグを含むoptions引数なしでインスタンス化されると、その結果として発生します。

  • Regex オブジェクトをインスタンス化し、ソース生成正規表現のインスタンス パターンマッチング メソッドを呼び出すことができます。 ほとんどの場合、この手法をお勧めします。 これを行うには、Regexを返す部分メソッドにGeneratedRegexAttribute属性を配置します。

  • Regex オブジェクトをインスタンス化し、コンパイルされた正規表現のインスタンス パターン マッチング メソッドを呼び出すことができます。 正規表現オブジェクトは、Compiled フラグを含むoptions引数を使用してRegex オブジェクトがインスタンス化されるときにコンパイルされたパターンを表します。

正規表現照合メソッドを呼び出す特定の方法は、アプリケーションのパフォーマンスに影響する可能性があります。 以降のセクションでは、アプリケーションのパフォーマンスを向上させるために、静的メソッド呼び出し、ソース生成正規表現、解釈された正規表現、コンパイルされた正規表現を使用するタイミングについて説明します。

Von Bedeutung

メソッド呼び出しの形式 (静的、解釈、ソース生成、コンパイル済み) は、メソッド呼び出しで同じ正規表現が繰り返し使用される場合、またはアプリケーションが正規表現オブジェクトを広範に使用する場合、パフォーマンスに影響します。

静的正規表現

同じ正規表現を使用して正規表現オブジェクトを繰り返しインスタンス化する代わりに、静的正規表現メソッドを使用することをお勧めします。 正規表現オブジェクトで使用される正規表現パターンとは異なり、静的メソッド呼び出しで使用されるパターンの操作コード (オペコード) またはコンパイル済みの共通中間言語 (CIL) は、正規表現エンジンによって内部的にキャッシュされます。

たとえば、イベント ハンドラーは、ユーザー入力を検証するために別のメソッドを頻繁に呼び出します。 この例は、 Button コントロールの Click イベントを使用して IsValidCurrency という名前のメソッドを呼び出し、ユーザーが通貨記号の後に少なくとも 1 つの 10 進数を入力したかどうかを確認する次のコードに反映されています。

public void OKButton_Click(object sender, EventArgs e)
{
   if (! String.IsNullOrEmpty(sourceCurrency.Text))
      if (RegexLib.IsValidCurrency(sourceCurrency.Text))
         PerformConversion();
      else
         status.Text = "The source currency value is invalid.";
}
Public Sub OKButton_Click(sender As Object, e As EventArgs) _
           Handles OKButton.Click

    If Not String.IsNullOrEmpty(sourceCurrency.Text) Then
        If RegexLib.IsValidCurrency(sourceCurrency.Text) Then
            PerformConversion()
        Else
            status.Text = "The source currency value is invalid."
        End If
    End If
End Sub

次の例では、 IsValidCurrency メソッドの非効率的な実装を示します。

各メソッド呼び出しでは、同じパターンを持つ Regex オブジェクトが再び立ち上がります。 つまり、メソッドが呼び出されるたびに正規表現パターンを再コンパイルする必要があります。

using System;
using System.Text.RegularExpressions;

public class RegexLib
{
   public static bool IsValidCurrency(string currencyValue)
   {
      string pattern = @"\p{Sc}+\s*\d+";
      Regex currencyRegex = new Regex(pattern);
      return currencyRegex.IsMatch(currencyValue);
   }
}
Imports System.Text.RegularExpressions

Public Module RegexLib
    Public Function IsValidCurrency(currencyValue As String) As Boolean
        Dim pattern As String = "\p{Sc}+\s*\d+"
        Dim currencyRegex As New Regex(pattern)
        Return currencyRegex.IsMatch(currencyValue)
    End Function
End Module

上記の非効率的なコードは、静的な Regex.IsMatch(String, String) メソッドの呼び出しに置き換える必要があります。 この方法では、パターン マッチング メソッドを呼び出すたびに Regex オブジェクトをインスタンス化する必要がなくなり、正規表現エンジンは、そのキャッシュからコンパイル済みのバージョンの正規表現を取得できます。

using System;
using System.Text.RegularExpressions;

public class RegexLib2
{
   public static bool IsValidCurrency(string currencyValue)
   {
      string pattern = @"\p{Sc}+\s*\d+";
      return Regex.IsMatch(currencyValue, pattern);
   }
}
Imports System.Text.RegularExpressions

Public Module RegexLib
    Public Function IsValidCurrency(currencyValue As String) As Boolean
        Dim pattern As String = "\p{Sc}+\s*\d+"
        Return Regex.IsMatch(currencyValue, pattern)
    End Function
End Module

既定では、最後に使用された最後の 15 個の静的正規表現パターンがキャッシュされます。 キャッシュされた静的正規表現の数が多いアプリケーションの場合は、 Regex.CacheSize プロパティを設定してキャッシュのサイズを調整できます。

この例で使用する正規表現 \p{Sc}+\s*\d+ は、入力文字列に通貨記号と少なくとも 1 つの 10 進数があることを確認します。 パターンは、次の表に示すように定義されています。

パターン 説明
\p{Sc}+ Unicode 記号の通貨カテゴリの 1 つ以上の文字と一致します。
\s* 0 個以上の空白文字と一致します。
\d+ 1 個以上の 10 進数と一致します。

解釈された正規表現とソース生成された正規表現の比較

Compiled オプションの指定によって正規表現エンジンにバインドされていない正規表現パターンが解釈されます。 正規表現オブジェクトがインスタンス化されると、正規表現エンジンは正規表現を一連の操作コードに変換します。 インスタンス メソッドが呼び出されると、操作コードは CIL に変換され、JIT コンパイラによって実行されます。 同様に、静的正規表現メソッドが呼び出され、正規表現がキャッシュに見つからない場合、正規表現エンジンは正規表現を一連の操作コードに変換し、キャッシュに格納します。 次に、JIT コンパイラが実行できるように、これらの操作コードを CIL に変換します。 解釈された正規表現は、実行時間が遅くなるというコストで起動時間を短縮します。 このプロセスにより、正規表現が少数のメソッド呼び出しで使用される場合、または正規表現メソッドの呼び出しの正確な数が不明であるが、小さいと予想される場合に最適です。 メソッド呼び出しの数が増えると、起動時間の短縮によるパフォーマンスの向上は、実行速度の低下によって低下します。

Compiled オプションの指定によって正規表現エンジンにバインドされている正規表現パターンがコンパイルされます。 したがって、正規表現オブジェクトがインスタンス化されるとき、または静的正規表現メソッドが呼び出され、正規表現がキャッシュに見つからない場合、正規表現エンジンは正規表現を操作コードの中間セットに変換します。 これらのコードは CIL に変換されます。 メソッドが呼び出されると、JIT コンパイラによって CIL が実行されます。 解釈される正規表現とは対照的に、コンパイルされた正規表現では起動時間が長くなりますが、個々のパターン マッチング メソッドの実行速度が向上します。 その結果、正規表現のコンパイルによって得られるパフォーマンス上の利点は、呼び出される正規表現メソッドの数に比例して増加します。

GeneratedRegexAttribute属性を持つRegex戻りメソッドの装飾を介して正規表現エンジンにバインドされている正規表現パターンは、ソース生成されます。 コンパイラにプラグインされるソース ジェネレーターは、CIL で出力されるのと同様のロジックを持つカスタム Regex派生実装 RegexOptions.Compiled C# コードとして出力します。 スループット パフォーマンスに関するRegexOptions.Compiled のすべての利点と (実際にはそれ以上)、起動時の Regex.CompileToAssembly の利点を、CompileToAssembly の複雑さなしで得られます。 出力されるソースはプロジェクトの一部であるため、簡単に表示およびデバッグすることもできます。

要約すると、次のことをお勧めします。

  • 特定の正規表現を使用して正規表現メソッドを比較的頻繁に呼び出す場合は、 解釈された 正規表現を使用します。
  • コンパイル時に既知の引数を持つ C# でRegexを使用していて、特定の正規表現を比較的頻繁に使用している場合は、ソース生成正規表現を使用します。
  • 特定の正規表現で正規表現メソッドを比較的頻繁に呼び出し、.NET 6 以前のバージョンを使用している場合は、 コンパイルされた 正規表現を使用します。

解釈された正規表現の実行速度が、起動時間の短縮による利益を上回る正確なしきい値を特定することは困難です。 また、ソース生成またはコンパイルされた正規表現の起動時間が、より高速な実行速度の向上を上回るしきい値を特定することも困難です。 しきい値は、正規表現の複雑さと処理する特定のデータなど、さまざまな要因によって異なります。 特定のアプリケーション シナリオに最適なパフォーマンスを提供する正規表現を決定するには、 Stopwatch クラスを使用して実行時間を比較します。

次の例では、最初の 10 個の文を読み取るとき、および William D. Guthrie の マグナ カルタとその他のアドレスのテキスト内のすべての文を読み取るときに、コンパイル、ソース生成、および解釈された正規表現のパフォーマンスを比較します。 この例の出力に示すように、正規表現の一致メソッドに対して呼び出しが 10 回だけ行われると、解釈された正規表現またはソースによって生成された正規表現は、コンパイルされた正規表現よりも優れたパフォーマンスを提供します。 ただし、コンパイルされた正規表現では、多数の呼び出し (この場合は 13,000 を超える) が行われると、パフォーマンスが向上します。

const string Pattern = @"\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]";

static readonly HttpClient s_client = new();

[GeneratedRegex(Pattern, RegexOptions.Singleline)]
private static partial Regex GeneratedRegex();

public async static Task RunIt()
{
    Stopwatch sw;
    Match match;
    int ctr;

    string text =
            await s_client.GetStringAsync("https://www.gutenberg.org/cache/epub/64197/pg64197.txt");

    // Read first ten sentences with interpreted regex.
    Console.WriteLine("10 Sentences with Interpreted Regex:");
    sw = Stopwatch.StartNew();
    Regex int10 = new(Pattern, RegexOptions.Singleline);
    match = int10.Match(text);
    for (ctr = 0; ctr <= 9; ctr++)
    {
        if (match.Success)
            // Do nothing with the match except get the next match.
            match = match.NextMatch();
        else
            break;
    }
    sw.Stop();
    Console.WriteLine($"   {ctr} matches in {sw.Elapsed}");

    // Read first ten sentences with compiled regex.
    Console.WriteLine("10 Sentences with Compiled Regex:");
    sw = Stopwatch.StartNew();
    Regex comp10 = new Regex(Pattern,
                 RegexOptions.Singleline | RegexOptions.Compiled);
    match = comp10.Match(text);
    for (ctr = 0; ctr <= 9; ctr++)
    {
        if (match.Success)
            // Do nothing with the match except get the next match.
            match = match.NextMatch();
        else
            break;
    }
    sw.Stop();
    Console.WriteLine($"   {ctr} matches in {sw.Elapsed}");

    // Read first ten sentences with source-generated regex.
    Console.WriteLine("10 Sentences with Source-generated Regex:");
    sw = Stopwatch.StartNew();

    match = GeneratedRegex().Match(text);
    for (ctr = 0; ctr <= 9; ctr++)
    {
        if (match.Success)
            // Do nothing with the match except get the next match.
            match = match.NextMatch();
        else
            break;
    }
    sw.Stop();
    Console.WriteLine($"   {ctr} matches in {sw.Elapsed}");

    // Read all sentences with interpreted regex.
    Console.WriteLine("All Sentences with Interpreted Regex:");
    sw = Stopwatch.StartNew();
    Regex intAll = new(Pattern, RegexOptions.Singleline);
    match = intAll.Match(text);
    int matches = 0;
    while (match.Success)
    {
        matches++;
        // Do nothing with the match except get the next match.
        match = match.NextMatch();
    }
    sw.Stop();
    Console.WriteLine($"   {matches:N0} matches in {sw.Elapsed}");

    // Read all sentences with compiled regex.
    Console.WriteLine("All Sentences with Compiled Regex:");
    sw = Stopwatch.StartNew();
    Regex compAll = new(Pattern,
                    RegexOptions.Singleline | RegexOptions.Compiled);
    match = compAll.Match(text);
    matches = 0;
    while (match.Success)
    {
        matches++;
        // Do nothing with the match except get the next match.
        match = match.NextMatch();
    }
    sw.Stop();
    Console.WriteLine($"   {matches:N0} matches in {sw.Elapsed}");

    // Read all sentences with source-generated regex.
    Console.WriteLine("All Sentences with Source-generated Regex:");
    sw = Stopwatch.StartNew();
    match = GeneratedRegex().Match(text);
    matches = 0;
    while (match.Success)
    {
        matches++;
        // Do nothing with the match except get the next match.
        match = match.NextMatch();
    }
    sw.Stop();
    Console.WriteLine($"   {matches:N0} matches in {sw.Elapsed}");

    return;
}
/* The example displays output similar to the following:

   10 Sentences with Interpreted Regex:
       10 matches in 00:00:00.0104920
   10 Sentences with Compiled Regex:
       10 matches in 00:00:00.0234604
   10 Sentences with Source-generated Regex:
       10 matches in 00:00:00.0060982
   All Sentences with Interpreted Regex:
       3,427 matches in 00:00:00.1745455
   All Sentences with Compiled Regex:
       3,427 matches in 00:00:00.0575488
   All Sentences with Source-generated Regex:
       3,427 matches in 00:00:00.2698670
*/

この例で使用する正規表現パターン ( \b(\w+((\r?\n)|,?\s))*\w+[.?:;!]) は、次の表に示すように定義されています。

パターン 説明
\b ワード境界から照合を開始します。
\w+ 1 つ以上の単語文字に一致します。
(\r?\n)|,?\s) 0 または 1 のキャリッジ リターンの後に改行文字、または 0 または 1 つのコンマの後に空白文字が続く文字列と一致します。
(\w+((\r?\n)|,?\s))* 0 個以上の単語文字の後に、0 個または 1 個の復帰文字と改行文字、または 0 個または 1 個のコンマの後に空白文字が続く、0 個以上の出現箇所と一致します。
\w+ 1 つ以上の単語文字に一致します。
[.?:;!] ピリオド、疑問符、コロン、セミコロン、感嘆符に一致します。

バックトラッキングを担当する

通常、正規表現エンジンは線形進行を使用して入力文字列内を移動し、正規表現パターンと比較します。 ただし、 *+? などの不確定な量指定子が正規表現パターンで使用されている場合、正規表現エンジンは、部分的な一致が成功した部分の一部を放棄し、パターン全体の一致を検索するために以前に保存された状態に戻る可能性があります。 このプロセスはバックトラッキングと呼ばれます。

ヒント

バックトラッキングの詳細については、「 正規表現の動作バックトラッキングの詳細」を参照してください。 バックトラッキングの詳細については、 .NET 7 の正規表現の改善正規表現パフォーマンスの最適化に関する ブログ記事を参照してください。

バックトラッキングのサポートにより、正規表現のパワーと柔軟性が得られます。 また、正規表現エンジンの操作を正規表現開発者の手で制御する責任も負います。 開発者は多くの場合、この責任を認識していないため、バックトラッキングの誤用や過剰なバックトラッキングへの依存は、正規表現のパフォーマンスを低下させる上で最も重要な役割を果たします。 最悪のシナリオでは、入力文字列の追加文字ごとに実行時間が 2 倍になることがあります。 実際、バックトラッキングを過剰に使用することで、入力が正規表現パターンとほぼ一致する場合、プログラムで無限ループに相当するループを簡単に作成できます。 正規表現エンジンでは、比較的短い入力文字列を処理するのに数時間から数日かかる場合があります。

多くの場合、バックトラッキングはマッチに不可欠ではありませんが、バックトラッキングを使用した場合、アプリケーションはパフォーマンスの低下を支払います。 たとえば、正規表現 \b\p{Lu}\w*\b は、次の表に示すように、大文字で始まるすべての単語と一致します。

パターン 説明
\b ワード境界から照合を開始します。
\p{Lu} 大文字と一致します。
\w* 0 個以上の単語文字と一致します。
\b ワード境界で照合を終了します。

単語の境界は単語文字と同じでもサブセットでもないので、単語の文字を照合するときに正規表現エンジンが単語の境界を越える可能性はありません。 したがって、この正規表現では、バックトラッキングが一致の全体的な成功に影響を与えることはありません。 正規表現エンジンは、単語文字の事前一致が成功するたびに状態を保存する必要があるため、パフォーマンスが低下する可能性があります。

バックトラッキングが必要ないと判断した場合は、いくつかの方法で無効にすることができます。

  • RegexOptions.NonBacktracking オプションを設定します (.NET 7 で導入)。 詳細については、「 非バックトラッキング モード」を参照してください。

  • アトミック グループと呼ばれる (?>subexpression) 言語要素を使用する。 次の例では、2 つの正規表現を使用して入力文字列を解析します。 1 つ目の \b\p{Lu}\w*\bはバックトラッキングに依存します。 2 つ目の \b\p{Lu}(?>\w*)\bでは、バックトラッキングが無効になります。 この例の出力に示すように、両方とも同じ結果が生成されます。

    using System;
    using System.Text.RegularExpressions;
    
    public class BackTrack2Example
    {
        public static void Main()
        {
            string input = "This this word Sentence name Capital";
            string pattern = @"\b\p{Lu}\w*\b";
            foreach (Match match in Regex.Matches(input, pattern))
                Console.WriteLine(match.Value);
    
            Console.WriteLine();
    
            pattern = @"\b\p{Lu}(?>\w*)\b";
            foreach (Match match in Regex.Matches(input, pattern))
                Console.WriteLine(match.Value);
        }
    }
    // The example displays the following output:
    //       This
    //       Sentence
    //       Capital
    //
    //       This
    //       Sentence
    //       Capital
    
    Imports System.Text.RegularExpressions
    
    Module Example
        Public Sub Main()
            Dim input As String = "This this word Sentence name Capital"
            Dim pattern As String = "\b\p{Lu}\w*\b"
            For Each match As Match In Regex.Matches(input, pattern)
                Console.WriteLine(match.Value)
            Next
            Console.WriteLine()
    
            pattern = "\b\p{Lu}(?>\w*)\b"
            For Each match As Match In Regex.Matches(input, pattern)
                Console.WriteLine(match.Value)
            Next
        End Sub
    End Module
    ' The example displays the following output:
    '       This
    '       Sentence
    '       Capital
    '       
    '       This
    '       Sentence
    '       Capital
    

多くの場合、バックトラッキングは正規表現パターンと入力テキストの照合に不可欠です。 ただし、過剰なバックトラッキングによってパフォーマンスが大幅に低下し、アプリケーションが応答を停止したという印象が生じる可能性があります。 特に、この問題は、量指定子が入れ子になり、外側の部分式に一致するテキストが内部部分式と一致するテキストのサブセットである場合に発生します。

Warnung

過剰なバックトラッキングを回避するだけでなく、タイムアウト機能を使用して、過剰なバックトラッキングによって正規表現のパフォーマンスが著しく低下しないようにする必要があります。 詳細については、「 タイムアウト値の使用 」セクションを参照してください。

たとえば、正規表現パターン ^[0-9A-Z]([-.\w]*[0-9A-Z])*\$$ は、少なくとも 1 つの英数字で構成される部品番号と一致することを目的としています。 追加の文字は英数字、ハイフン、アンダースコア、ピリオドで構成できますが、最後の文字は英数字である必要があります。 ドル記号は部品番号を終了します。 場合によっては、量指定子が入れ子になっているため、および部分式 [0-9A-Z] が部分式 [-.\w]*のサブセットであるため、この正規表現パターンのパフォーマンスが低下することがあります。

このような場合は、入れ子になった量指定子を削除し、外側の部分式をゼロ幅の先読みまたは後読みアサーションに置き換えることで、正規表現のパフォーマンスを最適化できます。 先読みアサーションと後読みアサーションはアンカーです。 入力文字列内のポインターは移動せず、代わりに、指定された条件が満たされているかどうかを確認するために前または後ろを見てください。 たとえば、部品番号の正規表現を ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$として書き換えることができます。 この正規表現パターンは、次の表に示すように定義されています。

パターン 説明
^ 入力文字列の先頭から照合を開始します。
[0-9A-Z] 英数字と一致します。 部品番号は、少なくともこの文字で構成されている必要があります。
[-.\w]* 任意の単語文字、ハイフン、またはピリオドの 0 回以上の出現に一致します。
\$ ドル記号と一致します。
(?<=[0-9A-Z]) 末尾のドル記号の後ろを見て、前の文字が英数字であることを確認します。
$ 入力文字列の末尾で一致を終了します。

次の例は、この正規表現を使用して、使用可能な部品番号を含む配列と一致させる方法を示しています。

using System;
using System.Text.RegularExpressions;

public class BackTrack4Example
{
    public static void Main()
    {
        string pattern = @"^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$";
        string[] partNos = { "A1C$", "A4", "A4$", "A1603D$", "A1603D#" };

        foreach (var input in partNos)
        {
            Match match = Regex.Match(input, pattern);
            if (match.Success)
                Console.WriteLine(match.Value);
            else
                Console.WriteLine("Match not found.");
        }
    }
}
// The example displays the following output:
//       A1C$
//       Match not found.
//       A4$
//       A1603D$
//       Match not found.
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim pattern As String = "^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$"
        Dim partNos() As String = {"A1C$", "A4", "A4$", "A1603D$",
                                    "A1603D#"}

        For Each input As String In partNos
            Dim match As Match = Regex.Match(input, pattern)
            If match.Success Then
                Console.WriteLine(match.Value)
            Else
                Console.WriteLine("Match not found.")
            End If
        Next
    End Sub
End Module
' The example displays the following output:
'       A1C$
'       Match not found.
'       A4$
'       A1603D$
'       Match not found.

.NET の正規表現言語には、入れ子になった量指定子を排除するために使用できる次の言語要素が含まれています。 詳細については、「 グループ化コンストラクト」を参照してください。

Language 要素 説明
(?= subexpression ) ゼロ幅の正の先読み。 現在の位置を先に見て、 subexpression が入力文字列と一致するかどうかを判断します。
(?! subexpression ) ゼロ幅の負の先読み。 現在の位置を先に見て、 subexpression が入力文字列と一致しないかどうかを判断します。
(?<= subexpression ) ゼロ幅の正の後読み。 現在の位置の背後を調べて、 subexpression が入力文字列と一致するかどうかを判断します。
(?<! subexpression ) ゼロ幅の負の後読み。 現在の位置の背後を調べて、 subexpression が入力文字列と一致しないかどうかを判断します。

タイムアウト値を使用する

正規表現が正規表現パターンとほぼ一致する入力を処理する場合、多くの場合、過剰なバックトラッキングに依存する可能性があり、パフォーマンスに大きな影響を与えます。 バックトラッキングの使用を慎重に検討し、ほぼ一致する入力に対して正規表現をテストすることに加えて、過剰なバックトラッキングが発生した場合の影響を最小限に抑えるために、タイムアウト値を常に設定する必要があります。

正規表現のタイムアウト間隔は、正規表現エンジンがタイムアウトする前に 1 つの一致を検索する期間を定義します。正規表現パターンと入力テキストによっては、実行時間が指定されたタイムアウト間隔を超える可能性がありますが、指定されたタイムアウト間隔よりもバックトラッキングに時間が費やされることはありません。 既定のタイムアウト間隔は Regex.InfiniteMatchTimeout。つまり、正規表現はタイムアウトしません。この値をオーバーライドし、タイムアウト間隔を次のように定義できます。

タイムアウト間隔を定義していて、その間隔の最後に一致するものが見つからない場合、正規表現メソッドは RegexMatchTimeoutException 例外をスローします。 例外ハンドラーでは、より長いタイムアウト間隔で一致を再試行するか、一致の試行を破棄して一致しないと仮定するか、一致の試行を破棄して、将来の分析のために例外情報をログに記録することを選択できます。

次の例では、350 ミリ秒のタイムアウト間隔で正規表現をインスタンス化し、テキスト ドキュメント内の単語の単語数と平均文字数を計算する GetWordData メソッドを定義します。 一致する操作がタイムアウトすると、タイムアウト間隔が 350 ミリ秒増加し、 Regex オブジェクトが再び作成されます。 新しいタイムアウト間隔が 1 秒を超えた場合、メソッドは例外を呼び出し元に再スローします。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

public class TimeoutExample
{
    public static void Main()
    {
        RegexUtilities util = new RegexUtilities();
        string title = "Doyle - The Hound of the Baskervilles.txt";
        try
        {
            var info = util.GetWordData(title);
            Console.WriteLine($"Words:               {info.Item1:N0}");
            Console.WriteLine($"Average Word Length: {info.Item2:N2} characters");
        }
        catch (IOException e)
        {
            Console.WriteLine($"IOException reading file '{title}'");
            Console.WriteLine(e.Message);
        }
        catch (RegexMatchTimeoutException e)
        {
            Console.WriteLine($"The operation timed out after {e.MatchTimeout.TotalMilliseconds:N0} milliseconds");
        }
    }
}

public class RegexUtilities
{
    public Tuple<int, double> GetWordData(string filename)
    {
        const int MAX_TIMEOUT = 1000;   // Maximum timeout interval in milliseconds.
        const int INCREMENT = 350;      // Milliseconds increment of timeout.

        List<string> exclusions = new List<string>(new string[] { "a", "an", "the" });
        int[] wordLengths = new int[29];        // Allocate an array of more than ample size.
        string input = null;
        StreamReader sr = null;
        try
        {
            sr = new StreamReader(filename);
            input = sr.ReadToEnd();
        }
        catch (FileNotFoundException e)
        {
            string msg = String.Format("Unable to find the file '{0}'", filename);
            throw new IOException(msg, e);
        }
        catch (IOException e)
        {
            throw new IOException(e.Message, e);
        }
        finally
        {
            if (sr != null) sr.Close();
        }

        int timeoutInterval = INCREMENT;
        bool init = false;
        Regex rgx = null;
        Match m = null;
        int indexPos = 0;
        do
        {
            try
            {
                if (!init)
                {
                    rgx = new Regex(@"\b\w+\b", RegexOptions.None,
                                    TimeSpan.FromMilliseconds(timeoutInterval));
                    m = rgx.Match(input, indexPos);
                    init = true;
                }
                else
                {
                    m = m.NextMatch();
                }
                if (m.Success)
                {
                    if (!exclusions.Contains(m.Value.ToLower()))
                        wordLengths[m.Value.Length]++;

                    indexPos += m.Length + 1;
                }
            }
            catch (RegexMatchTimeoutException e)
            {
                if (e.MatchTimeout.TotalMilliseconds < MAX_TIMEOUT)
                {
                    timeoutInterval += INCREMENT;
                    init = false;
                }
                else
                {
                    // Rethrow the exception.
                    throw;
                }
            }
        } while (m.Success);

        // If regex completed successfully, calculate number of words and average length.
        int nWords = 0;
        long totalLength = 0;

        for (int ctr = wordLengths.GetLowerBound(0); ctr <= wordLengths.GetUpperBound(0); ctr++)
        {
            nWords += wordLengths[ctr];
            totalLength += ctr * wordLengths[ctr];
        }
        return new Tuple<int, double>(nWords, totalLength / nWords);
    }
}
Imports System.Collections.Generic
Imports System.IO
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim util As New RegexUtilities()
        Dim title As String = "Doyle - The Hound of the Baskervilles.txt"
        Try
            Dim info = util.GetWordData(title)
            Console.WriteLine("Words:               {0:N0}", info.Item1)
            Console.WriteLine("Average Word Length: {0:N2} characters", info.Item2)
        Catch e As IOException
            Console.WriteLine("IOException reading file '{0}'", title)
            Console.WriteLine(e.Message)
        Catch e As RegexMatchTimeoutException
            Console.WriteLine("The operation timed out after {0:N0} milliseconds",
                              e.MatchTimeout.TotalMilliseconds)
        End Try
    End Sub
End Module

Public Class RegexUtilities
    Public Function GetWordData(filename As String) As Tuple(Of Integer, Double)
        Const MAX_TIMEOUT As Integer = 1000  ' Maximum timeout interval in milliseconds.
        Const INCREMENT As Integer = 350     ' Milliseconds increment of timeout.

        Dim exclusions As New List(Of String)({"a", "an", "the"})
        Dim wordLengths(30) As Integer        ' Allocate an array of more than ample size.
        Dim input As String = Nothing
        Dim sr As StreamReader = Nothing
        Try
            sr = New StreamReader(filename)
            input = sr.ReadToEnd()
        Catch e As FileNotFoundException
            Dim msg As String = String.Format("Unable to find the file '{0}'", filename)
            Throw New IOException(msg, e)
        Catch e As IOException
            Throw New IOException(e.Message, e)
        Finally
            If sr IsNot Nothing Then sr.Close()
        End Try

        Dim timeoutInterval As Integer = INCREMENT
        Dim init As Boolean = False
        Dim rgx As Regex = Nothing
        Dim m As Match = Nothing
        Dim indexPos As Integer = 0
        Do
            Try
                If Not init Then
                    rgx = New Regex("\b\w+\b", RegexOptions.None,
                                    TimeSpan.FromMilliseconds(timeoutInterval))
                    m = rgx.Match(input, indexPos)
                    init = True
                Else
                    m = m.NextMatch()
                End If
                If m.Success Then
                    If Not exclusions.Contains(m.Value.ToLower()) Then
                        wordLengths(m.Value.Length) += 1
                    End If
                    indexPos += m.Length + 1
                End If
            Catch e As RegexMatchTimeoutException
                If e.MatchTimeout.TotalMilliseconds < MAX_TIMEOUT Then
                    timeoutInterval += INCREMENT
                    init = False
                Else
                    ' Rethrow the exception.
                    Throw
                End If
            End Try
        Loop While m.Success

        ' If regex completed successfully, calculate number of words and average length.
        Dim nWords As Integer
        Dim totalLength As Long

        For ctr As Integer = wordLengths.GetLowerBound(0) To wordLengths.GetUpperBound(0)
            nWords += wordLengths(ctr)
            totalLength += ctr * wordLengths(ctr)
        Next
        Return New Tuple(Of Integer, Double)(nWords, totalLength / nWords)
    End Function
End Class

必要な場合にのみキャプチャする

.NET の正規表現は、正規表現パターンを 1 つ以上の部分式にグループ化できるグループ化コンストラクトをサポートします。 .NET 正規表現言語で最もよく使用されるグループ化コンストラクトは、番号付きキャプチャ グループを定義する (subexpression) と、名前付きキャプチャ グループを定義する (?<name>subexpression) です。 グループ化コンストラクトは、バックリファレンスを作成したり、量指定子を適用する部分式を定義したりするために不可欠です。

ただし、これらの言語要素の使用にはコストがかかります。 これにより、Match.Groups プロパティによって返されたGroupCollection オブジェクトに、最新の名前のないキャプチャまたは名前付きキャプチャが設定されます。 1 つのグループ化コンストラクトが入力文字列内の複数の部分文字列をキャプチャした場合、特定のキャプチャ グループのGroup.Captures プロパティによって返されるCaptureCollection オブジェクトにも、複数のCapture オブジェクトが設定されます。

多くの場合、グループ化コンストラクトは、量指定子を適用できるように正規表現でのみ使用されます。 これらの部分式によってキャプチャされたグループは、後で使用されません。 たとえば、正規表現 \b(\w+[;,]?\s?)+[.?!] は、文全体をキャプチャするように設計されています。 次の表では、この正規表現パターンの言語要素と、 Match オブジェクトの Match.Groups コレクションと Group.Captures コレクションへの影響について説明します。

パターン 説明
\b ワード境界から照合を開始します。
\w+ 1 つ以上の単語文字に一致します。
[;,]? 0 または 1 のコンマまたはセミコロンと一致します。
\s? 0 文字または 1 個の空白文字と一致します。
(\w+[;,]?\s?)+ 1 つ以上の単語文字の後に、省略可能なコンマまたはセミコロンの後に省略可能な空白文字が続く 1 つ以上の出現箇所と一致します。 このパターンでは、最初のキャプチャ グループを定義します。これは、正規表現エンジンが文の末尾に到達するまで、複数の単語文字 (つまり、単語) とそれに続く省略可能な句読点記号の組み合わせが繰り返されるようにする必要があります。
[.?!] ピリオド、疑問符、感嘆符に一致します。

次の例に示すように、一致が見つかると、 GroupCollection オブジェクトと CaptureCollection オブジェクトの両方に、一致からのキャプチャが設定されます。 この場合、キャプチャ グループ (\w+[;,]?\s?) が存在するため、 + 量指定子を適用できるため、正規表現パターンは文内の各単語と一致します。 それ以外の場合は、文の最後の単語と一致します。

using System;
using System.Text.RegularExpressions;

public class Group1Example
{
    public static void Main()
    {
        string input = "This is one sentence. This is another.";
        string pattern = @"\b(\w+[;,]?\s?)+[.?!]";

        foreach (Match match in Regex.Matches(input, pattern))
        {
            Console.WriteLine($"Match: '{match.Value}' at index {match.Index}.");
            int grpCtr = 0;
            foreach (Group grp in match.Groups)
            {
                Console.WriteLine($"   Group {grpCtr}: '{grp.Value}' at index {grp.Index}.");
                int capCtr = 0;
                foreach (Capture cap in grp.Captures)
                {
                    Console.WriteLine($"      Capture {capCtr}: '{cap.Value}' at {cap.Index}.");
                    capCtr++;
                }
                grpCtr++;
            }
            Console.WriteLine();
        }
    }
}
// The example displays the following output:
//       Match: 'This is one sentence.' at index 0.
//          Group 0: 'This is one sentence.' at index 0.
//             Capture 0: 'This is one sentence.' at 0.
//          Group 1: 'sentence' at index 12.
//             Capture 0: 'This ' at 0.
//             Capture 1: 'is ' at 5.
//             Capture 2: 'one ' at 8.
//             Capture 3: 'sentence' at 12.
//
//       Match: 'This is another.' at index 22.
//          Group 0: 'This is another.' at index 22.
//             Capture 0: 'This is another.' at 22.
//          Group 1: 'another' at index 30.
//             Capture 0: 'This ' at 22.
//             Capture 1: 'is ' at 27.
//             Capture 2: 'another' at 30.
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim input As String = "This is one sentence. This is another."
        Dim pattern As String = "\b(\w+[;,]?\s?)+[.?!]"

        For Each match As Match In Regex.Matches(input, pattern)
            Console.WriteLine("Match: '{0}' at index {1}.",
                              match.Value, match.Index)
            Dim grpCtr As Integer = 0
            For Each grp As Group In match.Groups
                Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                                  grpCtr, grp.Value, grp.Index)
                Dim capCtr As Integer = 0
                For Each cap As Capture In grp.Captures
                    Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                      capCtr, cap.Value, cap.Index)
                    capCtr += 1
                Next
                grpCtr += 1
            Next
            Console.WriteLine()
        Next
    End Sub
End Module
' The example displays the following output:
'       Match: 'This is one sentence.' at index 0.
'          Group 0: 'This is one sentence.' at index 0.
'             Capture 0: 'This is one sentence.' at 0.
'          Group 1: 'sentence' at index 12.
'             Capture 0: 'This ' at 0.
'             Capture 1: 'is ' at 5.
'             Capture 2: 'one ' at 8.
'             Capture 3: 'sentence' at 12.
'       
'       Match: 'This is another.' at index 22.
'          Group 0: 'This is another.' at index 22.
'             Capture 0: 'This is another.' at 22.
'          Group 1: 'another' at index 30.
'             Capture 0: 'This ' at 22.
'             Capture 1: 'is ' at 27.
'             Capture 2: 'another' at 30.

部分式のみを使用して量指定子を適用し、キャプチャされたテキストに関心がない場合は、グループ キャプチャを無効にする必要があります。 たとえば、 (?:subexpression) 言語要素は、適用先のグループが一致する部分文字列をキャプチャできないようにします。 次の例では、前の例の正規表現パターンが \b(?:\w+[;,]?\s?)+[.?!] に変更されています。 出力が示すように、正規表現エンジンが GroupCollection コレクションと CaptureCollection コレクションを設定できないようにします。

using System;
using System.Text.RegularExpressions;

public class Group2Example
{
    public static void Main()
    {
        string input = "This is one sentence. This is another.";
        string pattern = @"\b(?:\w+[;,]?\s?)+[.?!]";

        foreach (Match match in Regex.Matches(input, pattern))
        {
            Console.WriteLine($"Match: '{match.Value}' at index {match.Index}.");
            int grpCtr = 0;
            foreach (Group grp in match.Groups)
            {
                Console.WriteLine($"   Group {grpCtr}: '{grp.Value}' at index {grp.Index}.");
                int capCtr = 0;
                foreach (Capture cap in grp.Captures)
                {
                    Console.WriteLine($"      Capture {capCtr}: '{cap.Value}' at {cap.Index}.");
                    capCtr++;
                }
                grpCtr++;
            }
            Console.WriteLine();
        }
    }
}
// The example displays the following output:
//       Match: 'This is one sentence.' at index 0.
//          Group 0: 'This is one sentence.' at index 0.
//             Capture 0: 'This is one sentence.' at 0.
//
//       Match: 'This is another.' at index 22.
//          Group 0: 'This is another.' at index 22.
//             Capture 0: 'This is another.' at 22.
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim input As String = "This is one sentence. This is another."
        Dim pattern As String = "\b(?:\w+[;,]?\s?)+[.?!]"

        For Each match As Match In Regex.Matches(input, pattern)
            Console.WriteLine("Match: '{0}' at index {1}.",
                              match.Value, match.Index)
            Dim grpCtr As Integer = 0
            For Each grp As Group In match.Groups
                Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                                  grpCtr, grp.Value, grp.Index)
                Dim capCtr As Integer = 0
                For Each cap As Capture In grp.Captures
                    Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                      capCtr, cap.Value, cap.Index)
                    capCtr += 1
                Next
                grpCtr += 1
            Next
            Console.WriteLine()
        Next
    End Sub
End Module
' The example displays the following output:
'       Match: 'This is one sentence.' at index 0.
'          Group 0: 'This is one sentence.' at index 0.
'             Capture 0: 'This is one sentence.' at 0.
'       
'       Match: 'This is another.' at index 22.
'          Group 0: 'This is another.' at index 22.
'             Capture 0: 'This is another.' at 22.

キャプチャは、次のいずれかの方法で無効にすることができます。

  • (?:subexpression)言語要素を使用します。 この要素は、適用先のグループ内の一致する部分文字列のキャプチャを禁止します。 入れ子になったグループの部分文字列キャプチャは無効になりません。

  • ExplicitCapture オプションを使用します。 正規表現パターン内のすべての無名または暗黙的なキャプチャが無効になります。 このオプションを使用すると、 (?<name>subexpression) 言語要素で定義された名前付きグループと一致する部分文字列のみをキャプチャできます。 ExplicitCapture フラグは、Regex クラス コンストラクターのoptions パラメーター、またはRegex静的一致メソッドのoptions パラメーターに渡すことができます。

  • (?imnsx)言語要素で n オプションを使用します。 このオプションは、要素が出現する正規表現パターン内のポイントから、名前のないキャプチャまたは暗黙的なキャプチャをすべて無効にします。 キャプチャは、パターンの最後まで、または (-n) オプションで名前のないキャプチャまたは暗黙的なキャプチャが有効になるまで無効になります。 詳細については、「 その他のコンストラクト」を参照してください。

  • (?imnsx:subexpression)言語要素で n オプションを使用します。 このオプションは、 subexpression内のすべての名前のないキャプチャまたは暗黙的なキャプチャを無効にします。 名前のないキャプチャ グループまたは暗黙的な入れ子になったキャプチャ グループによるキャプチャも無効になります。

スレッドの安全性

Regex クラス自体はスレッド セーフで不変です (読み取り専用)。 つまり、 Regex オブジェクトは任意のスレッドで作成でき、スレッド間で共有できます。一致するメソッドは、任意のスレッドから呼び出して、グローバルな状態を変更することはできません。

ただし、Regexによって返される結果オブジェクト (MatchMatchCollection) は、1 つのスレッドで使用する必要があります。 これらのオブジェクトの多くは論理的に不変ですが、実装ではパフォーマンスを向上させるためにいくつかの結果の計算が遅れる可能性があり、その結果、呼び出し元はそれらのオブジェクトへのアクセスをシリアル化する必要があります。

複数のスレッド Regex 結果オブジェクトを共有する必要がある場合は、同期されたメソッドを呼び出すことによって、これらのオブジェクトをスレッド セーフなインスタンスに変換できます。 列挙子を除き、すべての正規表現クラスはスレッド セーフであるか、同期されたメソッドによってスレッド セーフオブジェクトに変換できます。

列挙子だけが例外です。 コレクション列挙子の呼び出しをシリアル化する必要があります。 ルールは、複数のスレッドで同時にコレクションを列挙できる場合は、列挙子によって走査されるコレクションのルート オブジェクトの列挙子メソッドを同期する必要があるということです。

タイトル 説明
正規表現の動作の詳細 .NET での正規表現エンジンの実装を調べます。 この記事では、正規表現の柔軟性に焦点を当て、正規表現エンジンの効率的で堅牢な操作を確保するための開発者の責任について説明します。
バックトラッキング バックトラッキングとは何か、およびそれが正規表現のパフォーマンスにどのように影響するかを説明し、バックトラッキングの代替手段を提供する言語要素を調べます。
正規表現言語 - クイック リファレンス .NET の正規表現言語の要素について説明し、各言語要素の詳細なドキュメントへのリンクを提供します。