C#の開発において、文字列のパターンマッチングや抽出、置換は避けて通れない作業です。

正規表現 (Regular Expression) は、特定のパターンに従った文字列を効率的に処理するための強力なツールであり、.NET環境では System.Text.RegularExpressions 名前空間を通じて非常に高度な機能が提供されています。

かつての正規表現は「便利だが実行速度が遅い」と言われることもありましたが、近年の.NET (特に .NET 7 以降) では ソース生成 (Source Generators) をはじめとする驚異的な進化を遂げており、パフォーマンス面でも非常に優れた選択肢となっています。

本記事では、C#における正規表現の基礎から、実戦で役立つサンプルコード、そして最新の高速化手法までをプロの視点で徹底的に解説します。

正規表現の基本概念と.NETにおける役割

正規表現とは、特定の「文字の並び」を記号を用いてパターン化して表現する手法です。

C#において正規表現を利用する主な目的は、大きく分けて以下の4つに集約されます。

  1. バリデーション (検証): 入力された文字列がメールアドレスや電話番号、郵便番号などの形式に従っているかを確認します。
  2. 検索・抽出: 長大なテキストの中から、特定のルールに合致する部分を取り出します。
  3. 置換: 特定のパターンに一致する箇所を、別の文字列や動的に生成した文字列に書き換えます。
  4. 分割: カンマ区切りだけでなく、複雑なパターンに基づいて文字列を分割します。

.NETの正規表現エンジンは バックトラッキング方式 を採用しており、非常に柔軟な記述が可能です。

また、Unicodeを標準でサポートしているため、日本語を含む多言語テキストの処理にも強いという特徴があります。

System.Text.RegularExpressions名前空間の主要クラス

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

まずは、この中の主要なクラスの役割を整理しましょう。

Regexクラスの主要メソッド

Regex クラスは、正規表現操作のメインエントリポイントです。

以下のメソッドが頻繁に使用されます。

メソッド名説明
IsMatch指定したパターンが文字列内に存在するかどうかを bool で返します。
Match最初に見つかった一致箇所を Match オブジェクトとして返します。
Matches一致するすべての箇所を MatchCollection として返します。
Replace一致した箇所を別の文字列に置換します。
Split正規表現パターンを区切り文字として文字列を分割します。

MatchとGroupの階層構造

正規表現で一致した結果は、階層構造で管理されます。

  • Match: 検索で見つかった1つの固まり全体を指します。
  • Group: 正規表現内で括弧 () を使ってグループ化した部分の抽出結果を指します。
  • Capture: 量指定子 (例えば +*) を使用した際に、個別のキャプチャ履歴を保持します。

基本的な正規表現の書き方とメタ文字一覧

C#で正規表現を記述する際、文字列リテラルの前に @ を付ける 逐語的文字列リテラル を使用するのが一般的です。

これにより、バックスラッシュ \ をエスケープせずに記述できるようになります。

文字クラスと量指定子

よく使われるメタ文字を以下の表にまとめます。

メタ文字説明
.任意の1文字 (改行を除く)a.c は “abc”, “axc” などに一致
\d任意の数字 (0-9)\d{3} は “123” などに一致
\w英数字およびアンダースコア\w+ は単語に一致
\s空白文字 (スペース、タブ等)\s+ は連続した空白に一致
[abc]括弧内のいずれかの文字[a-z] は小文字英字に一致
*0回以上の繰り返しba* は “b”, “ba”, “baa” に一致
+1回以上の繰り返しba+ は “ba”, “baa” に一致
?0回または1回ba? は “b”, “ba” に一致
{n,m}n回以上m回以下の繰り返し\d{2,4} は2桁から4桁の数字

アンカーと境界

文字列の「位置」を指定するためのメタ文字も重要です。

メタ文字説明
^文字列の先頭 (行頭)
$文字列の末尾 (行末)
\b単語の境界

C#における正規表現の実装パターン

それでは、実際のC#プログラムでどのように正規表現を記述するのか、基本的なコードサンプルを見ていきましょう。

