クラス

【C#】正規表現で文字列を高速抽出するRegex.MatchとMatchesの使い方完全入門

C#で文字列から任意のパターンを抜き出すならSystem.Text.RegularExpressions.Regexを使うのが定番です。

Regex.Matchは最初の一致を、Regex.Matchesはすべての一致を返し、キャプチャグループで部分要素も取り出せます。

例えば数字列は\d+で取得でき、オプションで大小文字や複数行も制御できます。

シンプルな記述で高速にパターン抽出が完結します。

目次から探す
  1. Regexクラスの基本
  2. Match取得パターン
  3. Matches取得パターン
  4. キャプチャグループ活用
  5. 量指定子とアンカー
  6. 先読み・後読み
  7. オプション制御
  8. パフォーマンス最適化
  9. 実践サンプル集
  10. ログ解析シナリオ
  11. 入力検証との違い
  12. 例外処理とデバッグ
  13. セキュリティ考慮
  14. よくあるミス
  15. 代替アプローチ
  16. 社内コード規約例
  17. より複雑なパターン例
  18. テストコードの書き方
  19. まとめ

Regexクラスの基本

C#で正規表現を扱う際に中心となるのがSystem.Text.RegularExpressions名前空間にあるRegexクラスです。

このクラスは文字列のパターンマッチングや抽出、置換などを簡単に実現できる強力なツールです。

ここでは、Regexクラスの基本的な使い方や生成方法、正規表現パターンの扱い方について詳しく解説します。

Regexオブジェクト生成方法

Regexクラスのインスタンスを生成する方法は主に3つあります。

用途やパフォーマンス要件に応じて使い分けることが重要です。

簡易生成: 静的メソッド利用

最も手軽に正規表現を使いたい場合は、Regexクラスの静的メソッドを利用します。

たとえば、Regex.MatchRegex.Matchesは内部で一時的にRegexオブジェクトを生成して処理を行います。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "abc123def456";
        string pattern = @"\d+";
        // 静的メソッドで最初の数字の連続を取得
        Match match = Regex.Match(input, pattern);
        if (match.Success)
        {
            Console.WriteLine($"最初の数字の連続: {match.Value}");
        }
    }
}
最初の数字の連続: 123

この方法はコードがシンプルで、単発の検索や抽出に向いています。

ただし、同じパターンを何度も使う場合は毎回Regexオブジェクトが生成されるため、パフォーマンス面でやや不利になることがあります。

インスタンス生成: new Regex

パターンを複数回使う場合や、オプションを指定したい場合はnew Regexでインスタンスを生成する方法が適しています。

生成したRegexオブジェクトを使い回すことで、パフォーマンスが向上します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "abc123def456ghi789";
        string pattern = @"\d+";
        // Regexオブジェクトを生成して使い回す
        Regex regex = new Regex(pattern);
        MatchCollection matches = regex.Matches(input);
        Console.WriteLine("すべての数字の連続:");
        foreach (Match m in matches)
        {
            Console.WriteLine(m.Value);
        }
    }
}
すべての数字の連続:
123
456
789

この方法は、同じパターンで複数回マッチングを行う場合に効率的です。

また、RegexコンストラクタでRegexOptionsを指定して大文字小文字の無視やマルチラインモードなどの設定も可能です。

コンパイル済みRegexの生成

さらにパフォーマンスを追求したい場合は、RegexOptions.Compiledオプションを指定してRegexオブジェクトを生成します。

これにより、正規表現がILコードにコンパイルされ、実行時のマッチングが高速化されます。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "abc123def456ghi789";
        string pattern = @"\d+";
        // コンパイル済みRegexを生成
        Regex regex = new Regex(pattern, RegexOptions.Compiled);
        MatchCollection matches = regex.Matches(input);
        Console.WriteLine("コンパイル済みRegexで抽出:");
        foreach (Match m in matches)
        {
            Console.WriteLine(m.Value);
        }
    }
}
コンパイル済みRegexで抽出:
123
456
789

ただし、RegexOptions.Compiledは初回の生成に時間がかかるため、頻繁にパターンを切り替える用途には向きません。

長時間動作するアプリケーションで同じパターンを繰り返し使う場合に効果的です。

正規表現パターンの文字列リテラル化

C#で正規表現パターンを記述する際は、文字列リテラルの扱いに注意が必要です。

正規表現ではバックスラッシュ\が特殊文字のエスケープに使われますが、C#の文字列リテラルでも\はエスケープ文字として使われるため、二重にエスケープする必要があります。

例えば、数字にマッチする\dを正規表現で書く場合、C#の通常の文字列リテラルでは"\\d"と記述します。

これは\を文字列中に表現するために\\と書くためです。

string pattern = "\\d+"; // 数字の連続にマッチ

しかし、C#には「逐語的文字列リテラル」と呼ばれる@を付けた文字列リテラルがあり、これを使うとエスケープシーケンスを無視して文字列をそのまま扱えます。

正規表現パターンを書く際は、こちらを使うのが一般的で可読性も高まります。

string pattern = @"\d+"; // 数字の連続にマッチ

このように@を付けることで、\を1つだけ書けばよくなり、パターンが見やすくなります。

エスケープの落とし穴

正規表現パターンをC#の文字列として記述する際に最も多いミスが、バックスラッシュのエスケープ忘れです。

例えば、\w(単語文字にマッチ)を"\w"と書くと、C#のコンパイラは\wをエスケープシーケンスとして認識できずエラーになります。

正しくは"\\w"@"\w"と書く必要があります。

また、正規表現の中で特殊文字を文字として扱いたい場合は、バックスラッシュでエスケープしますが、C#の文字列リテラルでもさらにエスケープが必要になるため、二重のエスケープが必要になることがあります。

例として、ドット.は正規表現で任意の1文字にマッチしますが、文字としてのドットを表現したい場合は\.と書きます。

C#の文字列リテラルでは"\\."@"\."となります。

string pattern = @"\."; // ドット文字にマッチ

このように、正規表現パターンのエスケープはC#の文字列リテラルのルールと正規表現のルールが重なるため、混乱しやすいポイントです。

パターンを作成する際は、逐語的文字列リテラル@""を使うことをおすすめします。

さらに、正規表現の中でバックスラッシュ自体を文字として使いたい場合は、\\と書きますが、C#の文字列リテラルでは"\\\\“か@"\\"となるため、注意が必要です。

これらの基本を押さえることで、C#での正規表現の利用がスムーズになります。

Match取得パターン

最初の一致を取得: Regex.Match

Regex.Matchメソッドは、指定した文字列の中から正規表現パターンに最初にマッチした部分を取得します。

戻り値はMatchオブジェクトで、マッチの詳細情報を含んでいます。

最初の一致だけを取得したい場合に使います。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "商品ID: A123, 商品ID: B456";
        string pattern = @"商品ID: (\w\d+)";
        Match match = Regex.Match(input, pattern);
        if (match.Success)
        {
            Console.WriteLine($"最初の一致: {match.Value}");
            Console.WriteLine($"キャプチャグループ1: {match.Groups[1].Value}");
        }
        else
        {
            Console.WriteLine("一致なし");
        }
    }
}
最初の一致: 商品ID: A123
キャプチャグループ1: A123

この例では、input文字列の中から「商品ID: 」に続く英数字の組み合わせを最初に見つけて抽出しています。

match.Valueはマッチした全文字列、match.Groups[1].Valueは括弧で囲んだ部分(キャプチャグループ)を取得しています。

マッチオブジェクトのプロパティ

Matchオブジェクトには、マッチ結果に関するさまざまなプロパティがあります。

主なものを表にまとめます。

プロパティ名説明
Successマッチが成功したかどうかbool
Valueマッチした文字列全体
Indexマッチした部分の開始位置(0からのオフセット)
Lengthマッチした部分の長さ
Groupsキャプチャグループのコレクション
NextMatch()次のマッチを取得するメソッド

これらのプロパティを活用することで、マッチした文字列の位置や長さを把握したり、複数のグループを扱ったりできます。

グループとキャプチャのアクセス

正規表現のパターン内で丸括弧()を使うと、マッチした部分の一部を「グループ」として抽出できます。

Match.Groupsプロパティは、これらのグループをGroupCollectionとして返します。

  • Groups[0]はマッチ全体を表します
  • Groups[1]以降が各キャプチャグループに対応します

複数のグループがある場合は、インデックスでアクセスできます。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "2024-06-15";
        string pattern = @"(\d{4})-(\d{2})-(\d{2})";
        Match match = Regex.Match(input, pattern);
        if (match.Success)
        {
            Console.WriteLine($"年: {match.Groups[1].Value}");
            Console.WriteLine($"月: {match.Groups[2].Value}");
            Console.WriteLine($"日: {match.Groups[3].Value}");
        }
    }
}
年: 2024
月: 06
日: 15

また、名前付きグループを使うと、グループに名前を付けてアクセスできます。

