C#を用いたアプリケーション開発において、テキストデータから特定の情報を抜き出す作業は非常に頻繁に発生します。

ログファイルからのエラーメッセージ取得、Webスクレイピング、ユーザー入力の整形など、その用途は多岐にわたります。

こうした文字列操作を効率的、かつ柔軟に行うための強力なツールが「正規表現 (Regular Expression)」です。

C#では System.Text.RegularExpressions 名前空間が提供されており、これを利用することで複雑なパターンマッチングと抽出を簡潔なコードで実装できます。

しかし、正規表現は多機能ゆえに、「単一の抽出」「複数箇所の抽出」「グループ化による特定部位の抜き出し」など、目的に応じた適切なメソッドを選択することが重要です。

本記事では、C#で正規表現を用いた文字列抽出を行うための実践的なテクニックを、基礎から最新の .NET 8/9 に対応した最適化手法まで詳しく解説します。

C#における正規表現抽出の基本

C#で正規表現を扱う中心となるのは Regexクラス です。

文字列の抽出を行う場合、主に Regex.Match メソッドと Regex.Matches メソッドの2種類を使い分けます。

正規表現を使用する際は、まず以下の名前空間をインポートする必要があります。

C#
using System.Text.RegularExpressions;

抽出処理の基本的な流れは、「対象となる文字列」に対して「検索パターン」を定義し、その結果を「Matchオブジェクト」として受け取るという手順になります。

C#の正規表現エンジンは非常に高機能で、Perl 5 互換の構文をベースにしつつ、名前付きグループ化後読み・先読みの言明 など、高度な機能もサポートしています。

単一の文字列を抽出する:Regex.Matchの使い方

特定のパターンに一致する最初の箇所だけを抽出したい場合は、Regex.Match メソッドを使用します。

このメソッドは、一致した結果を保持する Match オブジェクトを返します。

基本的な実装パターン

以下の例では、文章の中から最初に出現する電話番号の形式 (数字3つ-数字4つ-数字4つ) を抽出します。

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

public class Program
{
    public static void Main()
    {
        string input = "お問い合わせは 012-3456-7890 または 090-1111-2222 まで。";
        string pattern = @"\d{3}-\d{4}-\d{4}";

        // Regex.Matchで最初の1件を取得
        Match match = Regex.Match(input, pattern);

        if (match.Success)
        {
            // Successプロパティで一致したか確認
            Console.WriteLine("抽出成功: " + match.Value);
        }
        else
        {
            Console.WriteLine("一致する箇所が見つかりませんでした。");
        }
    }
}
実行結果
抽出成功: 012-3456-7890

注意点として、Regex.Match は最初に見つかった1件のみを対象とします。

複数の電話番号が文字列内に存在しても、このメソッドでは2件目以降は無視されます。

また、一致しなかった場合でも null は返らず、Success プロパティが false になった Match.Empty が返されるため、必ず match.Success で判定を行うのが安全なコーディング規約です。

グループ化を利用した詳細な抽出

正規表現の真価は、単にパターンに一致する文字列全体を抽出するだけでなく、その中の 特定の部分だけを取り出す「グループ化」 にあります。

インデックスによるグループ参照

丸括弧 () を使用してパターンを囲むことで、その部分を「グループ」として扱えます。

抽出された各グループには、左から順番にインデックス番号 (1, 2, 3…) が割り振られます。

なお、インデックス 0 はマッチした文字列全体を指します。

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

public class Program
{
    public static void Main()
    {
        string input = "商品コード: ID_9987 (在庫あり)";
        // ID_ の後の数字部分だけをグループ化して抽出
        string pattern = @"ID_(\d+)";

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

        if (match.Success)
        {
            // match.Groups[0] は "ID_9987" 全体
            // match.Groups[1] は "9987" の部分のみ
            Console.WriteLine("全体: " + match.Groups[0].Value);
            Console.WriteLine("抽出部分: " + match.Groups[1].Value);
        }
    }
}
実行結果
全体: ID_9987
抽出部分: 9987