基本的なマッチングと抽出

以下のコードは、文字列の中から電話番号の形式を探し、抽出する例です。

C#
using System;
using System.Text.RegularExpressions;

public class RegexExample
{
    public static void Main()
    {
        string input = "お問い合せ窓口は 03-1234-5678 または 090-9999-8888 です。";
        // 電話番号のパターン: 2~4桁-2~4桁-4桁
        string pattern = @"\d{2,4}-\d{2,4}-\d{4}";

        // Matchesメソッドですべての一致箇所を取得
        MatchCollection matches = Regex.Matches(input, pattern);

        Console.WriteLine($"{matches.Count} 件の電話番号が見つかりました。");

        foreach (Match match in matches)
        {
            // Indexで出現位置、Valueで一致した文字列を取得
            Console.WriteLine($"見つかった番号: {match.Value} (位置: {match.Index})");
        }
    }
}
実行結果
2 件の電話番号が見つかりました。
見つかった番号: 03-1234-5678 (位置: 9)
見つかった番号: 090-9999-8888 (位置: 22)

インスタンス化 vs 静的メソッド

C#の Regex クラスには、静的 (static) メソッドとインスタンスメソッドの2つの呼び出し方があります。

  • 静的メソッド: Regex.IsMatch(input, pattern) のように呼び出します。内部で直近に使用されたパターンがキャッシュされるため、使い回しが少ない場合に便利です。
  • インスタンスメソッド: var rx = new Regex(pattern); のようにオブジェクトを生成します。同じパターンをループ内で何度も利用する場合、インスタンスを再利用することでパターンの解析コストを抑えられます。

高度な正規表現テクニック

実務では、単に一致するかどうかだけでなく、複雑な情報の切り出しが必要になります。

名前付きキャプチャグループの活用

キャプチャグループに名前を付けることで、コードの可読性を劇的に向上させることができます。

書式は (?<name>pattern) です。

C#
using System;
using System.Text.RegularExpressions;

public class NamedGroupExample
{
    public static void Main()
    {
        string input = "田中 太郎, 1990/05/15, 東京都";
        // 日付部分を名前付きグループで抽出
        string pattern = @"(?<year>\d{4})/(?<month>\d{2})/(?<day>\d{2})";

        Match match = Regex.Match(input, pattern);

        if (match.Success)
        {
            // 名前を指定してグループの値を取得
            string year = match.Groups["year"].Value;
            string month = match.Groups["month"].Value;
            string day = match.Groups["day"].Value;

            Console.WriteLine($"年: {year}, 月: {month}, 日: {day}");
        }
    }
}
実行結果
年: 1990, 月: 05, 日: 15

非キャプチャグループと後読み・先読み

「特定の文字が後に続く場合だけマッチさせたいが、その文字自体は抽出結果に含めたくない」といった場合には、先読み (Lookahead)後読み (Lookbehind) を使用します。

  • 肯定先読み (?=pattern): 後ろに pattern が続く場合に一致。
  • 肯定後読み (?<=pattern): 前に pattern がある場合に一致。

例えば、100円2000円 という文字列から数値部分だけを「円」が付いていることを条件に抜き出したい場合は、\d+(?=円) というパターンが有効です。

【重要】パフォーマンスの最適化手法

正規表現は便利ですが、使い方を誤るとアプリケーションのパフォーマンスを著しく低下させます。

特に高負荷なシステムやリアルタイム性が求められる処理では、以下の最適化を必ず検討してください。

RegexOptions.Compiledの功罪

歴史的に、C#で正規表現を高速化する一般的な手法は RegexOptions.Compiled フラグを指定することでした。

C#
var rx = new Regex(@"\d+", RegexOptions.Compiled);

このオプションを指定すると、正規表現をランタイム時にMSILコードにコンパイルします。

  • メリット: 一度コンパイルが終われば、マッチング速度は非常に高速になります。
  • デメリット: 初回のコンパイルコスト (CPU・メモリ) が高く、アプリケーションの起動時間が延びる可能性があります。また、一度コンパイルされたコードはアンロードされにくいため、大量の異なるパターンをコンパイルするとメモリを圧迫します。