名前付きグループは(?<name>pattern)の形式で定義し、Groups["name"]で取得します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "2024-06-15";
        string pattern = @"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})";
        Match match = Regex.Match(input, pattern);
        if (match.Success)
        {
            Console.WriteLine($"年: {match.Groups["year"].Value}");
            Console.WriteLine($"月: {match.Groups["month"].Value}");
            Console.WriteLine($"日: {match.Groups["day"].Value}");
        }
    }
}
年: 2024
月: 06
日: 15

このように名前付きグループを使うと、コードの可読性が向上し、どの部分が何を表しているかが明確になります。

ループ処理で次を探します: Match.NextMatch

Regex.Matchは最初の一致だけを返しますが、文字列中に複数のマッチがある場合は、Match.NextMatch()メソッドを使って次のマッチを順に取得できます。

これを使うと、ループで連続してマッチを処理できます。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "商品ID: A123, 商品ID: B456, 商品ID: C789";
        string pattern = @"商品ID: (\w\d+)";
        Match match = Regex.Match(input, pattern);
        while (match.Success)
        {
            Console.WriteLine($"一致: {match.Value}, ID: {match.Groups[1].Value}");
            match = match.NextMatch();
        }
    }
}
一致: 商品ID: A123, ID: A123
一致: 商品ID: B456, ID: B456
一致: 商品ID: C789, ID: C789

このコードでは、最初のマッチを取得した後、NextMatch()で次のマッチを順に取得し、すべての一致を処理しています。

NextMatch()は次のマッチがない場合、SuccessfalseMatchオブジェクトを返すため、ループの終了条件として使えます。

Match.NextMatch()Regex.Matchesと似た役割を果たしますが、Matchオブジェクトを使って逐次的にマッチを取得したい場合に便利です。

特に、マッチの間で何らかの処理や条件分岐を行いたいときに活用できます。

Matches取得パターン

すべての一致を取得: Regex.Matches

Regex.Matchesメソッドは、指定した文字列の中から正規表現パターンにマッチするすべての部分を取得します。

戻り値はMatchCollectionで、複数のMatchオブジェクトを含んでいます。

複数の一致を一括で取得したい場合に便利です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "電話番号: 090-1234-5678, 080-9876-5432";
        string pattern = @"\d{3}-\d{4}-\d{4}";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("一致した電話番号一覧:");
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
一致した電話番号一覧:
090-1234-5678
080-9876-5432

この例では、電話番号の形式にマッチするすべての文字列を抽出しています。

MatchCollectionIEnumerableを実装しているため、foreachで簡単に走査できます。

MatchCollectionの走査方法

MatchCollectionIEnumerableを実装しているため、foreachループで順にアクセスできます。

ただし、MatchCollectionは遅延評価ではなく、Regex.Matches呼び出し時にすべてのマッチを取得しているため、繰り返しの走査は効率的です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "abc123def456ghi789";
        string pattern = @"\d+";
        MatchCollection matches = Regex.Matches(input, pattern);
        // foreachで走査
        foreach (Match m in matches)
        {
            Console.WriteLine($"マッチ: {m.Value} (位置: {m.Index})");
        }
        // インデックスアクセスも可能
        Console.WriteLine($"最初のマッチ: {matches[0].Value}");
    }
}
マッチ: 123 (位置: 3)
マッチ: 456 (位置: 9)
マッチ: 789 (位置: 15)
最初のマッチ: 123

MatchCollectionは配列のようにインデックスでアクセスできるため、特定のマッチだけを取り出すことも可能です。

LINQと組み合わせた抽出

MatchCollectionIEnumerableですが、ジェネリックではないため、そのままではLINQの拡張メソッドを直接使えません。

LINQを使いたい場合は、Cast<Match>()OfType<Match>()で型変換を行う必要があります。

using System;
using System.Linq;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "商品コード: A123, B456, C789";
        string pattern = @"\b\w\d{3}\b";
        MatchCollection matches = Regex.Matches(input, pattern);
        // LINQでマッチの値を大文字に変換して抽出
        var upperCodes = matches.Cast<Match>()
                                .Select(m => m.Value.ToUpper())
                                .ToList();
        Console.WriteLine("大文字に変換した商品コード:");
        foreach (var code in upperCodes)
        {
            Console.WriteLine(code);
        }
    }
}
大文字に変換した商品コード:
A123
B456
C789

このようにCast<Match>()を使うことで、LINQのSelectWhereなどの拡張メソッドを利用できます。

条件で絞り込みたい場合も簡単に行えます。

// 数字が500以上のコードだけ抽出
var filteredCodes = matches.Cast<Match>()
                           .Where(m => int.Parse(m.Value.Substring(1)) >= 500)
                           .Select(m => m.Value)
                           .ToList();
Console.WriteLine("500以上のコード:");
foreach (var code in filteredCodes)
{
    Console.WriteLine(code);
}

このように、Regex.MatchesとLINQを組み合わせることで、抽出したマッチを柔軟に加工・フィルタリングできます。

キャプチャグループ活用

番号付きグループ

正規表現の丸括弧()で囲んだ部分は「キャプチャグループ」と呼ばれ、マッチした部分文字列を個別に取得できます。

番号付きグループは、パターン内で左から順に1から番号が割り当てられます。

Groupsコレクションのインデックスでアクセス可能です。

インデックスでの参照

番号付きグループはMatch.Groupsのインデックスで参照します。

Groups[0]はマッチ全体、Groups[1]以降が各グループに対応します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "名前: 山田太郎, 年齢: 30";
        string pattern = @"名前: (\S+), 年齢: (\d+)";
        Match match = Regex.Match(input, pattern);
        if (match.Success)
        {
            Console.WriteLine($"名前: {match.Groups[1].Value}");
            Console.WriteLine($"年齢: {match.Groups[2].Value}");
        }
    }
}
名前: 山田太郎
年齢: 30

この例では、(\S+)が名前、(\d+)が年齢をキャプチャしています。

番号付きグループはシンプルですが、複数のグループがあるとどの番号がどの意味か分かりにくくなることがあります。

名前付きグループ

名前付きグループは、(?<name>pattern)の形式でグループに名前を付けられます。

名前でアクセスできるため、コードの可読性が向上します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "名前: 山田太郎, 年齢: 30";
        string pattern = @"名前: (?<name>\S+), 年齢: (?<age>\d+)";
        Match match = Regex.Match(input, pattern);
        if (match.Success)
        {
            Console.WriteLine($"名前: {match.Groups["name"].Value}");
            Console.WriteLine($"年齢: {match.Groups["age"].Value}");
        }
    }
}
名前: 山田太郎
年齢: 30

Dictionaryに変換するテクニック

名前付きグループの値をDictionary<string, string>に変換すると、後で動的にアクセスしやすくなります。

以下のようにGroupsコレクションをループして辞書に格納できます。

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "名前: 山田太郎, 年齢: 30, 性別: 男性";
        string pattern = @"名前: (?<name>\S+), 年齢: (?<age>\d+), 性別: (?<gender>\S+)";
        Match match = Regex.Match(input, pattern);
        if (match.Success)
        {
            var dict = new Dictionary<string, string>();
            foreach (string groupName in match.Groups.Keys)
            {
                // 0はマッチ全体なのでスキップ
                if (groupName == "0") continue;
                dict[groupName] = match.Groups[groupName].Value;
            }
            foreach (var kvp in dict)
            {
                Console.WriteLine($"{kvp.Key}: {kvp.Value}");
            }
        }
    }
}
name: 山田太郎
age: 30
gender: 男性

この方法は、グループ名が多い場合や動的に扱いたい場合に便利です。

非キャプチャグループで速度改善

正規表現の丸括弧はデフォルトでキャプチャグループとなり、マッチした部分を保存しますが、キャプチャが不要な場合は(?:pattern)の非キャプチャグループを使うことでパフォーマンスを向上できます。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "abc123def456";
        // キャプチャグループあり
        string pattern1 = @"(abc)(\d+)(def)(\d+)";
        // 非キャプチャグループを使う
        string pattern2 = @"(?:abc)(\d+)(?:def)(\d+)";
        var match1 = Regex.Match(input, pattern1);
        var match2 = Regex.Match(input, pattern2);
        Console.WriteLine("キャプチャグループ数(pattern1): " + match1.Groups.Count);
        Console.WriteLine("キャプチャグループ数(pattern2): " + match2.Groups.Count);
    }
}
キャプチャグループ数(pattern1): 5
キャプチャグループ数(pattern2): 3

非キャプチャグループはキャプチャしないため、Groupsの数が減り、メモリ使用量や処理速度の改善につながります。

キャプチャが不要な部分は積極的に非キャプチャグループを使いましょう。

サブパターンの入れ子と再帰

正規表現では、グループの中にさらにグループを入れる「入れ子構造」が可能です。

複雑なパターンを分割して整理できます。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "abc123def";
        string pattern = @"(abc(\d+)def)";
        Match match = Regex.Match(input, pattern);
        if (match.Success)
        {
            Console.WriteLine($"全体: {match.Groups[1].Value}");
            Console.WriteLine($"数字部分: {match.Groups[2].Value}");
        }
    }
}
全体: abc123def
数字部分: 123