名前付きグループによる抽出

インデックス番号による参照は、正規表現が複雑になると「何番目の括弧がどのデータか」が分かりづらくなり、保守性が低下します。

これを解決するのが 名前付きグループ (Named Groups) です。

構文は (?<name>pattern) のように記述します。

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

public class Program
{
    public static void Main()
    {
        string input = "2024/05/20 15:30:45 [ERROR] システム障害が発生しました。";
        // 日付、レベル、メッセージを名前付きで抽出
        string pattern = @"(?<date>\d{4}/\d{2}/\d{2}).*?\[(?<level>\w+)\].*?(?<msg>.+)";

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

        if (match.Success)
        {
            Console.WriteLine("日付: " + match.Groups["date"].Value);
            Console.WriteLine("レベル: " + match.Groups["level"].Value);
            Console.WriteLine("内容: " + match.Groups["msg"].Value.Trim());
        }
    }
}
実行結果
日付: 2024/05/20
レベル: ERROR
内容: システム障害が発生しました。

名前付きグループを使用することで、コードの可読性が飛躍的に向上します。

開発現場では、将来的なパターンの変更に強くなるため、この手法が推奨されます。

複数の文字列をまとめて抽出する:Regex.Matches

対象文字列の中に該当するパターンが複数存在し、そのすべてをリスト形式で取得したい場合は Regex.Matches メソッドを使用します。

このメソッドは MatchCollection オブジェクトを返します。

全一致箇所の列挙

MatchCollection は列挙可能 (IEnumerable) であるため、foreach 文で簡単にループ処理が可能です。

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

public class Program
{
    public static void Main()
    {
        string input = "価格は 1,200円、2,500円、そして最後は 15,000円です。";
        // 数字とカンマの組み合わせを抽出
        string pattern = @"[\d,]+";

        MatchCollection matches = Regex.Matches(input, pattern);

        Console.WriteLine($"{matches.Count} 件の価格が見つかりました:");

        foreach (Match m in matches)
        {
            Console.WriteLine("- " + m.Value);
        }
    }
}
実行結果
3 件の価格が見つかりました:
- 1,200
- 2,500
- 15,000

LINQを活用した抽出

MatchCollection は .NET の LINQ と非常に相性が良いです。

特定の条件でフィルタリングしたり、抽出結果を List<string> に変換したりする作業が簡潔に記述できます。

C#
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        string input = "UserA: 50pt, UserB: 120pt, UserC: 80pt";
        string pattern = @"(\d+)pt";

        // LINQを使用して80以上の数値だけをリスト化
        List<int> highScores = Regex.Matches(input, pattern)
                                    .Cast<Match>()
                                    .Select(m => int.Parse(m.Groups[1].Value))
                                    .Where(score => score >= 80)
                                    .ToList();

        highScores.ForEach(s => Console.WriteLine("High Score: " + s));
    }
}
実行結果
High Score: 120
High Score: 80

正規表現オプションによる抽出の制御

抽出精度を高めるために、RegexOptions 列挙型を利用してエンジンの動作をカスタマイズできます。

オプション内容
IgnoreCase大文字と小文字を区別せずに検索する。
Multiline^$ を行の始まりと終わりに一致させる。
Singlelineドット . が改行文字 \n にも一致するようにする。
Compiled正規表現をMSILコードにコンパイルして高速化する(初期化コストは増える)。
IgnorePatternWhitespaceパターン内の空白を無視し、コメントを記述可能にする。

実装例:複数行からの抽出

C#
string input = "apple\nBanana\nCHERRY";
// 大文字小文字を無視して a で終わる単語を抽出
var matches = Regex.Matches(input, @"\b\w*a\b", RegexOptions.IgnoreCase);

.NET 7以降の最適解:Source Generatorによる抽出

従来の C# では、正規表現は実行時にパース(解析)されるため、オーバーヘッドが存在しました。

しかし、.NET 7 および .NET 8/9 では、正規表現ソースジェネレーター (Source Generators) が導入され、ビルド時に抽出ロジックを C# コードとして生成できるようになりました。

