正規表現(regex)は、テキストを表すパターンです。「4桁の数字、ダッシュ、2桁の数字」「大文字で始まる単語」「角かっこの間にあるもの」などです。C#では、System.Text.RegularExpressions の Regex クラスが、そうしたパターンでテキストを検索し、取り出し、置換し、分割します。
IsMatch、Match、Matches
ほとんどの検索は3つのメソッドでまかなえます。IsMatch ははい・いいえで答え、Match は最初の一致を、Matches はすべての一致を返します。
出力:
True
2026-03-14 at index 22
4 matches:
1042
2026
1043
2026
Success: False, Value: []
失敗した Match は null を返しません。Success が false で Value が空の Match を返します。一致を使う前に、必ず Success を確認します。
4桁の検索は、パターンが意図より多くのものを拾う様子を示しています。\b\d{4}\b は「単語としてちょうど4桁の数字」を意味し、- は単語の境界なので、日付の中の年も当てはまります。意図するものを言い表すようにパターンを絞ります。たとえば (?<=Order |order )\d+ を使うか、次に説明するグループを使います。
パターンには逐語的文字列を使う
正規表現の構文はいたるところでバックスラッシュを使い(\d、\s、\b)、C#の文字列のエスケープも同じです。通常の文字列では、"\d" はコンパイルすらできず(CS1009、認識できないエスケープシーケンス)、"\b" はバックスペース文字にコンパイルされ、パターンが何も言わずに変わってしまいます。パターンは、バックスラッシュが単なるバックスラッシュである @ 付きの逐語的文字列で書きます。
var a = new Regex("\\d+\\.\\d{2}"); // escaped twice: hard to read
var b = new Regex(@"\d+\.\d{2}"); // verbatim: what the regex engine sees
逐語的文字列の中では、二重引用符は "" と書きます。C# 11の生文字列リテラル("""...""")なら、それさえ不要です。
グループ:一致の一部を取り出す
かっこはグループを作り、一致の後に各グループのテキストを取得できます。グループは開きかっこの順に1から番号が振られ、グループ0は一致全体です。名前付きグループ (?<name>...) は読みやすく、パターンを編集しても壊れにくくなります。
出力:
2026-03-14
ERROR
payments
Card declined for order 1042
60 by 90, area 5400
[ と ] は正規表現で意味を持つ(文字クラス)ので、そのままの角かっこに一致させるには \[ と \] が必要です。. * + ? ( ) { } ^ $ | \ も同じです。ユーザーが与えた文字列をパターンの中でそのまま一致させるには、メタ文字をエスケープしてくれる Regex.Escape に通します。Regex.Escape("price (USD)") は price\ \(USD\) を返します(空白もエスケープされますが、害はありません)。
キャプチャせずにまとめるだけのかっこは (?:...) と書きます。グループの番号付けをきれいに保ち、わずかに速くなります。
Replace:置換パターンとラムダ
Regex.Replace はすべての一致を置換します。置換文字列の中では、$1 がグループ1を、${name} が名前付きグループを、$0 が一致全体を挿入します。テンプレートでは表せないものには、各 Match を受け取ってその置換を返す関数(通常はラムダ)を渡します。
出力:
Invoiced 14/03/2026, paid 02/04/2026.
too many spaces
Card **** **** **** 1234
Tea 2.75, Scone 3.52, Jam 1.10
カードのマスクは先読み (?=...) を使っています。後ろにさらに4桁が続く場合にだけ数字に一致し、それらを消費しません。前後の確認((?=...)、(?!...)、(?<=...)、(?<!...))は、文脈を一致に含めずに判定します。これによって、1回の Replace で最後の4桁をそのまま残せます。
パターンのない普通のテキストなら、string.Replace のほうが簡単で高速です。置換する対象が変化する場合に Regex.Replace を使います。
Split
Regex.Split はパターンの一致ごとに分割するので、変化する区切り文字を扱えます。
出力:
csharp / dotnet / regex / tutorial / beginner
Order Shipped Event Handler
パターンにキャプチャするグループが含まれていると、Regex.Split はキャプチャした区切り文字も結果に含めます。それが不要なら (?:...) を使います。
RegexOptions
オプションはパターン全体の振る舞いを変えます。| で組み合わせます。
出力:
1
2
error INFO Error
True
使うことになるもの:
IgnoreCase:大文字と小文字を区別しない照合(インラインの形は(?i))。Multiline:^と$が、文字列全体だけでなく、各行の先頭と末尾に一致します。Singleline:.が\nにも一致します(既定では改行以外の任意の文字に一致します)。IgnorePatternWhitespace:パターンの中の空白が無視され、#がコメントを始めます。複数行にわたって書く長いパターン向けです。CultureInvariant:IgnoreCaseとともに、現在のカルチャの規則を使わずに比較します。Compiled:パターンを一度ILにコンパイルします。何度も使う正規表現向けです。
入力の検証:アンカーとメールアドレスの問題
検証では、パターンを ^ と $ で固定します。そうしないと、入力の一部が一致した時点で通ってしまいます。
出力:
PT-1000 loose=True strict=True
pt-1000 loose=False strict=False
XPT-1000Y loose=True strict=False
PT-10 loose=False strict=False
ana@example.com True
ana@example False
ana @example.com False
ana@@example.com False
メールアドレスのパターンが確認するのは形だけです。空白や @ を含まない何か、1つの @、ドットを含むドメインです。これは意図的です。アドレスの本当の文法(RFC 5322)は、引用符付きのローカル部、コメント、IPリテラルのドメインを許していて、それを網羅しようとするパターンは何百文字にもなり、それでも人が実際に使う有効なアドレスを拒否します。そして構文的に完璧なアドレスでも届かないことがあります。形を確認し、確認メールを送ります。パターンを書きたくなければ、System.Net.Mail.MailAddress も形の確認に使えます。
性能:静的メソッド、インスタンス、コンパイル
静的メソッド(Regex.IsMatch(input, pattern))はパターンを解析して小さなキャッシュ(既定で15パターン)に保持するので、同じパターンでの繰り返しの呼び出しは低コストです。性能が重要なループで使うパターンは、Regex のインスタンスを1つ作って static readonly のフィールドに保持します。何千回も実行するなら、起動が遅くなる代わりに照合が速くなる RegexOptions.Compiled を加えます。
タイムアウトと壊滅的なバックトラッキング
一部のパターンは、特定の入力に対して指数関数的な時間がかかります。典型的なのは、"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!" に対する ^(a+)+$ のような入れ子の量指定子です。エンジンは諦める前に、2つの + の間で a を分けるあらゆる方法を試します。Webサーバーでは、ユーザーからのそうした入力1つでCPUのコアを1つ占有されてしまいます(「ReDoS」攻撃)。信頼できない入力を扱う正規表現には、すべてタイムアウトを与えます。
var pattern = new Regex(@"^(\w+\s?)*$", RegexOptions.None, TimeSpan.FromMilliseconds(100));
try
{
bool ok = pattern.IsMatch(userInput);
}
catch (RegexMatchTimeoutException)
{
// treat as invalid input
}
すべての静的メソッドにも、タイムアウトを受け取るオーバーロードがあります。.NET 7では、一部の機能(前後の確認、後方参照)と引き換えに線形時間を保証するエンジンの RegexOptions.NonBacktracking も追加されました。
GeneratedRegex(.NET 7)
.NET 7では、パターンをコンパイル時に普通のC#のコードに変換するソースジェネレーターが追加されました。起動時のコストなしに Compiled の速度が得られ、生成されたコードはIDEで読め、トリミングやNative AOTでも動きます。
public static partial class Patterns
{
[GeneratedRegex(@"^[A-Z]{2}-\d{4}$", RegexOptions.IgnoreCase)]
public static partial Regex ProductCode();
}
bool ok = Patterns.ProductCode().IsMatch("pt-1000"); // True
.NET 7以降では、コンパイル時にわかっているパターンにはこの形が推奨されます。
よくある間違い
- 検証でアンカーがない。
^...$がないと、IsMatchは一致を含んでいるだけの入力を受け入れます。 - パターンに通常の文字列を使う。
"\b"は単語の境界ではなくバックスペースです。@"..."を使います。 - エスケープされていない特殊文字。
.は任意の文字に一致するので、パターンの3.50は3x50にも一致します。\.でエスケープするか、ユーザーの入力にはRegex.Escapeを使います。 - 貪欲な量指定子。
<b>bold</b>に対する<.*>は文字列全体に一致します。怠惰な.*?か否定の文字クラス[^>]*を使います。 - 信頼できない入力にタイムアウトがない。 入れ子の量指定子がリクエストを止めてしまうことがあります。
- 構造化された形式に正規表現を使う。 HTML、JSON、引用符を含むCSVには、パターンではなくパーサーが必要です。
よくある質問
C#で正規表現はどう使いますか?
using System.Text.RegularExpressions; を追加し、Regex の静的メソッドを呼びます。Regex.IsMatch(input, pattern) は bool を返し、Regex.Match は最初の一致を、Regex.Matches はすべての一致を返し、Regex.Replace は置換し、Regex.Split は分割します。バックスラッシュがそのまま正規表現エンジンに届くように、パターンは逐語的文字列 @"\d+" で書きます。
C#で正規表現の一致からグループの値を取得するには?
取り出したい部分をかっこで囲み、match.Groups[1].Value を読みます(グループは1から番号が振られ、グループ0は一致全体です)。名前付きグループのほうが明確です。(?<year>\d{4}) は match.Groups["year"].Value で読みます。失敗した一致のグループはnullではなく空なので、先に match.Success を確認します。
C#で正規表現を使ってテキストを置換するには?
Regex.Replace(input, pattern, replacement) はすべての一致を置換します。置換文字列ではグループを参照できます。番号付きのグループは $1、名前付きのものは ${name}、一致全体は $0 です。テンプレートでは表せない処理には、ラムダを渡します:Regex.Replace(text, @"\d+", m => (int.Parse(m.Value) * 2).ToString())。
C#の正規表現で大文字と小文字を区別しないようにするには?
RegexOptions.IgnoreCase を渡します:Regex.IsMatch(input, "error", RegexOptions.IgnoreCase)。オプションは | で組み合わせられ、たとえば RegexOptions.IgnoreCase | RegexOptions.Multiline とします。パターンの中で (?i) として有効にすることもできます。
C#で正規表現を使ってメールアドレスを検証するには?
^[^@\s]+@[^@\s]+\.[^@\s]+$ のようなパターンは明らかな入力ミス(@ の欠落、空白、ドメインのドットがない)を検出し、フォームにはたいていそれで十分です。正規表現でアドレスを本当に検証することはできません。完全な文法は実用的なパターンでは扱えない形を許し、構文的に正しいアドレスが存在しないこともあります。基本的な形を確認し、確認メールを送って確かめます。