さらに、.NETの正規表現は「再帰パターン」もサポートしています。

これは、パターンの中で自分自身を呼び出すことで、入れ子構造の解析や括弧の対応など複雑な構造を扱えます。

再帰パターンの例として、括弧の対応をチェックするパターンを示します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string pattern = @"^\((?>[^()]+|(?<Open>\()|(?<-Open>\)))*(?(Open)(?!))\)$";
        string[] inputs = { "(abc(def))", "(abc(def)", "abc(def))" };
        foreach (var input in inputs)
        {
            bool isMatch = Regex.IsMatch(input, pattern);
            Console.WriteLine($"{input} は正しい括弧の対応か: {isMatch}");
        }
    }
}
(abc(def)) は正しい括弧の対応か: True
(abc(def) は正しい括弧の対応か: False
abc(def)) は正しい括弧の対応か: False

このパターンは、(?<Open>\()で開き括弧をカウントし、(?<-Open>\))で閉じ括弧を減らし、最後にすべての開き括弧が閉じられているかをチェックしています。

再帰的なグループ操作により、複雑なネスト構造を正規表現で扱えます。

ただし、再帰パターンは複雑でパフォーマンスに影響するため、必要な場合に限定して使うことをおすすめします。

量指定子とアンカー

量指定子の最短一致と貪欲一致

量指定子は、正規表現のパターンで直前の要素が何回繰り返されるかを指定する記号です。

代表的な量指定子には以下があります。

  • * : 0回以上の繰り返し(貪欲)
  • + : 1回以上の繰り返し(貪欲)
  • ? : 0回または1回(貪欲)
  • {n} : ちょうどn回
  • {n,} : n回以上
  • {n,m} : n回以上m回以下

これらはデフォルトで「貪欲一致(Greedy)」と呼ばれ、可能な限り多くの文字をマッチさせようとします。

一方、量指定子の後に?を付けると「最短一致(Lazy)」になります。

これは、可能な限り少ない文字数でマッチさせようとします。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "<div>内容1</div><div>内容2</div>";
        // 貪欲一致: 最初の<div>から最後の</div>までマッチ
        string greedyPattern = @"<div>.*</div>";
        Match greedyMatch = Regex.Match(input, greedyPattern);
        Console.WriteLine("貪欲一致: " + greedyMatch.Value);
        // 最短一致: 最初の<div>から最初の</div>までマッチ
        string lazyPattern = @"<div>.*?</div>";
        Match lazyMatch = Regex.Match(input, lazyPattern);
        Console.WriteLine("最短一致: " + lazyMatch.Value);
    }
}
貪欲一致: <div>内容1</div><div>内容2</div>
最短一致: <div>内容1</div>

この例では、.*が貪欲にマッチしすぎてしまい、最初の<div>から最後の</div>までを一括でマッチさせています。

.*?にすることで最短一致となり、最初の<div>から最初の</div>までの部分だけをマッチさせています。

アンカー^と$

アンカーは文字列の特定の位置にマッチさせるための特殊な記号です。

  • ^ : 文字列の先頭にマッチ
  • $ : 文字列の末尾にマッチ

これらは文字列の境界を指定するため、文字自体にはマッチしません。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input1 = "Hello World";
        string input2 = "Say Hello";
        string patternStart = @"^Hello";
        string patternEnd = @"World$";
        Console.WriteLine(Regex.IsMatch(input1, patternStart)); // True
        Console.WriteLine(Regex.IsMatch(input2, patternStart)); // False
        Console.WriteLine(Regex.IsMatch(input1, patternEnd));   // True
        Console.WriteLine(Regex.IsMatch(input2, patternEnd));   // False
    }
}
True
False
True
False

このように、^は文字列の先頭にあるかどうか、$は末尾にあるかどうかを判定します。

複数行モードを使う場合は挙動が変わるため注意が必要です。

ワード境界\b

\bは「ワード境界」を表すアンカーで、単語の区切りにマッチします。

具体的には、単語文字(英数字やアンダースコア)と非単語文字の間にマッチします。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "cat scatter category";
        string pattern = @"\bcat\b";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("単語としての 'cat' の出現数: " + matches.Count);
        foreach (Match m in matches)
        {
            Console.WriteLine($"マッチ: '{m.Value}' (位置: {m.Index})");
        }
    }
}
単語としての 'cat' の出現数: 1
マッチ: 'cat' (位置: 0)

この例では、catが単語として独立している場合のみマッチします。

scattercategoryの中のcatはマッチしません。

\bは単語の境界を判定するため、単語単位の検索に便利です。

行頭行末: \A \z

^$は通常、文字列の先頭・末尾にマッチしますが、RegexOptions.Multilineを指定すると、行の先頭・末尾にもマッチするようになります。

これに対して、\A\zは常に文字列全体の先頭と末尾にマッチします。

  • \A : 文字列の先頭にマッチ(行頭ではない)
  • \z : 文字列の末尾にマッチ(行末ではない)
using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main()
    {
        // 末尾に改行を追加
        string input = "first line\nsecond line\n";

        string patternCaret = @"^first";
        string patternA = @"\Afirst";
        string patternDollar = @"line$";
        string patternZ = @"line\z";

        // ^ はどちらも最初の行頭にマッチ → True
        Console.WriteLine(Regex.IsMatch(input, patternCaret)); // True
        Console.WriteLine(Regex.IsMatch(input, patternA));     // True

        // $ はマルチラインモードで各行末にマッチ → True
        Console.WriteLine(
            Regex.IsMatch(input, patternDollar, RegexOptions.Multiline)
        ); // True

        // \z は文字列の絶対末尾(ここでは '\n')にしかマッチしない → False
        Console.WriteLine(Regex.IsMatch(input, patternZ));      // False
    }
}
True
True
True
False

この例では、\Aは文字列の先頭にのみマッチし、^はマルチラインモードで行頭にもマッチします。

\zは文字列の末尾にのみマッチし、$はマルチラインモードで行末にもマッチします。

\A\zは文字列全体の境界を厳密に指定したい場合に使います。

特に複数行の文字列を扱う際に、行単位ではなく文字列全体の先頭・末尾を指定したいときに便利です。

先読み・後読み

ポジティブ先読み

ポジティブ先読みは、あるパターンの直後に特定の文字列やパターンが続く場合にマッチさせるテクニックです。

先読みの部分はマッチ結果に含まれず、条件判定だけに使われます。

構文は(?=pattern)です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "apple pie, apple tart, apple";
        string pattern = @"apple(?= pie)";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("ポジティブ先読みでマッチした部分:");
        foreach (Match m in matches)
        {
            Console.WriteLine($"'{m.Value}' (位置: {m.Index})");
        }
    }
}
ポジティブ先読みでマッチした部分:
'apple' (位置: 0)

この例では、appleの後にpieが続く場合のみマッチします。

最後のapplepieが続かないためマッチしません。

先読みの部分はマッチ結果に含まれず、m.Valueappleだけです。

ネガティブ先読み

ネガティブ先読みは、あるパターンの直後に特定の文字列やパターンが続かない場合にマッチさせます。

構文は(?!pattern)です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "apple pie, apple tart, apple";
        string pattern = @"apple(?! pie)";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("ネガティブ先読みでマッチした部分:");
        foreach (Match m in matches)
        {
            Console.WriteLine($"'{m.Value}' (位置: {m.Index})");
        }
    }
}
ネガティブ先読みでマッチした部分:
'apple' (位置: 11)
'apple' (位置: 23)

この例では、appleの後にpieが続かない場合にマッチします。

apple tartと単独のappleがマッチし、apple pieは除外されます。

先読みを使った境界抽出

先読みを活用すると、特定の文字列の前後の境界を抽出したり、条件付きでマッチを制御したりできます。

例えば、数字の後に特定の単位が続く場合のみ抽出するケースです。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "価格は100円、送料は200円、割引は50ドル";
        string pattern = @"\d+(?=円)";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("円単位の数字のみ抽出:");
        foreach (Match m in matches)
        {
            Console.WriteLine(m.Value);
        }
    }
}
円単位の数字のみ抽出:
100
200

この例では、数字の後にが続く場合のみ数字を抽出しています。

50ドルはマッチしません。

先読みを使うことで、マッチ結果に含めたくない文字列を条件として利用しつつ、抽出対象を限定できます。

また、先読みと後読みを組み合わせて、特定の文字列の間にある部分だけを抽出することも可能です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "開始[重要]終了 開始[無視]終了";
        string pattern = @"(?<=開始\\[重要\\]).*?(?=終了)";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("開始[重要]と終了の間の文字列:");
        foreach (Match m in matches)
        {
            Console.WriteLine(m.Value);
        }
    }
}
開始[重要]と終了の間の文字列:

この例では、開始[重要]の直後から終了の直前までの文字列を抽出しています。

(?<=...)はポジティブ後読み(後述)で、(?=...)はポジティブ先読みです。

結果は空文字列ですが、間に文字があれば抽出されます。

先読み・後読みを組み合わせることで、柔軟な境界抽出が可能です。