これにより、実行時のパフォーマンスが劇的に向上し、さらに「トリミング(AOTコンパイル)」時の互換性も確保されます。

Source Generator の使い方

partial クラス内で、[GeneratedRegex] 属性を付与した partial メソッドを定義します。

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

public partial class MyExtractor
{
    // ビルド時に正規表現の解析が行われる
    [GeneratedRegex(@"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", RegexOptions.IgnoreCase)]
    private static partial Regex EmailRegex();

    public static void ExtractEmails(string text)
    {
        var matches = EmailRegex().Matches(text);
        foreach (Match match in matches)
        {
            Console.WriteLine("Found Email: " + match.Value);
        }
    }
}

この手法の利点は、正規表現の構文エラーがコンパイル時にチェックされること、そして 実行時のパフォーマンスが最高速であること です。

大規模なログ解析や高頻度のリクエストを捌くサーバーサイドの実装では、この方法が現在最も推奨されるアプローチです。

正規表現抽出の実践的なユースケース

ここでは、開発現場でよく遭遇する具体的な抽出シナリオを紹介します。

HTMLタグ内のテキストを抽出する

特定のHTMLタグ(例:<a>タグ)からリンクテキストとURLを抽出する場合です。

C#
string html = "<a href=\"https://example.com\">公式サイトはこちら</a>";
string pattern = @"<a\s+href=""(?<url>[^""]+)"">(?<text>.*?)";

Match m = Regex.Match(html, pattern);
if (m.Success)
{
    Console.WriteLine("URL: " + m.Groups["url"].Value);
    Console.WriteLine("Text: " + m.Groups["text"].Value);
}

非キャプチャグループによる効率化

グループ化はしたいが、結果として抽出(メモリ保持)する必要がない箇所には、非キャプチャグループ (?:...) を使用します。

これにより、わずかですがパフォーマンスが向上します。

C#
// (?:https|http) 部分は Groups に含まれない
string pattern = @"(?:https|http)://([\w.-]+)";

パフォーマンスと注意点

正規表現は強力ですが、使い方を誤ると「バックトラッキングの爆発」と呼ばれる現象により、CPU使用率が100%に張り付くなどの致命的な問題を引き起こす可能性があります。

コンパイルの再利用

ループ内でnew Regex(pattern)を毎回生成するとコンパイルコストが発生して性能に悪影響を与えます。

静的にインスタンスを保持するか、ビルド時に最適化されたソースジェネレーターを利用してRegexインスタンスを再利用してください。

タイムアウトの設定

外部からの信頼できない入力を扱う場合は、必ずMatchTimeoutを指定して無限の処理や極端に長い実行時間を防止してください。

タイムアウトを設定することは安全対策として重要です。

C#
// 1秒以内に処理が終わらなければ例外を投げる
var regex = new Regex(pattern, RegexOptions.None, TimeSpan.FromSeconds(1));
欲張りすぎないマッチング

デフォルトの量指定子(*+)は貪欲(Greedy)で、期待より広く一致することがあります。

必要最小限の範囲で一致させたい場合は、?+?などの最短一致(Lazy)を使用して過剰マッチやバックトラッキングを抑制してください。

まとめ

C#における正規表現を用いた文字列抽出は、単純なパターンから複雑なデータ構造の解析まで幅広く対応できる強力な機能です。

  • 最初の1件だけなら Regex.Match
  • すべての候補を網羅するなら Regex.Matches
  • 複雑なデータ抽出には 名前付きグループ
  • 性能を追求するなら最新の Source Generator

これらの手法を適切に使い分けることで、堅牢でメンテナンス性の高いテキスト処理ロジックを構築できます。

正規表現は一見難解に見えますが、C#の提供する豊富なAPIとツールを活用すれば、開発の生産性を劇的に向上させる武器になります。

まずは小さなパターンから試し、徐々に実戦的な抽出ロジックへとステップアップしていきましょう。