.NET 7以降のソース生成(GeneratedRegex)

現代のC# (特に .NET 7 / .NET 8 以降) における 最適解 は、GeneratedRegexAttribute を使用した ソース生成 (Source Generators) です。

これは、コンパイル時に正規表現を解析し、それを実行するためのC#コードを自動生成する仕組みです。

C#
using System;
using System.Text.RegularExpressions;

public partial class MyValidator
{
    // コンパイル時にこのパターンの解析コードが生成される
    [GeneratedRegex(@"^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$")]
    private static partial Regex EmailRegex();

    public void Validate(string email)
    {
        if (EmailRegex().IsMatch(email))
        {
            Console.WriteLine("有効なメールアドレスです。");
        }
    }
}

ソース生成の利点:

  1. 実行時コストのゼロ化: 実行時のパターンの解析やコンパイルが不要になります。
  2. トリミング・AOT親和性: ネイティブAOT (Ahead-Of-Time) コンパイル時に、正規表現エンジン全体を含める必要がなくなり、バイナリサイズを削減できます。
  3. デバッグの容易さ: 生成されたC#コードをステップ実行して、正規表現がどのように処理されているかを確認できます。

ReadOnlySpan<char>によるメモリ効率化

最新の.NETでは、文字列のコピーを避け、メモリ効率を高めるために ReadOnlySpan<char> をサポートしています。

Regex クラスのメソッドもこれに対応しており、大きな文字列の一部をスライスして処理する際に、余計なメモリ割り当て (Allocation) を発生させずにマッチングを行うことが可能です。

正規表現のセキュリティとタイムアウト設定

正規表現を利用する上で絶対に無視できないのが、ReDoS (Regular Expression Denial of Service) 攻撃です。

ReDoS攻撃の脅威

複雑なパターン(特にグループ化と繰り返しが組み合わさったもの)に対して、意図的に細工された長い文字列を入力すると、指数関数的に計算量が増大し、CPU使用率が100%に張り付いてサービスが停止してしまうことがあります。

悪意のある入力によってサーバーがダウンするリスクを常に考慮しなければなりません。

タイムアウト値の適切な設定方法

ReDoS対策として最も確実な方法は、タイムアウト値 を設定することです。

.NETでは、インスタンス生成時や静的メソッドの引数として TimeSpan を指定できます。

C#
try
{
    // マッチングに100ミリ秒以上かかったら中断する
    var options = RegexOptions.None;
    var timeout = TimeSpan.FromMilliseconds(100);
    bool isMatch = Regex.IsMatch(input, pattern, options, timeout);
}
catch (RegexMatchTimeoutException)
{
    // タイムアウト発生時の処理
    Console.WriteLine("正規表現の処理がタイムアウトしました。");
}

デフォルトではタイムアウトが無制限になっていることが多いため、不特定多数のユーザーが入力を提供するWebアプリケーションなどでは、必ず明示的なタイムアウトを設定すること が推奨されます。

まとめ

C#における正規表現は、単なる文字列処理のライブラリを超え、言語の進化と共に高度な最適化が行われる重要なコンポーネントとなっています。

本記事のポイントを振り返ります。

  • Regex クラスを中心に、MatchGroup を使い分けて情報を抽出する。
  • 名前付きグループ (?<name>...) を活用して、コードのメンテナンス性を高める。
  • 最新のプロジェクトでは ソース生成 ([GeneratedRegex]) を第一選択とし、実行時のパフォーマンスと起動速度を両立させる。
  • セキュリティのために タイムアウト設定 を忘れずに行い、ReDoS攻撃からアプリケーションを守る。

正規表現をマスターすることで、複雑なデータ加工やバリデーションを数行のコードで実装できるようになります。

本記事で紹介したテクニックと最新機能を活用し、より高速で安全なC#プログラミングを実践してください。