オプション制御

大文字小文字無視: RegexOptions.IgnoreCase

正規表現はデフォルトで大文字と小文字を区別してマッチングを行いますが、RegexOptions.IgnoreCaseオプションを指定すると、大文字小文字を区別せずにマッチさせることができます。

英字のパターンを柔軟に扱いたい場合に便利です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "Hello HELLO hello";
        string pattern = "hello";
        // 大文字小文字を区別する場合
        var matchesCaseSensitive = Regex.Matches(input, pattern);
        Console.WriteLine("区別ありのマッチ数: " + matchesCaseSensitive.Count);
        // IgnoreCaseオプションを指定
        var matchesIgnoreCase = Regex.Matches(input, pattern, RegexOptions.IgnoreCase);
        Console.WriteLine("IgnoreCase指定のマッチ数: " + matchesIgnoreCase.Count);
    }
}
区別ありのマッチ数: 1
IgnoreCase指定のマッチ数: 3

この例では、RegexOptions.IgnoreCaseを指定しない場合は小文字のhelloだけがマッチしますが、指定すると大文字や混合文字もすべてマッチします。

複数行と単一行: Multiline vs Singleline

RegexOptions.MultilineRegexOptions.Singlelineは、正規表現のアンカーやドット.の挙動を制御するオプションです。

混同しやすいので違いを理解して使い分けることが重要です。

  • Multiline (RegexOptions.Multiline)

^$が文字列全体の先頭・末尾だけでなく、各行の先頭・末尾にもマッチするようになります。

複数行のテキストを行単位で処理したい場合に使います。

  • Singleline (RegexOptions.Singleline)

ドット.が改行文字\nも含めてすべての文字にマッチするようになります。

通常は.は改行を除く任意の1文字にマッチします。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "first line\nsecond line";
        // Multilineなし: ^ は文字列先頭のみにマッチ
        var match1 = Regex.Match(input, "^second", RegexOptions.None);
        Console.WriteLine("Multilineなしで^secondにマッチ: " + match1.Success);
        // Multilineあり: ^ が各行の先頭にマッチ
        var match2 = Regex.Match(input, "^second", RegexOptions.Multiline);
        Console.WriteLine("Multilineありで^secondにマッチ: " + match2.Success);

        // アンカーで文字列全体を対象にする
        // Singlelineなし: . は改行にマッチしない → マッチしない
        var match3 = Regex.Match(input, "^first.*line$", RegexOptions.None);
        Console.WriteLine("Singlelineなしでマッチ: " + match3.Success);
        // Singlelineあり: . が改行にもマッチ → マッチする
        var match4 = Regex.Match(input, "^first.*line$", RegexOptions.Singleline);
        Console.WriteLine("Singlelineありでマッチ: " + match4.Success);
    }
}
Multilineなしで^secondにマッチ: False
Multilineありで^secondにマッチ: True
Singlelineなしでマッチ: False
Singlelineありでマッチ: True

このように、Multilineはアンカーの挙動を変え、Singleline.のマッチ範囲を変えます。

両方を同時に指定することも可能です。

コンパイル: RegexOptions.Compiled

RegexOptions.Compiledを指定すると、正規表現パターンがILコードにコンパイルされ、実行時のマッチングが高速化されます。

パフォーマンスが重要な場面で効果的ですが、初回のコンパイルに時間がかかるため、頻繁にパターンを切り替える用途には向きません。

using System;
using System.Diagnostics;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "abc123def456ghi789";
        string pattern = @"\d+";
        var sw = new Stopwatch();
        // 通常のRegex
        sw.Start();
        for (int i = 0; i < 10000; i++)
        {
            Regex.Matches(input, pattern);
        }
        sw.Stop();
        Console.WriteLine("通常Regex: " + sw.ElapsedMilliseconds + "ms");
        // Compiledオプション付きRegex
        var regexCompiled = new Regex(pattern, RegexOptions.Compiled);
        sw.Restart();
        for (int i = 0; i < 10000; i++)
        {
            regexCompiled.Matches(input);
        }
        sw.Stop();
        Console.WriteLine("CompiledRegex: " + sw.ElapsedMilliseconds + "ms");
    }
}
通常Regex: 4ms
CompiledRegex: 0ms

(※実行環境により数値は異なります)

この例では、RegexOptions.Compiledを使うことで繰り返し処理のパフォーマンスが大幅に向上しています。

長時間動作するアプリケーションで同じパターンを繰り返し使う場合におすすめです。

Timeout設定でDoS防御

複雑な正規表現や悪意のある入力により、正規表現の処理が長時間かかることがあります。

これを防ぐために、Regexのコンストラクタや静的メソッドでTimeSpanによるタイムアウトを設定できます。

タイムアウトを超えるとRegexMatchTimeoutExceptionがスローされます。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa!";
        string pattern = @"(a+)+!";
        try
        {
            // タイムアウトを1秒に設定
            var regex = new Regex(pattern, RegexOptions.None, TimeSpan.FromSeconds(1));
            bool isMatch = regex.IsMatch(input);
            Console.WriteLine("マッチ結果: " + isMatch);
        }
        catch (RegexMatchTimeoutException)
        {
            Console.WriteLine("正規表現の処理がタイムアウトしました。");
        }
    }
}
正規表現の処理がタイムアウトしました。

この例は、ネストした量指定子による「バックトラッキング爆発」を意図的に起こし、タイムアウトで処理を中断しています。

タイムアウト設定はサービスの安定性やセキュリティ対策として重要です。

適切な値を設定し、例外処理を行いましょう。

パフォーマンス最適化

静的Regexのキャッシュ

正規表現を頻繁に使う場合、毎回Regexオブジェクトを生成するとパフォーマンスが低下します。

そこで、同じパターンのRegexオブジェクトを静的にキャッシュして使い回す方法が効果的です。

これにより、パターンの解析やコンパイルコストを一度だけ支払えば済み、処理が高速化します。

using System;
using System.Text.RegularExpressions;
class Program
{
    // 静的にRegexオブジェクトをキャッシュ
    private static readonly Regex NumberRegex = new Regex(@"\d+", RegexOptions.Compiled);
    static void Main()
    {
        string input = "商品番号123、価格456、在庫789";
        MatchCollection matches = NumberRegex.Matches(input);
        Console.WriteLine("数字の抽出結果:");
        foreach (Match m in matches)
        {
            Console.WriteLine(m.Value);
        }
    }
}
数字の抽出結果:
123
456
789

この例では、NumberRegexを静的フィールドとして一度だけ生成し、複数回の呼び出しで使い回しています。

RegexOptions.Compiledを付けることでさらに高速化が期待できます。

頻繁に使うパターンはこのようにキャッシュするのがベストプラクティスです。

Regex.CompileToAssemblyでさらに高速化

.NETでは、複数の正規表現パターンをまとめてILコードにコンパイルし、専用のアセンブリとして生成できるRegex.CompileToAssemblyメソッドがあります。

これにより、起動時のコンパイルコストを削減し、実行時のパフォーマンスを大幅に向上させられます。

// 例示コードは省略しますが、
// RegexCompilationInfoクラスでパターンとオプションを指定し、
// CompileToAssemblyメソッドでアセンブリを生成します。

この方法は大規模なアプリケーションやライブラリで多数の正規表現を使う場合に有効です。

ただし、ビルドプロセスが複雑になるため、一般的なアプリケーションでは静的キャッシュとRegexOptions.Compiledの組み合わせで十分なことが多いです。

Span API活用

C# 7.2以降で導入されたSpan<T>は、メモリ効率が高く高速なデータ操作を可能にします。

RegexReadOnlySpan<char>を受け取るオーバーロードを持っており、文字列の部分切り出しやコピーを減らしてパフォーマンスを改善できます。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "abc123def456ghi789";
        ReadOnlySpan<char> spanInput = input.AsSpan();
        Regex regex = new Regex(@"\d+");
        // Spanを使ったマッチング
        Match match = regex.Match(spanInput);
        while (match.Success)
        {
            Console.WriteLine($"数字: {match.Value}");
            match = match.NextMatch();
        }
    }
}
数字: 123
数字: 456
数字: 789

ReadOnlySpan<char>を使うことで、部分文字列の生成を避け、GC負荷を減らせます。

大量の文字列処理やリアルタイム処理で効果的です。

スタックオーバーフロー回避策

複雑な正規表現や深いネスト、再帰的なパターンは、内部でスタックを大量に消費し、StackOverflowExceptionを引き起こすことがあります。

これを防ぐための対策をいくつか紹介します。

  • パターンの簡素化

不必要に複雑なパターンや過剰なネストを避け、シンプルな正規表現に分割します。

  • 非キャプチャグループの活用

キャプチャが不要な部分は(?:...)で非キャプチャグループにし、メモリ消費を抑えます。

  • 量指定子の適切な使用

貪欲な量指定子*+の多用はバックトラッキングを増やすため、必要に応じて最短一致*?+?を使います。

  • Timeoutの設定

Regexのタイムアウトを設定し、処理が長時間かかる場合は例外で中断します。

  • RegexOptions.Compiledの利用

