【C#】正規表現で文字列を高速抽出するRegex.MatchとMatchesの使い方完全入門
C#で文字列から任意のパターンを抜き出すならSystem.Text.RegularExpressions.Regex
を使うのが定番です。
Regex.Match
は最初の一致を、Regex.Matches
はすべての一致を返し、キャプチャグループで部分要素も取り出せます。
例えば数字列は\d+
で取得でき、オプションで大小文字や複数行も制御できます。
シンプルな記述で高速にパターン抽出が完結します。
Regexクラスの基本
C#で正規表現を扱う際に中心となるのがSystem.Text.RegularExpressions
名前空間にあるRegex
クラスです。
このクラスは文字列のパターンマッチングや抽出、置換などを簡単に実現できる強力なツールです。
ここでは、Regex
クラスの基本的な使い方や生成方法、正規表現パターンの扱い方について詳しく解説します。
Regexオブジェクト生成方法
Regex
クラスのインスタンスを生成する方法は主に3つあります。
用途やパフォーマンス要件に応じて使い分けることが重要です。
簡易生成: 静的メソッド利用
最も手軽に正規表現を使いたい場合は、Regex
クラスの静的メソッドを利用します。
たとえば、Regex.Match
やRegex.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()
は次のマッチがない場合、Success
がfalse
のMatch
オブジェクトを返すため、ループの終了条件として使えます。
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
この例では、電話番号の形式にマッチするすべての文字列を抽出しています。
MatchCollection
はIEnumerable
を実装しているため、foreach
で簡単に走査できます。
MatchCollectionの走査方法
MatchCollection
はIEnumerable
を実装しているため、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と組み合わせた抽出
MatchCollection
はIEnumerable
ですが、ジェネリックではないため、そのままでは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のSelect
やWhere
などの拡張メソッドを利用できます。
条件で絞り込みたい場合も簡単に行えます。
// 数字が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
が単語として独立している場合のみマッチします。
scatter
やcategory
の中の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
が続く場合のみマッチします。
最後のapple
はpie
が続かないためマッチしません。
先読みの部分はマッチ結果に含まれず、m.Value
はapple
だけです。
ネガティブ先読み
ネガティブ先読みは、あるパターンの直後に特定の文字列やパターンが続かない場合にマッチさせます。
構文は(?!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.Multiline
とRegexOptions.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>
は、メモリ効率が高く高速なデータ操作を可能にします。
Regex
はReadOnlySpan<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.Matches
やRegex.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
が発生しています。
例外のMessage
やIndex
プロパティを使ってエラー内容や位置を特定し、修正に役立てましょう。
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(サービス拒否)状態を引き起こす
対策方法
- 複雑なパターンの見直し
ネストした量指定子(例: (a+)+
)や曖昧なパターンはバックトラッキングを増やすため避けます。
可能な限りシンプルで明確なパターンにします。
- タイムアウトの設定
.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("正規表現の処理がタイムアウトしました。");
}
}
}
正規表現の処理がタイムアウトしました。
- 正規表現の事前検証
複雑なパターンを使う場合は、ReDoS脆弱性がないか専門ツールやオンラインチェッカーで検証します。
- 代替手段の検討
正規表現以外の文字列処理(例えばSpan<T>
や手動パース)を検討し、バックトラッキングのリスクを回避します。
- 入力長の制限
入力文字列の長さを制限し、極端に長い文字列を処理しないようにします。
これらの対策を組み合わせて、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.Contains
とRegex.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.Contains
やSpan/Memory
を活用した低レベルパースを検討すると良いでしょう。
パフォーマンス要件や処理内容に応じて適切な手法を選択してください。
社内コード規約例
パターンの命名ルール
正規表現パターンは複雑になりやすく、可読性や保守性を高めるために社内で命名ルールを設けることが重要です。
以下は一般的に推奨される命名ルールの例です。
- 意味が明確な名前を付ける
何を表すパターンか一目でわかる名前にします。
例えば、メールアドレス用ならEmailPattern
、電話番号用ならPhoneNumberPattern
など。
- 定数または静的読み取り専用フィールドとして管理
パターンは変更されることが少ないため、const
やstatic 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
クラスを使った正規表現の基本から応用まで幅広く解説しました。
Match
やMatches
の使い方、キャプチャグループの活用、量指定子やアンカーの特徴、先読み・後読みのテクニック、パフォーマンス最適化やセキュリティ対策まで網羅しています。
さらに、実践的なサンプルやテストコードの書き方、デバッグ方法も紹介し、効率的かつ安全に正規表現を活用するための知識が身につきます。