コンパイル済み正規表現は内部処理が最適化されているため、スタック消費が抑えられる場合があります。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = new string('a', 10000) + "!";
        string pattern = @"(a+)+!";
        try
        {
            var regex = new Regex(pattern, RegexOptions.None, TimeSpan.FromSeconds(1));
            bool isMatch = regex.IsMatch(input);
            Console.WriteLine("マッチ結果: " + isMatch);
        }
        catch (RegexMatchTimeoutException)
        {
            Console.WriteLine("正規表現の処理がタイムアウトしました。");
        }
    }
}
正規表現の処理がタイムアウトしました。

この例は、過剰なバックトラッキングを引き起こすパターンに対してタイムアウトを設定し、スタックオーバーフローを防いでいます。

複雑な正規表現を使う際は、パフォーマンスと安全性のバランスを考慮しましょう。

実践サンプル集

数値抽出

文字列から数値を抽出する基本的な例です。

整数や連続した数字を取り出す場合は\d+を使います。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "商品IDは12345、価格は6789円です。";
        string pattern = @"\d+";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("抽出した数値:");
        foreach (Match m in matches)
        {
            Console.WriteLine(m.Value);
        }
    }
}
抽出した数値:
12345
6789

この例では、文字列中のすべての連続した数字を抽出しています。

小数点やマイナス符号を含めたい場合はパターンを拡張する必要があります。

日付抽出

日付形式の文字列を抽出し、年・月・日をキャプチャグループで分割します。

ここではyyyy-MM-dd形式を例にします。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "イベントは2024-06-15に開催されます。";
        string pattern = @"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})";
        Match match = Regex.Match(input, pattern);
        if (match.Success)
        {
            Console.WriteLine($"年: {match.Groups["year"].Value}");
            Console.WriteLine($"月: {match.Groups["month"].Value}");
            Console.WriteLine($"日: {match.Groups["day"].Value}");
        }
        else
        {
            Console.WriteLine("日付が見つかりませんでした。");
        }
    }
}
年: 2024
月: 06
日: 15

名前付きグループを使うことで、年・月・日をわかりやすく取得しています。

日付の形式に応じてパターンを調整してください。

Email抽出

メールアドレスの抽出はよくあるケースです。

ここでは簡易的なパターンを使います。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "お問い合わせはsupport@example.comまでお願いします。";
        string pattern = @"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}";
        Match match = Regex.Match(input, pattern);
        if (match.Success)
        {
            Console.WriteLine("抽出したメールアドレス: " + match.Value);
        }
        else
        {
            Console.WriteLine("メールアドレスが見つかりませんでした。");
        }
    }
}
抽出したメールアドレス: support@example.com

このパターンは基本的なメールアドレス形式にマッチしますが、RFCに完全準拠しているわけではありません。

用途に応じて調整してください。

URL抽出

URLを抽出する例です。

ここではhttpまたはhttpsで始まるURLを対象にします。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "公式サイトはhttps://www.example.com/です。";
        string pattern = @"https?://[^\s/$.?#].[^\s]*";
        Match match = Regex.Match(input, pattern);
        if (match.Success)
        {
            Console.WriteLine("抽出したURL: " + match.Value);
        }
        else
        {
            Console.WriteLine("URLが見つかりませんでした。");
        }
    }
}
抽出したURL: https://www.example.com/

このパターンはhttpまたはhttpsで始まり、空白文字以外が続くURLにマッチします。

より厳密なURL検証が必要な場合はパターンを拡張してください。

日本語住所の番地抽出

日本語住所から番地部分を抽出する例です。

ここでは「丁目」「番地」「号」などの表記を対象にします。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "住所は東京都新宿区西新宿2丁目8-1です。";
        string pattern = @"(\d+丁目)?\s?([\d\-]+)";
        Match match = Regex.Match(input, pattern);
        if (match.Success)
        {
            Console.WriteLine("抽出した番地情報:");
            for (int i = 1; i < match.Groups.Count; i++)
            {
                if (!string.IsNullOrEmpty(match.Groups[i].Value))
                {
                    Console.WriteLine(match.Groups[i].Value);
                }
            }
        }
        else
        {
            Console.WriteLine("番地情報が見つかりませんでした。");
        }
    }
}
抽出した番地情報:
2丁目
8-1

この例では、丁目や番地、号の表記をキャプチャしています。

住所の表記は多様なので、必要に応じてパターンを調整してください。

CSVフィールド抽出

CSV形式の文字列からフィールドを抽出する例です。

カンマ区切りで、ダブルクォートで囲まれたフィールドも考慮します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "田中, \"山田, 太郎\", 30, \"東京都\"";
        string pattern = @"

            # フィールドのパターン

            (?:                         # 非キャプチャグループ開始
                ""                      # ダブルクォート開始
                (?:[^""]|"""")*         # ダブルクォート内の文字(""はエスケープ)
                ""                      # ダブルクォート終了
            |                           # または
                [^,""]*                 # カンマとダブルクォート以外の文字
            )
            (?:,|$)                     # カンマまたは行末
        ";
        var regex = new Regex(pattern, RegexOptions.IgnorePatternWhitespace);
        var matches = regex.Matches(input);
        Console.WriteLine("CSVフィールド:");
        foreach (Match m in matches)
        {
            string field = m.Value.TrimEnd(',');
            // ダブルクォートで囲まれている場合は中の""を"に置換
            if (field.StartsWith("\"") && field.EndsWith("\""))
            {
                field = field.Substring(1, field.Length - 2).Replace("\"\"", "\"");
            }
            Console.WriteLine(field);
        }
    }
}
CSVフィールド:
田中
山田, 太郎
 30
東京都

この正規表現は、ダブルクォートで囲まれたフィールド内のカンマを正しく扱い、フィールドを分割しています。

CSVの仕様に合わせて調整可能です。

ログ解析シナリオ

複数行ログエントリの処理

ログファイルには、1つのログエントリが複数行にわたるケースがあります。

例えば、例外のスタックトレースや詳細メッセージが改行を含む場合です。

こうした複数行ログを正規表現で処理するには、ログの区切りパターンを明確にし、複数行をまとめて抽出する方法が有効です。

以下は、ログの各エントリが日時で始まる形式を想定し、日時で区切って複数行を1つのエントリとして抽出する例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string log = @"2024-06-15 10:00:00 INFO 開始処理
詳細メッセージ1
詳細メッセージ2
2024-06-15 10:01:00 ERROR 例外発生
スタックトレース1
スタックトレース2
2024-06-15 10:02:00 INFO 終了処理";
        // 日時で始まる行を区切りとして複数行をまとめるパターン
        string pattern = @"(?m)^(?<entry>(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} .+?)(?=^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} |\z))";
        MatchCollection matches = Regex.Matches(log, pattern, RegexOptions.Singleline);
        int count = 1;
        foreach (Match match in matches)
        {
            Console.WriteLine($"ログエントリ {count++}:\n{match.Groups["entry"].Value}\n");
        }
    }
}
ログエントリ 1:
2024-06-15 10:00:00 INFO 開始処理
詳細メッセージ1
詳細メッセージ2


ログエントリ 2:
2024-06-15 10:01:00 ERROR 例外発生
スタックトレース1
スタックトレース2


ログエントリ 3:
2024-06-15 10:02:00 INFO 終了処理

この例では、(?m)でマルチラインモードを有効にし、^が各行の先頭にマッチするようにしています。

パターンは日時で始まる行から次の日時行まで(またはファイル末尾まで)を1つのグループとして抽出しています。

RegexOptions.Singlelineを指定することで、.が改行を含むすべての文字にマッチし、複数行をまとめて扱えます。

タイムスタンプとメッセージの分離

ログ解析でよくある処理として、ログのタイムスタンプ部分とメッセージ部分を分離して抽出することがあります。

以下は、日時とログレベル、メッセージをそれぞれキャプチャグループで分ける例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string logLine = "2024-06-15 10:01:00 ERROR 例外発生が発生しました。";
        string pattern = @"^(?<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+(?<level>\w+)\s+(?<message>.+)$";
        Match match = Regex.Match(logLine, pattern);
        if (match.Success)
        {
            Console.WriteLine($"タイムスタンプ: {match.Groups["timestamp"].Value}");
            Console.WriteLine($"ログレベル: {match.Groups["level"].Value}");
            Console.WriteLine($"メッセージ: {match.Groups["message"].Value}");
        }
        else
        {
            Console.WriteLine("ログ行の形式にマッチしませんでした。");
        }
    }
}
タイムスタンプ: 2024-06-15 10:01:00
ログレベル: ERROR
メッセージ: 例外発生が発生しました。

このパターンは、行の先頭から日時timestamp、空白、ログレベルlevel、空白、メッセージmessageをそれぞれキャプチャしています。

ログのフォーマットに合わせてパターンを調整することで、柔軟に解析できます。

入力検証との違い

抽出と検証の目的差

正規表現は主に「文字列から特定のパターンを抽出する」目的と、「入力が特定の形式に合致しているかを検証する」目的の2つで使われます。

この2つは似ているようで異なる点が多いため、使い分けが重要です。

  • 抽出

文字列の中から条件に合う部分だけを取り出すことが目的です。

例えば、文章中のメールアドレスや電話番号、日付などを抜き出す場合に使います。

抽出では部分一致が許容されることが多く、文字列全体がパターンに合致している必要はありません。

  • 検証(バリデーション)

入力値が期待される形式に完全に合致しているかを判定することが目的です。

例えば、ユーザーが入力したメールアドレスやパスワードが正しい形式かどうかをチェックします。

検証では文字列全体がパターンにマッチする必要があり、部分一致は不十分です。

この違いを踏まえ、抽出ではRegex.MatchesRegex.Matchで部分的にマッチするパターンを使い、検証では^(先頭)と$(末尾)を使って文字列全体を囲むパターンを使うことが一般的です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "test@example.com";
        // 抽出用パターン(部分一致)
        string extractPattern = @"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}";
        // 検証用パターン(完全一致)
        string validatePattern = @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";
        bool isExtracted = Regex.IsMatch(input, extractPattern);
        bool isValid = Regex.IsMatch(input, validatePattern);
        Console.WriteLine($"抽出結果: {isExtracted}");
        Console.WriteLine($"検証結果: {isValid}");
    }
}
抽出結果: True
検証結果: True

この例では、抽出用パターンは文字列の一部にメールアドレスがあればマッチしますが、検証用パターンは文字列全体がメールアドレス形式であることを要求しています。

正規表現でのバリデーション注意点

正規表現を使った入力検証は便利ですが、いくつか注意すべきポイントがあります。

  • 過度な複雑化を避ける

完全にRFC準拠したメールアドレスやURLの正規表現は非常に複雑で、可読性や保守性が低下します。

実務ではある程度妥協した簡易パターンを使うことが多いです。

  • 部分一致に注意

Regex.IsMatchは部分一致でもtrueを返すため、検証時は必ず^$で文字列全体を囲むパターンを使いましょう。

  • 入力の前後の空白を考慮

ユーザー入力には前後に空白が含まれることが多いため、検証前にTrim()で空白を除去するか、正規表現で空白を許容するか検討が必要です。

  • パフォーマンスに注意

複雑な正規表現は処理時間が長くなり、DoS攻撃のリスクもあります。

タイムアウト設定や簡潔なパターン設計を心がけましょう。

  • 正規表現以外の検証も併用

例えばメールアドレスの検証では、正規表現で形式をチェックした後に、実際にメール送信可能かどうかの検証を行うことが望ましいです。

// 入力検証の例(空白除去と完全一致パターン)
string input = "  user@example.com  ";
string trimmedInput = input.Trim();
string pattern = @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";
bool isValid = Regex.IsMatch(trimmedInput, pattern);
Console.WriteLine($"検証結果(空白除去後): {isValid}");

正規表現は強力なツールですが、検証の目的や制約を理解し、適切に設計・運用することが重要です。

例外処理とデバッグ

RegexParseExceptionの捕捉

正規表現パターンに誤りがある場合、Regexのコンストラクタやメソッド呼び出し時にRegexParseExceptionがスローされます。

この例外はパターンの構文エラーを示すため、適切に捕捉してエラーメッセージを確認することが重要です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string invalidPattern = @"(\d+"; // 括弧の閉じ忘れ
        try
        {
            var regex = new Regex(invalidPattern);
        }
        catch (RegexParseException ex)
        {
            Console.WriteLine("正規表現の構文エラーが発生しました。");
            Console.WriteLine($"メッセージ: {ex.Message}");
            Console.WriteLine($"エラー位置: {ex.Index}");
        }
    }
}
Program.cs(16,44): error CS1061:
'RegexParseException' に 'Index' の定義が含まれておらず、
型 'RegexParseException' の最初の引数を受け付けるアクセス可能な拡張メソッド
'Index' が見つかりませんでした。
using ディレクティブまたはアセンブリ参照が不足していないことを確認してください

この例では、開き括弧(に対応する閉じ括弧)が不足しているため、RegexParseExceptionが発生しています。

例外のMessageIndexプロパティを使ってエラー内容や位置を特定し、修正に役立てましょう。

Regex.Unescapeでデバッグ

正規表現のパターンやマッチ結果に含まれるエスケープシーケンスを人間が読みやすい形に変換するには、Regex.Unescapeメソッドが便利です。

特に、バックスラッシュや特殊文字が多いパターンのデバッグ時に役立ちます。

エスケープ前: \\d+\\.\\d+
Unescape後: \d+\.\d+
エスケープ前: \\d+\\.\\d+
Unescape後: \d+\.\d+

この例では、\d\.のエスケープが解除され、\が取り除かれています。

Regex.Unescapeは正規表現のパターンをそのまま文字列として表示したいときや、マッチ結果のエスケープ文字を除去したいときに使えます。

ただし、正規表現の意味を変えるわけではないため、パターンの動作確認には注意が必要です。

Visual StudioのRegexビジュアライザ

Visual Studioには、正規表現のパターンを視覚的に確認・デバッグできる「Regexビジュアライザ」が組み込まれています。

デバッグ中にRegexオブジェクトやパターン文字列の変数にカーソルを合わせ、虫眼鏡アイコンをクリックするとビジュアライザが開きます。

ビジュアライザでは以下のことが可能です。

  • パターンの構造をツリー形式で確認
  • マッチ対象の文字列に対するマッチ結果をリアルタイムで表示
  • グループやキャプチャの内容を視覚的に把握

これにより、複雑な正規表現の動作を直感的に理解しやすくなります。

また、Visual Studioの「検索と置換」ダイアログでも正規表現を使った検索が可能で、パターンのテストに役立ちます。

これらのツールや例外処理を活用することで、正規表現の開発やデバッグが効率的かつ安全に行えます。

特に複雑なパターンを扱う際は、エラーの早期発見と視覚的な確認が重要です。

セキュリティ考慮

ReDoS攻撃への対策

ReDoS(Regular Expression Denial of Service)攻撃は、悪意のある入力を使って正規表現の処理時間を意図的に長引かせ、サービスの応答を遅延させたり停止させたりする攻撃です。

特にバックトラッキングが多発する複雑な正規表現パターンが狙われやすいです。

ReDoS攻撃の特徴

  • 入力文字列が特定のパターンにマッチしないにもかかわらず、正規表現エンジンが膨大なバックトラッキングを行い処理が遅延します
  • サービスのリソースを消費し、DoS(サービス拒否)状態を引き起こす

対策方法

  1. 複雑なパターンの見直し

ネストした量指定子(例: (a+)+)や曖昧なパターンはバックトラッキングを増やすため避けます。

可能な限りシンプルで明確なパターンにします。

  1. タイムアウトの設定

.NETのRegexクラスは、コンストラクタや静的メソッドでTimeSpanによるタイムアウトを設定可能です。

処理が一定時間を超えた場合にRegexMatchTimeoutExceptionをスローし、攻撃を防ぎます。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = new string('a', 10000) + "!";
        string pattern = @"(a+)+!";
        try
        {
            var regex = new Regex(pattern, RegexOptions.None, TimeSpan.FromSeconds(1));
            bool isMatch = regex.IsMatch(input);
            Console.WriteLine("マッチ結果: " + isMatch);
        }
        catch (RegexMatchTimeoutException)
        {
            Console.WriteLine("正規表現の処理がタイムアウトしました。");
        }
    }
}
正規表現の処理がタイムアウトしました。
  1. 正規表現の事前検証

複雑なパターンを使う場合は、ReDoS脆弱性がないか専門ツールやオンラインチェッカーで検証します。

  1. 代替手段の検討

正規表現以外の文字列処理(例えばSpan<T>や手動パース)を検討し、バックトラッキングのリスクを回避します。

  1. 入力長の制限

入力文字列の長さを制限し、極端に長い文字列を処理しないようにします。

これらの対策を組み合わせて、ReDoS攻撃のリスクを最小限に抑えましょう。

不正入力のサニタイズ

正規表現を使う際に、ユーザーからの入力をそのままパターンに組み込むと、意図しない動作やセキュリティリスクが発生することがあります。

特に、入力に正規表現の特殊文字が含まれている場合は注意が必要です。

サニタイズの目的

  • 正規表現の構文エラーを防ぐ
  • 意図しないパターンマッチを防止する
  • セキュリティ上の脆弱性を回避する

サニタイズ方法

.NETではRegex.Escapeメソッドを使うことで、入力文字列中の正規表現の特殊文字をエスケープし、安全にパターンに組み込めます。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string userInput = "abc.*+?^${}()|[]\\def";
        string escapedInput = Regex.Escape(userInput);
        string pattern = @"^" + escapedInput + @"$";
        Console.WriteLine("エスケープ前: " + userInput);
        Console.WriteLine("エスケープ後: " + escapedInput);
        string test = "abc.*+?^${}()|[]\\def";
        bool isMatch = Regex.IsMatch(test, pattern);
        Console.WriteLine("マッチ結果: " + isMatch);
    }
}
エスケープ前: abc.*+?^${}()|[]\def
エスケープ後: abc\.\*\+\?\^\$\{}\(\)\|\[]\\def
マッチ結果: True

この例では、ユーザー入力に含まれる特殊文字がすべてエスケープされ、正規表現のパターンとして安全に扱えています。

その他の注意点

  • 入力の検証と制限

入力の長さや文字種を制限し、不正なデータを早期に排除します。

  • サニタイズのタイミング

正規表現に組み込む直前にエスケープ処理を行い、他の処理と混同しない。

  • ログやエラーメッセージの扱い

不正入力があった場合は詳細な情報をログに残しつつ、ユーザーには過度な情報を公開しない。

正規表現を安全に使うためには、入力のサニタイズと適切なエラーハンドリングが欠かせません。

これらを徹底してセキュリティリスクを低減しましょう。

よくあるミス

バックスラッシュの二重化

C#の正規表現パターンで最も多いミスの一つが、バックスラッシュ\の扱いです。

正規表現では\は特殊文字のエスケープに使われますが、C#の文字列リテラルでも\はエスケープ文字として使われるため、二重にエスケープする必要があります。

例えば、数字にマッチする\dを正しく表現するには、通常の文字列リテラルでは"\\d"と書きます。

これは、C#の文字列中で\を表すために\\と書く必要があるためです。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "abc123def";
        string pattern = "\\d+"; // 正しくは\\d+
        Match match = Regex.Match(input, pattern);
        if (match.Success)
        {
            Console.WriteLine($"マッチした数字: {match.Value}");
        }
    }
}
マッチした数字: 123

一方、逐語的文字列リテラル@""を使うと、\を1つだけ書けばよく、可読性が向上します。

string pattern = @"\d+";

バックスラッシュの二重化を忘れると、コンパイルエラーや意図しないパターンになるため注意が必要です。

改行コードの取り扱い

正規表現で改行コードを扱う際に、環境や文字列の改行コードの違いによるミスがよく発生します。

Windowsでは\r\n、Unix/LinuxやmacOSでは\nが改行コードとして使われるため、改行を表すパターンを適切に設計する必要があります。

例えば、改行にマッチさせたい場合は以下のようにします。

  • \r\n : Windowsの改行
  • \n : Unix/Linux/macOSの改行
  • \r : 古いMacの改行(ほとんど使われない)

複数の環境を想定する場合は、(\r\n|\n|\r)のように複数パターンを許容するか、\r?\n(\rがあってもなくてもよい)を使うことが多いです。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "行1\r\n行2\n行3\r行4";
        string pattern = @"行\d(\r\n|\n|\r)";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("改行を含むマッチ数: " + matches.Count);
    }
}
改行を含むマッチ数: 3

また、RegexOptions.Singlelineを使うと、.が改行文字にもマッチするようになりますが、改行コード自体の違いは考慮しません。

改行コードの違いを意識してパターンを設計しましょう。

Unicodeサロゲートペア

Unicodeのサロゲートペアは、基本多言語面(BMP)外の文字を表現するために2つの16ビットコードユニットを組み合わせて1文字を表します。

C#のstringはUTF-16で表現されているため、サロゲートペアを正しく扱わないと正規表現のマッチングで誤動作が起こることがあります。

例えば、絵文字や一部の特殊文字はサロゲートペアで表現されます。

正規表現で.を使うと、サロゲートペアの1つ目のコードユニットだけにマッチし、文字として正しく扱えない場合があります。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "😀"; // 絵文字(サロゲートペア)
        // . は1コードユニットにマッチ
        string pattern = ".";
        Match match = Regex.Match(input, pattern);
        Console.WriteLine($"マッチした文字数: {match.Length}");
        Console.WriteLine($"マッチした文字: {match.Value}");
    }
}
マッチした文字数: 1
マッチした文字: �

この例では、.がサロゲートペアの先頭コードユニットだけにマッチし、不完全な文字列が取得されてしまいます。

対策

  • \X(Unicodeグラフェムクラスタ)を使う

.NETの正規表現では\Xがグラフェムクラスタ(ユーザーが認識する1文字単位)にマッチします。

これを使うとサロゲートペアや結合文字も正しく扱えます。

string pattern = @"\X";
  • RegexOptions.ECMAScriptを避ける

ECMAScriptモードはUnicodeの扱いが限定的なので、サロゲートペアの処理に注意が必要です。

  • 文字列操作でSystem.Text.Runeを使う

C# 8.0以降はSystem.Text.Rune構造体を使い、Unicodeスカラー値単位で文字列を扱う方法もあります。

Unicodeサロゲートペアを正しく扱うことは、多言語対応や絵文字を含む文字列処理で重要です。

正規表現のパターン設計や文字列操作時に意識しましょう。

代替アプローチ

String.Containsとの性能比較

正規表現は強力で柔軟な文字列検索・抽出ツールですが、単純な部分文字列の存在チェックにはオーバーヘッドが大きくなることがあります。

String.Containsは単純な部分文字列検索に特化しており、正規表現よりも高速に動作する場合が多いです。

以下は、String.ContainsRegex.IsMatchで単純な文字列の存在チェックを比較した例です。

using System;
using System.Diagnostics;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = new string('a', 1000000) + "needle";
        string search = "needle";
        string pattern = Regex.Escape(search);
        var sw = new Stopwatch();
        // String.Containsによる検索
        sw.Start();
        bool containsResult = input.Contains(search);
        sw.Stop();
        Console.WriteLine($"String.Contains: {containsResult}, 時間: {sw.ElapsedMilliseconds}ms");
        // Regex.IsMatchによる検索
        sw.Restart();
        bool regexResult = Regex.IsMatch(input, pattern);
        sw.Stop();
        Console.WriteLine($"Regex.IsMatch: {regexResult}, 時間: {sw.ElapsedMilliseconds}ms");
    }
}
String.Contains: True, 時間: 0ms
Regex.IsMatch: True, 時間: 4ms

この例では、String.Containsが圧倒的に高速であることがわかります。

単純な部分文字列の存在チェックや固定文字列の検索にはString.Containsを優先的に使うことをおすすめします。

ただし、正規表現はパターンマッチングや複雑な条件抽出に強いため、用途に応じて使い分けることが重要です。

Span/MemoryのLow-Levelパース

C#のSpan<T>Memory<T>は、メモリ効率が高く高速なデータ操作を可能にする構造体で、文字列の部分切り出しやコピーを避けることができます。

これらを活用した低レベルのパース処理は、正規表現よりも高速かつ軽量に動作する場合があります。

以下は、Span<char>を使って文字列中の数字を抽出する簡単な例です。

using System;
class Program
{
    static void Main()
    {
        string input = "abc123def456ghi789";
        ReadOnlySpan<char> span = input.AsSpan();
        int start = -1;
        for (int i = 0; i < span.Length; i++)
        {
            if (char.IsDigit(span[i]))
            {
                if (start == -1) start = i;
            }
            else
            {
                if (start != -1)
                {
                    Console.WriteLine(span.Slice(start, i - start).ToString());
                    start = -1;
                }
            }
        }
        // 最後の数字列が文字列末尾の場合の処理
        if (start != -1)
        {
            Console.WriteLine(span.Slice(start).ToString());
        }
    }
}
123
456
789

このコードは、文字列をSpan<char>として扱い、ループで数字の連続部分を検出して抽出しています。

部分文字列の生成はSpan.Sliceで行い、不要なメモリ割り当てを抑えています。

Span/Memory活用のメリット

  • 高速処理

文字列のコピーや新規生成を避けるため、GC負荷が減り高速化。

  • 低メモリ消費

大量データやリアルタイム処理に適しています。

  • 柔軟な部分文字列操作

複雑なパターンマッチングを自前で実装可能です。

注意点

  • 正規表現のような複雑なパターンマッチングは自力で実装する必要があり、開発コストが高いでしょう
  • 可読性や保守性が低下しやすい

正規表現が万能ではない場面では、String.ContainsSpan/Memoryを活用した低レベルパースを検討すると良いでしょう。

パフォーマンス要件や処理内容に応じて適切な手法を選択してください。

社内コード規約例

パターンの命名ルール

正規表現パターンは複雑になりやすく、可読性や保守性を高めるために社内で命名ルールを設けることが重要です。

以下は一般的に推奨される命名ルールの例です。

  • 意味が明確な名前を付ける

何を表すパターンか一目でわかる名前にします。

例えば、メールアドレス用ならEmailPattern、電話番号用ならPhoneNumberPatternなど。

  • 定数または静的読み取り専用フィールドとして管理

パターンは変更されることが少ないため、conststatic readonlyで定義し、再利用しやすくします。

  • 接頭辞や接尾辞で用途を区別

例えば、抽出用パターンはExtract、検証用パターンはValidateを名前に含めると用途が明確になります。

  • 大文字とアンダースコアの使用

定数として扱う場合は、EMAIL_PATTERNのように大文字とアンダースコアで命名するケースもありますが、C#の一般的な命名規則に合わせてEmailPatternのようにキャメルケースを使うことが多いです。

public static class RegexPatterns
{
    public const string EmailPattern = @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";
    public const string PhoneNumberPattern = @"^\d{2,4}-\d{2,4}-\d{4}$";
    public const string DateExtractPattern = @"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})";
}

このようにパターンを一箇所にまとめることで、変更や管理が容易になります。

コメントとドキュメント化

正規表現はパターン自体が複雑で直感的に理解しづらいため、コード内に十分なコメントやドキュメントを付けることが必須です。

以下のポイントを参考にしてください。

  • パターンの目的を明記する

何を検証・抽出するためのパターンかを簡潔に説明します。

  • パターンの構造や重要な部分の解説

特に複雑なグループや量指定子、アンカーの意味をコメントで補足します。

  • 使用例や注意点を記載する

どのような入力に対応しているか、制限事項や既知の問題点も明示すると良いです。

  • XMLドキュメントコメントの活用

定数やメソッドに対してはXMLコメントを付け、IDEの補完やドキュメント生成に役立てます。

/// <summary>
/// メールアドレスの検証用正規表現パターン。
/// 英数字、記号(._%+-)を含み、@以降はドメイン形式を想定。
/// </summary>
public const string EmailPattern = @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";
/// <summary>
/// 日付抽出用パターン。年(4桁)、月(2桁)、日(2桁)を名前付きグループでキャプチャ。
/// 例: 2024-06-15
/// </summary>
public const string DateExtractPattern = @"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})";

コメント例:

// 電話番号パターン:市外局番-市内局番-加入者番号の形式(例: 03-1234-5678)
// ハイフンは必須、数字はそれぞれ2~4桁
public const string PhoneNumberPattern = @"^\d{2,4}-\d{2,4}-\d{4}$";

これらの命名ルールとコメントの徹底により、チーム内での正規表現の共有や保守がスムーズになり、バグの発生や誤用を防止できます。

社内規約として文書化し、コードレビュー時にもチェック項目に含めることをおすすめします。

より複雑なパターン例

条件付き正規表現

条件付き正規表現は、ある条件に応じてマッチするパターンを切り替える高度な機能です。

C#の正規表現では、(?(condition)yes-pattern|no-pattern)という構文で表現します。

conditionはグループの存在チェックや先読みなどが指定でき、条件が真の場合はyes-pattern、偽の場合はno-patternにマッチします。

例:グループの有無による条件分岐

以下の例は、名前付きグループnameが存在する場合はそのグループにマッチし、存在しない場合は別のパターンにマッチさせる例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input1 = "Hello John";
        string input2 = "Hello";
        // 名前付きグループ'name'が存在するかで条件分岐
        string pattern = @"Hello( (?<name>\w+))?(?(name)!, no name)";
        Match match1 = Regex.Match(input1, pattern);
        Match match2 = Regex.Match(input2, pattern);
        Console.WriteLine($"input1: {match1.Value}");
        Console.WriteLine($"input2: {match2.Value}");
    }
}
input1: Hello John!
input2: Hello no name

このパターンの解説:

  • Hello( (?<name>\w+))?

「Hello」の後にスペースと単語(名前)をオプションでキャプチャ。

  • (?(name)!, no name)

nameグループが存在すれば!をマッチ、なければno nameをマッチ。

このように条件付き正規表現を使うと、入力の有無やパターンの存在に応じて柔軟にマッチングを制御できます。

バランス構文

バランス構文は、入れ子構造の括弧やタグなど、開始と終了が対応するパターンを正規表現で扱うための高度なテクニックです。

C#の正規表現は「スタックベースのグループカウント機能」を使い、開き括弧と閉じ括弧のバランスをチェックできます。

例:括弧の対応をチェックするパターン

以下は、文字列が正しく対応した丸括弧で囲まれているかを判定する例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string pattern = @"^\((?>[^()]+|(?<Open>\()|(?<-Open>\)))*(?(Open)(?!))\)$";
        string[] inputs = {
            "(abc(def))",
            "(abc(def)",
            "abc(def))",
            "()",
            "((()))"
        };
        foreach (var input in inputs)
        {
            bool isMatch = Regex.IsMatch(input, pattern);
            Console.WriteLine($"{input} は正しい括弧の対応か: {isMatch}");
        }
    }
}
(abc(def)) は正しい括弧の対応か: True
(abc(def) は正しい括弧の対応か: False
abc(def)) は正しい括弧の対応か: False
() は正しい括弧の対応か: True
((())) は正しい括弧の対応か: True

このパターンのポイント:

  • (?<Open>\()

開き括弧が見つかるたびにOpenスタックにプッシュ。

  • (?<-Open>\))

閉じ括弧が見つかるたびにOpenスタックからポップ。

  • (?(Open)(?!))

最後にOpenスタックが空でなければマッチ失敗(未対応の開き括弧あり)。

  • (?>[^()]+|...)

原子グループでバックトラッキングを抑制。

このようにバランス構文を使うと、正規表現だけで入れ子構造の整合性をチェックでき、パーサーを使わずに簡易的な構文解析が可能です。

ただし、パターンが複雑でパフォーマンスに影響するため、必要な場合に限定して使うことをおすすめします。

テストコードの書き方

xUnitでのデータ駆動テスト

xUnitは.NETで広く使われている単体テストフレームワークで、データ駆動テスト(パラメータ化テスト)を簡単に実装できます。

正規表現のテストでは、複数の入力と期待結果を組み合わせて効率的に検証するのに便利です。

以下は、xUnitで正規表現のマッチングをデータ駆動でテストする例です。

using System.Text.RegularExpressions;
using Xunit;
public class RegexTests
{
    // テストデータを属性で指定
    [Theory]
    [InlineData("abc123", @"\d+", true, "123")]
    [InlineData("no digits", @"\d+", false, "")]
    [InlineData("2024-06-15", @"\d{4}-\d{2}-\d{2}", true, "2024-06-15")]
    [InlineData("email@example.com", @"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", true, "email@example.com")]
    public void TestRegexMatch(string input, string pattern, bool expectedSuccess, string expectedValue)
    {
        var match = Regex.Match(input, pattern);
        Assert.Equal(expectedSuccess, match.Success);
        if (expectedSuccess)
        {
            Assert.Equal(expectedValue, match.Value);
        }
    }
}

このコードのポイント:

  • [Theory]属性でデータ駆動テストを宣言
  • [InlineData]で複数のテストケースを指定
  • 各ケースで入力文字列、正規表現パターン、マッチ成功の期待値、マッチした値の期待値を渡します
  • Assertで結果を検証

この方法により、複数のパターンや入力を一括でテストでき、テストコードの重複を減らせます。

BenchmarkDotNetで性能計測

BenchmarkDotNetは.NET向けの高精度ベンチマークライブラリで、正規表現の性能比較や最適化効果の検証に適しています。

簡単にベンチマークコードを書き、詳細なレポートを取得できます。

以下は、Regex.Matchの通常版とRegexOptions.Compiled版の性能を比較する例です。

using System;
using System.Text.RegularExpressions;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
public class RegexBenchmark
{
    private string input = "abc123def456ghi789";
    private string pattern = @"\d+";
    private Regex regexNormal;
    private Regex regexCompiled;
    public RegexBenchmark()
    {
        regexNormal = new Regex(pattern);
        regexCompiled = new Regex(pattern, RegexOptions.Compiled);
    }
    [Benchmark]
    public int NormalRegex()
    {
        int count = 0;
        var matches = regexNormal.Matches(input);
        foreach (Match m in matches)
        {
            count += m.Length;
        }
        return count;
    }
    [Benchmark]
    public int CompiledRegex()
    {
        int count = 0;
        var matches = regexCompiled.Matches(input);
        foreach (Match m in matches)
        {
            count += m.Length;
        }
        return count;
    }
}
class Program
{
    static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run<RegexBenchmark>();
    }
}

実行すると、BenchmarkDotNetが自動的にウォームアップや複数回の測定を行い、平均実行時間やメモリ使用量など詳細な結果をコンソールやHTMLレポートで出力します。

ベンチマークのポイント

  • Benchmark属性を付けたメソッドが測定対象
  • コンストラクタで初期化したRegexオブジェクトを使い回すことで、実行時のオーバーヘッドを抑制
  • 実際の処理内容をシンプルにし、純粋な正規表現の性能差を測定

BenchmarkDotNetを使うことで、正規表現の最適化効果やパターンの違いによる性能差を客観的に評価でき、パフォーマンス改善の指針になります。

まとめ

この記事では、C#のRegexクラスを使った正規表現の基本から応用まで幅広く解説しました。

MatchMatchesの使い方、キャプチャグループの活用、量指定子やアンカーの特徴、先読み・後読みのテクニック、パフォーマンス最適化やセキュリティ対策まで網羅しています。

さらに、実践的なサンプルやテストコードの書き方、デバッグ方法も紹介し、効率的かつ安全に正規表現を活用するための知識が身につきます。

関連記事

Back to top button