クラス

【C#】正規表現を使った数字の判定・抽出・桁数制限の方法と実装例

C#で数字を扱う正規表現は^\d+$で文字列全体が数字か判定し、\d+で部分的な数字列を抽出するのが基本です。

桁数制限には\d{3,6}など量指定子を加えます。

判定はRegex.IsMatch、抽出はRegex.Matchesを呼ぶだけなので記述量が少なく、入力チェックやID解析を手早く実装できます。

@付き文字列を使えばバックスラッシュの二重記述を避けられます。

目次から探す
  1. 数字判定用正規表現の基礎
  2. 数値抽出パターンの設計
  3. パターン実装テクニック
  4. Regex.IsMatch を使った入力検証
  5. Regex.Matches でのデータ抽出
  6. Regex.Replace による数字の加工
  7. パフォーマンス最適化
  8. Unicode と国際化対応
  9. 先読み・後読みの応用
  10. 単体テストとデバッグ
  11. よくある落とし穴
  12. 正規表現と数値パースの使い分け
  13. ケーススタディ: 業務シナリオ別パターン例
  14. 実務での運用ポイント
  15. 参考実装の拡張アイデア
  16. まとめ

数字判定用正規表現の基礎

正規表現を使って数字を判定する際の基本的な要素について解説します。

数字の判定や抽出を行うためには、まず文字クラスやアンカー、量指定子の意味を理解することが重要です。

ここではC#でよく使われる正規表現の基本構成を丁寧に説明いたします。

文字クラス \d と [0-9]

数字を表す正規表現の文字クラスには主に2種類あります。

1つは \d、もう1つは [0-9] です。

どちらも数字にマッチしますが、微妙に挙動が異なるため使い分けが必要です。

各文字クラスがマッチする範囲

  • \d は「数字」を意味する特殊文字クラスです。C#の正規表現では、\dは半角の0から9までの数字だけでなく、Unicodeの数字カテゴリに含まれる全角数字やその他の数字文字にもマッチします。つまり、123だけでなく、全角の「123」やアラビア数字なども含まれます
  • [0-9] は文字クラスの一種で、0から9までの半角数字のいずれか1文字にマッチします。こちらは純粋にASCIIの数字だけを対象とするため、全角数字や他の数字文字にはマッチしません

この違いは、国際化対応や入力の厳密さを求める場合に重要です。

例えば、ユーザー入力が半角数字のみであることを保証したい場合は [0-9] を使うほうが安全です。

以下にサンプルコードを示します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string halfWidth = "12345";
        string fullWidth = "12345"; // 全角数字
        // \d は全角数字にもマッチする
        Console.WriteLine(Regex.IsMatch(halfWidth, @"^\d+$")); // True
        Console.WriteLine(Regex.IsMatch(fullWidth, @"^\d+$")); // True
        // [0-9] は半角数字のみマッチ
        Console.WriteLine(Regex.IsMatch(halfWidth, @"^[0-9]+$")); // True
        Console.WriteLine(Regex.IsMatch(fullWidth, @"^[0-9]+$")); // False
    }
}
True
True
True
False

このように、\dはより広範囲の数字にマッチし、[0-9]は半角数字に限定されることがわかります。

Unicode 数字カテゴリー \p{Nd} を利用する

C#の正規表現では、Unicodeの文字カテゴリを指定することも可能です。

\p{Nd}は「Decimal Digit Number(10進数字)」のUnicodeカテゴリを表し、\dとほぼ同じ範囲の数字にマッチします。

\p{Nd}を使うことで、より明示的にUnicodeの数字を対象にできるため、国際化対応の正規表現を書く際に役立ちます。

以下は\p{Nd}を使った例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "12345"; // 半角と全角混在
        // \p{Nd} はUnicodeの数字にマッチ
        bool isMatch = Regex.IsMatch(input, @"^\p{Nd}+$");
        Console.WriteLine(isMatch); // True
        // [0-9] は半角数字のみ
        bool isHalfMatch = Regex.IsMatch(input, @"^[0-9]+$");
        Console.WriteLine(isHalfMatch); // False
    }
}
True
False

このように\p{Nd}はUnicodeの数字全般にマッチし、\dと同様の挙動を示します。

用途に応じて使い分けてください。

アンカー ^ と $ で完全一致

正規表現で数字の判定を行う際、文字列全体が数字だけで構成されているかを確認したい場合があります。

そのときに使うのがアンカーです。

  • ^ は文字列の先頭を示します
  • $ は文字列の末尾を示します

これらを組み合わせることで、文字列全体が指定したパターンにマッチしているかを判定できます。

例えば、^\d+$ は「文字列の先頭から末尾まで、1文字以上の数字のみで構成されている」ことを意味します。

アンカーを使わずに \d+ だけを使うと、文字列の一部に数字が含まれていればマッチしてしまうため、完全一致の判定には不向きです。

以下に例を示します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input1 = "12345";
        string input2 = "abc123";
        string input3 = "123abc";
        // 完全一致判定
        Console.WriteLine(Regex.IsMatch(input1, @"^\d+$")); // True
        Console.WriteLine(Regex.IsMatch(input2, @"^\d+$")); // False
        Console.WriteLine(Regex.IsMatch(input3, @"^\d+$")); // False
        // 部分一致判定(アンカーなし)
        Console.WriteLine(Regex.IsMatch(input2, @"\d+")); // True
        Console.WriteLine(Regex.IsMatch(input3, @"\d+")); // True
    }
}
True
False
False
True
True

このように、アンカーを使うことで文字列全体の判定が可能になります。

量指定子 + * ? と繰り返し {n,m}

正規表現の量指定子は、直前の文字やグループが何回繰り返されるかを指定するために使います。

数字の判定や抽出では、数字の桁数を制御する際に重要です。

主な量指定子は以下の通りです。

量指定子意味
+1回以上の繰り返し\d+ → 1桁以上の数字
*0回以上の繰り返し\d* → 0桁以上の数字
?0回か1回の繰り返し\d? → 0桁か1桁の数字
{n}ちょうどn回の繰り返し\d{3} → 3桁の数字
{n,}n回以上の繰り返し\d{2,} → 2桁以上の数字
{n,m}n回以上m回以下の繰り返し\d{3,6} → 3桁から6桁の数字

例えば、3桁から6桁の数字だけを許可したい場合は、^\d{3,6}$というパターンを使います。

以下に量指定子の使い方を示すサンプルコードを記載します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string[] inputs = { "12", "123", "123456", "1234567" };
        foreach (var input in inputs)
        {
            bool isMatch = Regex.IsMatch(input, @"^\d{3,6}$");
            Console.WriteLine($"{input} は3桁以上6桁以下の数字か: {isMatch}");
        }
    }
}
12 は3桁以上6桁以下の数字か: False
123 は3桁以上6桁以下の数字か: True
123456 は3桁以上6桁以下の数字か: True
1234567 は3桁以上6桁以下の数字か: False

このように量指定子を使うことで、数字の桁数を柔軟に制御できます。

+*は範囲指定が不要な場合に便利で、{n,m}は厳密な桁数制限が必要な場合に使います。

これらの基本を押さえることで、C#の正規表現で数字の判定や抽出を正確に行えるようになります。

次のステップでは、これらの要素を組み合わせて実際の数字抽出や桁数制限の具体的なパターン設計について解説いたします。

数値抽出パターンの設計

連続数字の抽出例 \d+

文字列の中から連続した数字を抽出するには、\d+というパターンを使います。

\dは数字1文字にマッチし、+は1回以上の繰り返しを意味します。

これにより、連続した数字の塊をすべて取得できます。

以下のサンプルコードでは、文字列中のすべての数字の連続部分を抽出しています。

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

このように、\d+は数字の連続した部分を簡単に抽出できます。

桁数を限定するパターン

固定桁数 \d{5}

数字の桁数を固定したい場合は、量指定子 {n} を使います。

例えば、5桁の数字だけを抽出したい場合は \d{5} と記述します。

以下の例では、文字列中から5桁の数字だけを抽出しています。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "郵便番号は12345、電話番号は1234567890です。";
        MatchCollection matches = Regex.Matches(input, @"\d{5}");
        Console.WriteLine("5桁の数字:");
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
5桁の数字:
12345

このように、固定桁数の数字だけを抽出できます。

範囲指定 \d{3,6}

桁数の範囲を指定したい場合は、量指定子 {n,m} を使います。

例えば、3桁から6桁の数字を抽出したい場合は \d{3,6} と記述します。

以下の例では、3桁以上6桁以下の数字を抽出しています。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "コードは12、123、1234、123456、1234567です。";
        MatchCollection matches = Regex.Matches(input, @"\d{3,6}");
        Console.WriteLine("3桁から6桁の数字:");
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
3桁から6桁の数字:
123
1234
123456
123456

7桁の数字は抽出されていないことがわかります。

区切り付き表記の許可

小数点を含む数字

小数点を含む数字を抽出する場合は、数字の連続に加えて小数点を許可する必要があります。

小数点は正規表現で.と書くと任意の1文字を意味するため、エスケープして \. と記述します。

小数点以下の数字は0回以上の繰り返しで表現し、全体としては以下のようなパターンになります。

\d+(\.\d+)? これは「1つ以上の数字の後に、小数点と1つ以上の数字が0回か1回続く」という意味です。

サンプルコードは以下の通りです。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "価格は123.45円、割引は0.5、税率は10%です。";
        MatchCollection matches = Regex.Matches(input, @"\d+(\.\d+)?");
        Console.WriteLine("小数点を含む数字:");
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
小数点を含む数字:
123.45
0.5
10

整数の「10」も抽出されていることに注意してください。

マイナス記号やプラス記号の扱い

数値の前にマイナス-やプラス+記号が付く場合は、これらの記号をオプションとして許可します。

符号は数字の前に1回だけ現れることが多いため、[-+]? と記述します。

小数点を含む符号付き数値のパターンは以下のようになります。

[-+]?\d+(\.\d+)? サンプルコードを示します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "温度は-5.5度、目標は+10度、現在は0度です。";
        MatchCollection matches = Regex.Matches(input, @"[-+]?\d+(\.\d+)?");
        Console.WriteLine("符号付きの数値:");
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
符号付きの数値:
-5.5
+10
0

符号がない数値も抽出されていることがわかります。

科学技術表記 1.23e+10

科学技術表記(指数表記)を含む数値を抽出する場合は、指数部を正規表現に追加します。

指数部は e または E に続き、符号付きの整数が続く形式です。

パターンは以下のように書けます。

[-+]?\d+(\.\d+)?([eE][-+]?\d+)?

  • [-+]?:符号(オプション)
  • \d+(\.\d+)?:整数または小数
  • ([eE][-+]?\d+)?:指数部(オプション)

以下のサンプルコードで確認します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "放射能は1.23e+10ベクレル、半減期は4.56E-3秒です。";
        MatchCollection matches = Regex.Matches(input, @"[-+]?\d+(\.\d+)?([eE][-+]?\d+)?");
        Console.WriteLine("科学技術表記の数値:");
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
科学技術表記の数値:
1.23e+10
4.56E-3

指数表記の数値も正しく抽出できています。

指定文字を挟む数字の抽出

ハイフン区切りの電話番号

電話番号など、数字の間にハイフン-が入る形式を抽出する場合は、数字とハイフンの組み合わせをパターンに含めます。

例えば、3桁-4桁-4桁の電話番号を抽出する場合は以下のように書きます。

\d{3}-\d{4}-\d{4} 以下のサンプルコードです。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "連絡先は090-1234-5678、緊急は080-9876-5432です。";
        MatchCollection matches = Regex.Matches(input, @"\d{3}-\d{4}-\d{4}");
        Console.WriteLine("電話番号:");
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
電話番号:
090-1234-5678
080-9876-5432

ハイフンを含む数字のパターンを正確に指定できます。

コロン区切りの時刻

時刻表記のように数字の間にコロン:が入る場合も同様です。

例えば、HH:mm形式の時刻を抽出するには以下のパターンを使います。

\d{2}:\d{2} サンプルコードは以下の通りです。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "開始時刻は09:30、終了は18:45です。";
        MatchCollection matches = Regex.Matches(input, @"\d{2}:\d{2}");
        Console.WriteLine("時刻:");
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
時刻:
09:30
18:45

このように、数字の間に特定の区切り文字が入るパターンも正規表現で簡単に抽出できます。

パターン実装テクニック

エスケープミスを防ぐ文字列リテラル

正規表現パターンをC#の文字列として記述する際、バックスラッシュ\の扱いに注意が必要です。

バックスラッシュは正規表現の特殊文字を表すために多用されますが、C#の通常文字列リテラル内でもエスケープ文字として使われるため、二重にエスケープしなければなりません。

これが原因でミスが起こりやすいので、適切な文字列リテラルの使い分けが重要です。

通常リテラルとバックスラッシュ

通常の文字列リテラルはダブルクォーテーションで囲みますが、バックスラッシュはエスケープ文字として機能します。

例えば、正規表現で数字を表す\dを文字列に書く場合は、"\\d"と二重にエスケープしなければなりません。

以下の例をご覧ください。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string pattern = "\\d+"; // \d+ を表すために \\d+ と記述
        string input = "123abc456";
        MatchCollection matches = Regex.Matches(input, pattern);
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
123
456

このように、通常リテラルではバックスラッシュを2回書く必要があり、パターンが複雑になるとエスケープミスが起こりやすくなります。

Verbatim リテラル @””

C#では、文字列の前に@を付けることで、バックスラッシュのエスケープを無効化した「Verbatimリテラル」を使えます。

これにより、正規表現パターンをより直感的に記述できます。

例えば、\d+@"\d+"と書けます。

バックスラッシュを2回書く必要がなくなるため、可読性が向上します。

以下の例を見てください。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string pattern = @"\d+";
        string input = "abc123def456";
        MatchCollection matches = Regex.Matches(input, pattern);
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
123
456

Verbatimリテラルは正規表現パターンを記述する際に非常に便利で、特に複雑なパターンでバックスラッシュが多用される場合に推奨されます。

Raw 文字列リテラル “”” (C# 11)

C# 11からはRaw文字列リテラルが導入され、複数行にわたる文字列やエスケープ不要の文字列を簡単に書けるようになりました。

Raw文字列リテラルは3つ以上のダブルクォーテーションで囲み、文字列内のバックスラッシュや改行をそのまま記述できます。

正規表現パターンをRaw文字列リテラルで書くと、さらにエスケープミスを減らせます。

以下はRaw文字列リテラルを使った例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string pattern = """
            \d+  # 1回以上の数字
            """;
        string input = "abc123def456";
        MatchCollection matches = Regex.Matches(input, pattern, RegexOptions.IgnorePatternWhitespace);
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
123
456

Raw文字列リテラルは複雑な正規表現や複数行のパターンを記述する際に特に有効です。

コメントや空白も含めて書けるため、パターンの可読性が大幅に向上します。

コメント付き正規表現 RegexOptions.IgnorePatternWhitespace

正規表現は複雑になると読みにくくなりますが、RegexOptions.IgnorePatternWhitespaceオプションを使うと、パターン内の空白や改行を無視し、#以降の文字列をコメントとして扱えます。

これにより、パターンを複数行に分けて書き、コメントを入れて可読性を高められます。

以下の例は、数字の連続を抽出するパターンにコメントを付けたものです。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string pattern = @"
            \d+     # 1回以上の数字にマッチ
        ";
        string input = "abc123def456";
        MatchCollection matches = Regex.Matches(input, pattern, RegexOptions.IgnorePatternWhitespace);
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
123
456

コメント付き正規表現は、複雑なパターンを他の開発者と共有したり、後から修正する際に役立ちます。

条件分岐とパイプ |

正規表現のパイプ記号 | は「または」を意味し、複数のパターンのいずれかにマッチする場合に使います。

条件分岐のように使うことで、異なる形式の数字や文字列を一つのパターンで扱えます。

例えば、3桁または5桁の数字にマッチさせたい場合は、\d{3}|\d{5}と書きます。

以下の例で動作を確認します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string pattern = @"\d{5}|\d{3}"; // 5桁の数字からチェックするようにする
        string input = "123 12345 12 1234";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("3桁または5桁の数字:");
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
3桁または5桁の数字:
123
12345
123

パイプは複数のパターンを組み合わせる際に非常に便利ですが、優先順位に注意が必要です。

例えば、\d{3}|\d{5}では3桁の数字が先にマッチするため、5桁の数字の一部が3桁としてマッチすることがあります。

これを防ぐには、長いパターンを先に書くなど工夫が必要です。

string pattern = @"\d{5}|\d{3}"; // 5桁を先にマッチさせる

このように、パイプを使った条件分岐は正規表現の柔軟性を高める重要なテクニックです。

Regex.IsMatch を使った入力検証

サンプルコードの構成

Regex.IsMatchメソッドは、指定した文字列が正規表現パターンにマッチするかどうかを真偽値で返します。

ユーザー入力の検証やフォームのバリデーションなどでよく使われます。

基本的な構成は以下の通りです。

  1. 検証したい文字列を用意する
  2. 判定用の正規表現パターンを用意する
  3. Regex.IsMatch に文字列とパターンを渡してマッチ判定を行う
  4. 結果に応じて処理を分岐する

以下のサンプルコードは、入力文字列が数字のみで構成されているかを判定する例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "12345";
        string pattern = @"^\d+$"; // 文字列全体が数字のみ
        bool isValid = Regex.IsMatch(input, pattern);
        Console.WriteLine($"入力「{input}」は数字のみか: {isValid}");
    }
}
入力「12345」は数字のみか: True

このように、Regex.IsMatch はシンプルに入力の妥当性をチェックできます。

マッチ結果による処理分岐

Regex.IsMatch の戻り値は bool型なので、条件分岐にそのまま使えます。

例えば、数字のみの入力かどうかで処理を分ける場合は以下のように書けます。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "abc123";
        if (Regex.IsMatch(input, @"^\d+$"))
        {
            Console.WriteLine("数字のみの入力です。");
        }
        else
        {
            Console.WriteLine("数字以外の文字が含まれています。");
        }
    }
}
数字以外の文字が含まれています。

このように、入力の妥当性に応じて処理を分けることで、ユーザーに適切なフィードバックを返したり、次の処理へ進めるか判断できます。

文化依存の挙動 RegexOptions.CultureInvariant

正規表現のマッチングは、文化(カルチャ)に依存する場合があります。

特に数字の判定で使う \d は、環境によって半角数字だけでなく全角数字や他のUnicode数字にもマッチすることがあります。

C#の Regexクラスでは、RegexOptions.CultureInvariant オプションを指定すると、文化依存の挙動を無効化し、より一貫したマッチングが可能になります。

以下の例で比較します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "12345"; // 全角数字
        // 文化依存あり(デフォルト)
        bool matchDefault = Regex.IsMatch(input, @"^\d+$");
        Console.WriteLine($"デフォルトでのマッチ: {matchDefault}");
        // 文化依存なし
        bool matchInvariant = Regex.IsMatch(input, @"^\d+$", RegexOptions.CultureInvariant);
        Console.WriteLine($"CultureInvariant指定時のマッチ: {matchInvariant}");
    }
}
デフォルトでのマッチ: True
CultureInvariant指定時のマッチ: False

このように、RegexOptions.CultureInvariant を指定すると、\d は半角数字のみにマッチし、全角数字はマッチしなくなります。

全角数字を許可するかどうか

全角数字を許可したい場合は、RegexOptions.CultureInvariant を指定せずにデフォルトの挙動を使うか、明示的に全角数字を含む文字クラスを作成します。

例えば、全角数字のUnicode範囲は \uFF10 から \uFF19 です。

これを含めたパターンは以下のように書けます。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "12345"; // 全角数字
        // 半角数字と全角数字の両方を許可
        string pattern = @"^[0-9\uFF10-\uFF19]+$";
        bool isMatch = Regex.IsMatch(input, pattern);
        Console.WriteLine($"半角・全角数字のみか: {isMatch}");
    }
}
半角・全角数字のみか: True

逆に、全角数字を許可したくない場合は、RegexOptions.CultureInvariant を使うか、[0-9] のみを使うことで半角数字だけに限定できます。

このように、文化依存の挙動を理解し、必要に応じてオプションやパターンを調整することが重要です。

Regex.Matches でのデータ抽出

MatchCollection と Match オブジェクト

Regex.Matchesメソッドは、指定した文字列の中から正規表現パターンにマッチするすべての部分を抽出し、MatchCollectionオブジェクトとして返します。

MatchCollection は複数の Matchオブジェクトを保持しており、それぞれが1つのマッチ結果を表します。

Matchオブジェクトは、マッチした文字列の値や位置情報、キャプチャグループの内容などを取得できます。

以下のサンプルコードでは、文字列中のすべての数字の連続部分を抽出し、MatchCollectionMatch の使い方を示しています。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "注文番号123と456、合計7890円です。";
        string pattern = @"\d+";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("抽出された数字:");
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
抽出された数字:
123
456
7890

このように、MatchCollection をループで回して各 MatchValueプロパティからマッチした文字列を取得します。

位置情報 Index と Length の活用

Matchオブジェクトには、マッチした部分の開始位置を示す Indexプロパティと、マッチした文字列の長さを示す Lengthプロパティがあります。

これらを使うことで、元の文字列のどの位置にマッチがあったかを把握できます。

例えば、マッチした数字の位置を表示するコードは以下の通りです。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "注文番号123と456、合計7890円です。";
        string pattern = @"\d+";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("数字の位置情報:");
        foreach (Match match in matches)
        {
            Console.WriteLine($"値: {match.Value}, 開始位置: {match.Index}, 長さ: {match.Length}");
        }
    }
}
数字の位置情報:
値: 123, 開始位置: 4, 長さ: 3
値: 456, 開始位置: 8, 長さ: 3
値: 7890, 開始位置: 14, 長さ: 4

この情報は、文字列の一部を置換したり、ハイライト表示したりする際に役立ちます。

キャプチャグループのネーミング

正規表現では、丸括弧 () を使って部分文字列をキャプチャできます。

さらに、名前付きキャプチャグループを使うと、グループに名前を付けてアクセスしやすくなります。

名前付きキャプチャは (?<名前>パターン) の形式で記述します。

数字と単位を同時に取得

例えば、数値とその単位(例:100kg50m)を同時に抽出したい場合、数字部分と単位部分を別々の名前付きグループでキャプチャできます。

以下の例では、数字を number、単位を unit という名前でキャプチャしています。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "荷物の重さは100kgで、距離は50mです。";
        string pattern = @"(?<number>\d+)(?<unit>kg|m)";
        MatchCollection matches = Regex.Matches(input, pattern);
        foreach (Match match in matches)
        {
            string number = match.Groups["number"].Value;
            string unit = match.Groups["unit"].Value;
            Console.WriteLine($"数値: {number}, 単位: {unit}");
        }
    }
}
数値: 100, 単位: kg
数値: 50, 単位: m

名前付きキャプチャグループを使うことで、複数の情報を一度に抽出し、コードの可読性と保守性を高められます。

Groupsプロパティから名前でアクセスできるため、インデックス番号を覚える必要がありません。

Regex.Replace による数字の加工

プレースホルダー置換

プレースホルダー置換 $0 $1

Regex.Replaceメソッドは、正規表現にマッチした部分を指定した文字列で置換できます。

置換文字列内で $0$1 といったプレースホルダーを使うと、マッチした文字列やキャプチャグループの内容を参照できます。

  • $0 はマッチ全体を表します
  • $1 は最初のキャプチャグループの内容を表します
  • $2 以降は2番目、3番目のキャプチャグループを指します

以下の例では、数字の後に「円」という単位が付いている文字列から、数字部分だけを「$1」に置換して加工しています。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "価格は1000円、送料は500円です。";
        string pattern = @"(\d+)円";
        // 数字部分だけを「[数字]」の形式に置換
        string replaced = Regex.Replace(input, pattern, "[$1]");
        Console.WriteLine(replaced);
    }
}
価格は[1000]、送料は[500]です。

このように、キャプチャグループを使って置換文字列内でマッチした部分を自由に加工できます。

MatchEvaluator デリゲートによる動的変換

Regex.Replace は、置換文字列の代わりに MatchEvaluator デリゲートを渡すこともできます。

これにより、マッチした内容に応じて動的に置換文字列を生成できます。

例えば、数字を3桁ごとにカンマ区切りの形式に変換する例を示します。

using System;
using System.Text.RegularExpressions;
using System.Globalization;
class Program
{
    static void Main()
    {
        string input = "売上は1000000円、利益は50000円です。";
        string pattern = @"\d+";
        string replaced = Regex.Replace(input, pattern, new MatchEvaluator(FormatNumberWithComma));
        Console.WriteLine(replaced);
    }
    static string FormatNumberWithComma(Match m)
    {
        // マッチした数字を整数に変換し、カンマ区切りの文字列に変換
        if (int.TryParse(m.Value, out int number))
        {
            return number.ToString("N0", CultureInfo.InvariantCulture);
        }
        return m.Value;
    }
}
売上は1,000,000円、利益は50,000円です。

この方法は、単純な置換では対応できない複雑な変換やフォーマットに便利です。

マスキングやフォーマット統一の例

Regex.ReplaceMatchEvaluator を組み合わせることで、個人情報のマスキングや数値のフォーマット統一も簡単に実装できます。

以下は、電話番号の一部をマスキングする例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "連絡先は090-1234-5678です。";
        string pattern = @"(\d{3})-(\d{4})-(\d{4})";
        string masked = Regex.Replace(input, pattern, new MatchEvaluator(MaskPhoneNumber));
        Console.WriteLine(masked);
    }
    static string MaskPhoneNumber(Match m)
    {
        // 2番目の4桁を「****」に置換
        return $"{m.Groups[1].Value}-****-{m.Groups[3].Value}";
    }
}
連絡先は090-****-5678です。

また、数値のフォーマットを統一する例として、小数点以下の桁数を揃える処理も可能です。

using System;
using System.Text.RegularExpressions;
using System.Globalization;
class Program
{
    static void Main()
    {
        string input = "値は3.14159、誤差は2.7です。";
        string pattern = @"\d+(\.\d+)?";
        string formatted = Regex.Replace(input, pattern, m =>
        {
            if (double.TryParse(m.Value, out double num))
            {
                // 小数点以下2桁で丸めて文字列化
                return num.ToString("F2", CultureInfo.InvariantCulture);
            }
            return m.Value;
        });
        Console.WriteLine(formatted);
    }
}
値は3.14、誤差は2.70です。

このように、Regex.ReplaceMatchEvaluator を活用することで、数字のマスキングやフォーマット統一など多様な加工が柔軟に行えます。

パフォーマンス最適化

コンパイル済み正規表現 RegexOptions.Compiled

正規表現はパターンの解析や実行にコストがかかるため、頻繁に同じパターンを使う場合はパフォーマンスの最適化が重要です。

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

ただし、コンパイルには初回の生成時に時間がかかるため、使いどころを考慮する必要があります。

頻繁に使うパターンを静的に保持し、繰り返し利用するケースで効果的です。

以下は RegexOptions.Compiled を使った例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "123abc456";
        Regex regex = new Regex(@"\d+", RegexOptions.Compiled);
        MatchCollection matches = regex.Matches(input);
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
123
456

このように、Regexオブジェクトを生成時にコンパイルオプションを付けることで、マッチング処理が高速化されます。

静的初期化でキャッシュを活用

正規表現オブジェクトを毎回生成するとパフォーマンスが低下するため、静的フィールドやプロパティで一度だけ生成し、使い回す方法が推奨されます。

これにより、パターンの解析やコンパイルコストを一度だけ負担し、以降はキャッシュされた正規表現を利用できます。

以下は静的初期化でキャッシュを活用する例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    // 静的にRegexオブジェクトを生成しキャッシュ
    private static readonly Regex NumberRegex = new Regex(@"^\d+$", RegexOptions.Compiled);
    static void Main()
    {
        string[] inputs = { "12345", "abc123", "67890" };
        foreach (var input in inputs)
        {
            bool isMatch = NumberRegex.IsMatch(input);
            Console.WriteLine($"{input} は数字のみか: {isMatch}");
        }
    }
}
12345 は数字のみか: True
abc123 は数字のみか: False
67890 は数字のみか: True

この方法は、同じパターンを複数回使う場合に特に効果的です。

ソースジェネレータ [GeneratedRegex]

.NET 7以降では、[GeneratedRegex] 属性を使ったソースジェネレータ機能が導入され、正規表現のパターンをコンパイル済みコードとして自動生成できます。

これにより、起動時のコンパイルコストを削減しつつ、高速なマッチングが可能です。

使い方は、部分クラスにメソッドを定義し、[GeneratedRegex] 属性でパターンを指定します。

コンパイラが自動的に正規表現コードを生成します。

以下はサンプルコードです。

using System;
using System.Text.RegularExpressions;
partial class Program
{
    [GeneratedRegex(@"^\d+$", RegexOptions.Compiled)]
    private static partial Regex NumberRegex();
    static void Main()
    {
        string input = "12345";
        bool isMatch = NumberRegex().IsMatch(input);
        Console.WriteLine($"{input} は数字のみか: {isMatch}");
    }
}
12345 は数字のみか: True

この機能を使うには、プロジェクトのターゲットフレームワークを.NET 7以上に設定し、System.Text.RegularExpressions 名前空間をインポートしてください。

タイムアウト設定で ReDoS を回避

正規表現は複雑なパターンや悪意のある入力により、処理時間が極端に長くなる「正規表現拒否サービス攻撃(ReDoS)」のリスクがあります。

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

タイムアウトを設定すると、指定時間を超えたマッチング処理は例外をスローして中断されます。

これにより、無限ループや過剰なバックトラッキングを防止できます。

以下はタイムアウトを設定した例です。

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

このように、タイムアウト設定は安全な正規表現運用に欠かせない対策です。

特に外部からの入力を扱う場合は必ず設定することをおすすめします。

Unicode と国際化対応

全角・半角数字の検証

日本語環境などでは、数字が半角(ASCIIの0~9)だけでなく全角(全角数字:Unicode範囲 U+FF10~U+FF19)で入力されることがあります。

正規表現で数字を検証する際に、全角数字を許可するかどうかは重要なポイントです。

C#の正規表現で \d はデフォルトで半角数字だけでなく全角数字も含むUnicodeの数字にマッチします。

つまり、\d は半角「123」と全角「123」の両方にマッチします。

ただし、半角数字のみを厳密に検証したい場合は、文字クラス [0-9] を使うか、RegexOptions.CultureInvariant を指定して文化依存の挙動を抑制します。

以下のサンプルコードで全角・半角数字の検証を比較します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string halfWidth = "12345";
        string fullWidth = "12345";
        // \d は全角・半角数字にマッチ
        Console.WriteLine(Regex.IsMatch(halfWidth, @"^\d+$")); // True
        Console.WriteLine(Regex.IsMatch(fullWidth, @"^\d+$")); // True
        // [0-9] は半角数字のみ
        Console.WriteLine(Regex.IsMatch(halfWidth, @"^[0-9]+$")); // True
        Console.WriteLine(Regex.IsMatch(fullWidth, @"^[0-9]+$")); // False
    }
}
True
True
True
False

全角数字を許可したい場合は \d を使い、半角数字のみを許可したい場合は [0-9] を使うのが基本です。

アラビア数字以外のスクリプト

Unicodeにはアラビア数字以外にも多くの数字スクリプトが存在します。

例えば、東アラビア数字(U+0660~U+0669)、デーヴァナーガリー数字(U+0966~U+096F)、タイ数字(U+0E50~U+0E59)などです。

\d や Unicodeの数字カテゴリ \p{Nd} はこれらの数字も含むため、国際化対応が必要な場合はこれらの数字も検出対象になります。

以下の例では、東アラビア数字を含む文字列に対して \d[0-9] のマッチを比較しています。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string arabicIndicDigits = "١٢٣"; // 東アラビア数字 (U+0661, U+0662, U+0663)
        // \d は東アラビア数字にもマッチ
        Console.WriteLine(Regex.IsMatch(arabicIndicDigits, @"^\d+$")); // True
        // [0-9] は半角数字のみ
        Console.WriteLine(Regex.IsMatch(arabicIndicDigits, @"^[0-9]+$")); // False
    }
}
True
False

このように、\d は多言語の数字を包括的に扱うため、国際化対応が必要なアプリケーションでは便利ですが、半角数字のみに限定したい場合は [0-9] を使うか、明示的に許可する数字スクリプトを指定する必要があります。

入力正規化と String.Normalize

Unicode文字列は同じ見た目でも複数のコードポイントの組み合わせで表現されることがあります。

例えば、合成済み文字と分解済み文字の違いです。

これを正規化(Normalization)と呼び、正規化を行うことで文字列の比較や正規表現マッチングの一貫性を保てます。

C#の String.Normalizeメソッドを使うと、Unicode正規化形式(NFC、NFD、NFKC、NFKD)に変換できます。

数字に関しては通常は問題になりにくいですが、全角数字や特殊な数字記号を扱う場合は正規化を行うことで予期せぬマッチング漏れを防げます。

以下は正規化の例です。

using System;
using System.Text;
class Program
{
    static void Main()
    {
        // 例として、分解済みのé(e + ´)を用意
        string decomposed = "e\u0301"; // e + アクセント記号
        string composed = "é";         // 合成済み文字
        Console.WriteLine($"分解済みです: {decomposed}, 長さ: {decomposed.Length}");
        Console.WriteLine($"合成済みです: {composed}, 長さ: {composed.Length}");
        // 正規化して比較
        bool equalsBefore = decomposed == composed;
        bool equalsAfter = decomposed.Normalize(NormalizationForm.FormC) == composed.Normalize(NormalizationForm.FormC);
        Console.WriteLine($"正規化前の比較: {equalsBefore}");
        Console.WriteLine($"正規化後の比較: {equalsAfter}");
    }
}
分解済みです: é, 長さ: 2
合成済みです: é, 長さ: 1
正規化前の比較: False
正規化後の比較: True

数字の入力検証でも、ユーザーが異なるUnicode表現を使う可能性がある場合は、Normalize を使って正規化してから正規表現を適用すると安全です。

これらのUnicodeと国際化対応のポイントを押さえることで、多言語環境でも正確かつ柔軟に数字の検証や抽出が行えます。

先読み・後読みの応用

正規表現の先読み(Lookahead)と後読み(Lookbehind)は、特定の条件を満たすかどうかを判定しつつ、マッチ対象の文字列自体には含めない高度なテクニックです。

数字の検証や抽出で条件を細かく制御したい場合に非常に役立ちます。

否定先読みで英字混入をチェック

否定先読み(Negative Lookahead)は、特定のパターンが続かないことを条件にマッチさせる方法です。

数字列に英字が混入していないかをチェックしたい場合に使えます。

例えば、数字のみで構成されているが、英字が含まれていないことを保証したい場合、以下のようなパターンが考えられます。

^\d+(?!.*[a-zA-Z])$

  • ^\d+:文字列の先頭から1文字以上の数字にマッチ
  • (?!.*[a-zA-Z]):後ろに英字が1文字でも続く場合はマッチしない(否定先読み)
  • $:文字列の末尾

このパターンは、数字の後に英字が含まれていないことを保証します。

サンプルコードを示します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string[] inputs = { "12345", "123a45", "67890", "12b34" };
        string pattern = @"^\d+(?!.*[a-zA-Z])$";
        foreach (var input in inputs)
        {
            bool isValid = Regex.IsMatch(input, pattern);
            Console.WriteLine($"{input} は数字のみかつ英字なし: {isValid}");
        }
    }
}
12345 は数字のみかつ英字なし: True
123a45 は数字のみかつ英字なし: False
67890 は数字のみかつ英字なし: True
12b34 は数字のみかつ英字なし: False

このように否定先読みを使うことで、数字列に英字が混入していないかを効率的にチェックできます。

肯定後読みで単位付き数値を許可

肯定後読み(Positive Lookbehind)は、特定のパターンの直後にマッチさせる条件を指定します。

単位付きの数値を抽出したい場合に便利です。

例えば、数値の後に「kg」や「m」などの単位が続く場合のみマッチさせたいとき、以下のように書けます。

(?<=\d)(kg|m) ただし、単位だけを抽出するのではなく、数値と単位の両方を含めてマッチさせる場合は、数値部分と単位部分を組み合わせてパターンを作ります。

以下は数値と単位を同時に抽出する例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "重さは100kg、長さは50m、幅は30cmです。";
        string pattern = @"\d+(?=kg|m|cm)";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("単位付き数値:");
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
単位付き数値:
100
50
30

この例では、\d+ の後に kgmcm のいずれかが続く場合に数字だけを抽出しています。

肯定後読みで単位の存在を条件にしているため、単位がない数字はマッチしません。

高速化のための単純化テクニック

正規表現は複雑になるほど処理時間が増加し、パフォーマンスに影響を与えます。

特に先読み・後読みを多用するとバックトラッキングが増えやすいため、パターンの単純化が重要です。

高速化のためのポイントは以下の通りです。

  • 具体的な文字クラスを使う

例えば、[0-9]\d のように明確な文字クラスを使い、曖昧なパターンを避けます。

  • 量指定子の最小化

*+ の無制限繰り返しはバックトラッキングを増やすため、可能な限り {n,m} のように範囲を限定します。

  • 先読み・後読みの使用を必要最小限に

先読み・後読みは便利ですが、使いすぎると複雑化するため、代替手段があればそちらを検討します。

  • 長いパターンは先にマッチさせる

パイプ | を使う場合は、長いパターンを先に書くことで早期マッチを促し、無駄な試行を減らす。

  • 正規表現のコンパイルオプションを活用する

RegexOptions.Compiled を使い、パターンをコンパイル済みにすることで実行速度を向上させます。

以下は単純化の例です。

複雑な否定先読みを使わず、単純な文字クラスと量指定子で数字のみを判定しています。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "12345";
        string pattern = @"^[0-9]{1,10}$"; // 1~10桁の数字のみ
        bool isMatch = Regex.IsMatch(input, pattern);
        Console.WriteLine($"{input} は数字のみ(1~10桁): {isMatch}");
    }
}
12345 は数字のみ(1~10桁): True

このように、必要な条件を満たしつつパターンをシンプルに保つことで、正規表現のパフォーマンスを最適化できます。

単体テストとデバッグ

正規表現は複雑になりやすく、意図した通りに動作しているかを確実に検証することが重要です。

ここではC#での単体テストフレームワークxUnitを使ったパターン検証方法、Visual Studioの正規表現ツールウィンドウの活用法、そして失敗ケースのログ出力について解説します。

xUnit でのパターン検証

xUnitは.NETで広く使われている単体テストフレームワークです。

正規表現のパターンが期待通りにマッチするかどうかをテストコードで自動化できます。

以下は、数字のみの文字列にマッチする正規表現パターンをxUnitで検証する例です。

using System.Text.RegularExpressions;
using Xunit;
public class RegexTests
{
    private const string Pattern = @"^\d+$";
    [Theory]
    [InlineData("12345", true)]
    [InlineData("abc123", false)]
    [InlineData("67890", true)]
    [InlineData("12 34", false)]
    public void TestDigitsOnly(string input, bool expected)
    {
        bool result = Regex.IsMatch(input, Pattern);
        Assert.Equal(expected, result);
    }
}

このコードでは、[Theory][InlineData] 属性を使い、複数の入力値と期待結果をまとめてテストしています。

Regex.IsMatch の結果が期待値と一致するかを検証し、失敗するとテストが落ちます。

xUnitを使うことで、正規表現の変更やリファクタリング時に動作保証ができ、品質を保てます。

VS 正規表現ツールウィンドウの利用

Visual Studioには正規表現のテストやデバッグに便利なツールウィンドウがあります。

これを使うと、リアルタイムでパターンのマッチ結果を確認でき、パターンの修正や検証が効率的に行えます。

  • 検索ウィンドウの正規表現モード

Ctrl + F で検索ウィンドウを開き、正規表現モードに切り替えると、入力したパターンがファイル内のどこにマッチするかを確認できます。

  • 拡張機能の利用

「Regex Tester」などのVisual Studio拡張機能をインストールすると、専用のウィンドウでパターンとテスト文字列を入力し、マッチ結果やキャプチャグループを視覚的に確認できます。

  • ライブプレビュー

パターンを編集すると即座にマッチ結果が更新されるため、複雑な正規表現の動作を理解しやすくなります。

これらのツールを活用することで、正規表現の開発効率が大幅に向上します。

失敗ケースのログ出力

正規表現のマッチングが期待通りに動作しない場合、どの入力で失敗したのかをログに残すことがトラブルシューティングに役立ちます。

特に大量のデータを処理するバッチやユーザー入力の検証で重要です。

以下は、マッチ失敗時に入力文字列をログに出力する例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    private const string Pattern = @"^\d+$";
    static void Main()
    {
        string[] inputs = { "12345", "abc123", "67890", "12 34" };
        foreach (var input in inputs)
        {
            if (!Regex.IsMatch(input, Pattern))
            {
                Console.WriteLine($"[ERROR] 入力が不正です: \"{input}\"");
            }
            else
            {
                Console.WriteLine($"入力 \"{input}\" は有効です。");
            }
        }
    }
}
入力 "12345" は有効です。
[ERROR] 入力が不正です: "abc123"
入力 "67890" は有効です。
[ERROR] 入力が不正です: "12 34"

実際のアプリケーションでは、ログファイルや監査システムに出力することが多いですが、コンソール出力でもデバッグ時に役立ちます。

ログには失敗した入力だけでなく、どのパターンで失敗したかや、マッチングに使った正規表現も記録すると、原因特定がスムーズになります。

これらの単体テストとデバッグ手法を組み合わせることで、正規表現の品質を高め、開発や保守の効率化を図れます。

よくある落とし穴

正規表現は強力なツールですが、使い方を誤るとパフォーマンス低下や誤マッチなどの問題が発生しやすいです。

ここでは数字の判定や抽出で特に注意すべき代表的な落とし穴を解説します。

バックトラッキングによる性能低下

正規表現のバックトラッキングとは、パターンのマッチング中に複数の候補を試行し、失敗した場合に前の状態に戻って別のパターンを試す動作です。

複雑なパターンや曖昧な量指定子を多用すると、バックトラッキングが膨大になり、処理時間が急激に増加することがあります。

特に数字の桁数制限や繰り返しを含むパターンで、.*.+ のような貪欲な量指定子を使うと、ReDoS(正規表現拒否サービス攻撃)と呼ばれる脆弱性の原因にもなります。

例として、以下のパターンはバックトラッキングが多発しやすいです。

^(a+)+$ このパターンは「1文字以上の ‘a’ の繰り返し」がさらに繰り返されるため、長い文字列に対して非常に遅くなります。

数字の判定でよくある例は、桁数制限を曖昧に書いたパターンです。

string pattern = @"^\d{1,1000}$";

このように大量の繰り返しを指定すると、入力が長い場合にバックトラッキングが増え、パフォーマンスが低下します。

対策としては、

  • 量指定子の範囲を必要最小限に絞る
  • 貪欲な量指定子の代わりに限定的なものを使う
  • RegexOptions.Compiled を使う
  • タイムアウトを設定する

などがあります。

文字クラスにおける範囲指定ミス

文字クラスの範囲指定は [0-9] のように書きますが、範囲の順序や指定ミスで意図しない文字が含まれることがあります。

例えば、[9-0] と書くと範囲が逆になり、正しくマッチしません。

C#の正規表現では範囲の開始文字コードが終了文字コードより小さい必要があります。

また、複数の範囲を混ぜる際にハイフンの位置を間違えると、ハイフン自体が文字として認識されてしまうこともあります。

  • [0-9a-z] は0~9とa~zの範囲を含む
  • [0-9\-a-z] は0~9、ハイフン、a~zを含む(ハイフンを文字として扱うためにエスケープが必要)

ハイフンを文字として含めたい場合は、先頭か末尾に置くか、\- とエスケープしてください。

誤った例:

string pattern = @"[0-9-az]";

これは 0~9-az のいずれかにマッチしますが、意図が不明瞭で誤解を招きます。

桁数制限が意図通りに動かないケース

数字の桁数制限を正規表現で行う際、量指定子 {n,m} を使いますが、パターンの書き方によっては意図しないマッチが発生することがあります。

例えば、以下のパターンは「3桁から6桁の数字」にマッチさせたい意図ですが、実際には部分的にマッチすることがあります。

string pattern = @"\d{3,6}";

このパターンは文字列のどこかに3~6桁の数字があればマッチしますが、文字列全体がその桁数であることは保証しません。

文字列全体の桁数を制限したい場合は、アンカーを使って以下のように書く必要があります。

string pattern = @"^\d{3,6}$";

また、複数のパターンをパイプ | で組み合わせる場合、優先順位に注意しないと短いパターンに先にマッチしてしまい、長い桁数の数字が正しくマッチしないことがあります。

string pattern = @"\d{3}|\d{5}";

この場合、5桁の数字でも3桁の部分にマッチしてしまうため、長いパターンを先に書くことが推奨されます。

string pattern = @"\d{5}|\d{3}";

このように、桁数制限のパターンはアンカーの有無やパターンの順序に注意して設計してください。

正規表現と数値パースの使い分け

数字の入力検証や抽出を行う際、C#では正規表現と数値パースint.TryParsedouble.TryParseの両方が利用されます。

それぞれの特徴を理解し、適切に使い分けることが重要です。

int.TryParse と double.TryParse

int.TryParsedouble.TryParse は、文字列を整数型や浮動小数点型に変換できるかどうかを判定し、変換に成功した場合は対応する数値を取得します。

これらは数値としての妥当性を厳密にチェックできるため、数値演算やビジネスロジックでの利用に適しています。

以下は int.TryParse の例です。

using System;
class Program
{
    static void Main()
    {
        string input = "12345";
        if (int.TryParse(input, out int number))
        {
            Console.WriteLine($"整数に変換成功: {number}");
        }
        else
        {
            Console.WriteLine("整数への変換に失敗しました。");
        }
    }
}
整数に変換成功: 12345

double.TryParse は小数点や指数表記を含む文字列も変換可能です。

using System;
class Program
{
    static void Main()
    {
        string input = "3.14159";
        if (double.TryParse(input, out double number))
        {
            Console.WriteLine($"浮動小数点数に変換成功: {number}");
        }
        else
        {
            Console.WriteLine("浮動小数点数への変換に失敗しました。");
        }
    }
}
浮動小数点数に変換成功: 3.14159

これらのメソッドは、数値としての妥当性を保証し、文化依存のフォーマット(小数点や桁区切り)も考慮されるため、数値処理に最適です。

フォーマット制約が強い場合のメリット

一方、正規表現は数値のフォーマットを細かく制約したい場合に有効です。

例えば、特定の桁数、符号の有無、小数点以下の桁数制限、特定の単位付き数値など、ビジネスルールに沿った入力検証が可能です。

正規表現を使うメリットは以下の通りです。

  • 入力形式の厳密な制御

例えば、3桁から6桁の数字のみ許可、符号付きの小数点数のみ許可など、細かい条件をパターンで表現できます。

  • 部分文字列の抽出や置換が可能

数値の一部だけを抽出したり、特定の形式に変換したりできます。

  • 文化依存を排除できる

正規表現は文化に依存しない文字列パターンで判定できるため、特定のフォーマットに限定したい場合に便利。

例えば、3桁から6桁の数字のみを許可する正規表現は以下のように書けます。

string pattern = @"^\d{3,6}$";

このような細かい制約は、TryParse だけでは実現しにくい場合があります。

パース成功後のビジネスロジック分岐

正規表現やパースで入力が妥当と判定された後は、数値としての処理やビジネスロジックに分岐します。

例えば、数値の範囲チェックや単位変換、計算処理などです。

以下は、正規表現で形式を検証し、int.TryParse で数値に変換してから範囲チェックを行う例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "150";
        string pattern = @"^\d{1,3}$"; // 1~3桁の数字
        if (Regex.IsMatch(input, pattern) && int.TryParse(input, out int number))
        {
            if (number >= 100 && number <= 200)
            {
                Console.WriteLine($"入力値 {number} は許容範囲内です。");
            }
            else
            {
                Console.WriteLine($"入力値 {number} は範囲外です。");
            }
        }
        else
        {
            Console.WriteLine("入力が不正です。");
        }
    }
}
入力値 150 は許容範囲内です。

このように、正規表現でフォーマットを制限し、パースで数値変換を行い、ビジネスルールに基づく処理を実装するのが一般的なパターンです。

正規表現と数値パースはそれぞれ得意分野が異なるため、用途に応じて使い分けることで堅牢かつ効率的な数値処理が実現できます。

ケーススタディ: 業務シナリオ別パターン例

会員IDチェック

会員IDは通常、特定の形式や桁数で構成されることが多いです。

例えば、英数字の組み合わせで8桁固定、先頭は必ずアルファベットというルールがある場合、正規表現で簡単に検証できます。

以下は、先頭がアルファベット1文字、その後に7桁の英数字が続く会員IDの検証例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string[] inputs = { "A1234567", "B7654321", "12345678", "AB123456" };
        string pattern = @"^[A-Za-z][A-Za-z0-9]{7}$";
        foreach (var input in inputs)
        {
            bool isValid = Regex.IsMatch(input, pattern);
            Console.WriteLine($"{input} は有効な会員IDか: {isValid}");
        }
    }
}
A1234567 は有効な会員IDか: True
B7654321 は有効な会員IDか: True
12345678 は有効な会員IDか: False
AB123456 は有効な会員IDか: True

このように、正規表現で会員IDの形式を厳密にチェックできます。

クレジットカード番号の基本検証

クレジットカード番号は通常16桁の数字で構成され、4桁ごとにスペースやハイフンで区切られることもあります。

基本的な検証として、16桁の数字か、4桁区切りの形式かを判定するパターンを作成します。

以下は、16桁の数字または「1234-5678-9012-3456」や「1234 5678 9012 3456」の形式を許可する例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string[] inputs = {
            "1234567890123456",
            "1234-5678-9012-3456",
            "1234 5678 9012 3456",
            "1234_5678_9012_3456"
        };
        string pattern = @"^(\d{16}|(\d{4}[- ]\d{4}[- ]\d{4}[- ]\d{4}))$";
        foreach (var input in inputs)
        {
            bool isValid = Regex.IsMatch(input, pattern);
            Console.WriteLine($"{input} は有効なカード番号か: {isValid}");
        }
    }
}
1234567890123456 は有効なカード番号か: True
1234-5678-9012-3456 は有効なカード番号か: True
1234 5678 9012 3456 は有効なカード番号か: True
1234_5678_9012_3456 は有効なカード番号か: False

このパターンは基本的な形式チェックであり、実際のカード番号検証にはLuhnアルゴリズムなどの追加処理が必要です。

バージョン番号の抽出

ソフトウェアのバージョン番号は「メジャー.マイナー.パッチ」の形式が一般的です。

数字がドットで区切られたパターンを抽出する正規表現を作成します。

以下は、文字列中から「数字.数字.数字」の形式を抽出する例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "現在のバージョンは1.2.3で、次は2.0.0が予定されています。";
        string pattern = @"\d+\.\d+\.\d+";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("抽出されたバージョン番号:");
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
抽出されたバージョン番号:
1.2.3
2.0.0

\b は単語境界を表し、数字の区切りを明確にしています。

ファイル名から連番を取得

ファイル名に連番が含まれている場合、その連番部分だけを抽出したいことがあります。

例えば、「image_001.jpg」「image_002.jpg」のようなファイル名から「001」「002」を取得します。

以下は、ファイル名の末尾にある3桁の連番を抽出する例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string[] filenames = { "image_001.jpg", "image_002.jpg", "photo_123.png", "doc_45.txt" };
        string pattern = @"_(\d{3})\.";
        foreach (var filename in filenames)
        {
            Match match = Regex.Match(filename, pattern);
            if (match.Success)
            {
                Console.WriteLine($"{filename} の連番: {match.Groups[1].Value}");
            }
            else
            {
                Console.WriteLine($"{filename} に連番はありません。");
            }
        }
    }
}
image_001.jpg の連番: 001
image_002.jpg の連番: 002
photo_123.png の連番: 123
doc_45.txt に連番はありません。

このように、キャプチャグループを使って連番部分だけを簡単に取得できます。

桁数や区切り文字は業務ルールに合わせて調整してください。

実務での運用ポイント

設定ファイルにパターンを外出し

正規表現パターンをソースコードに直接埋め込むと、パターンの修正や管理が煩雑になりやすいです。

実務では、正規表現パターンを設定ファイルや外部リソースに分離して管理することが推奨されます。

設定ファイルにパターンを外出しすることで、以下のメリットがあります。

  • パターンの修正が容易

コードを再コンパイルせずにパターンを変更できるため、運用中の微調整やルール変更に柔軟に対応可能です。

  • 複数環境での差分管理がしやすい

開発・テスト・本番環境で異なるパターンを使い分ける場合に便利です。

  • 非エンジニアでもパターンの調整が可能

運用担当者が設定ファイルを編集するだけで対応できるケースもあります。

例えば、JSON形式の設定ファイルにパターンを記述し、アプリケーション起動時に読み込む方法があります。

{
  "Patterns": {
    "MemberId": "^[A-Za-z][A-Za-z0-9]{7}$",
    "PhoneNumber": @"^\\d{3}-\\d{4}-\\d{4}$"
  }
}

C#コードでは、設定ファイルからパターンを読み込み、Regexオブジェクトを生成して利用します。

// 設定ファイルから読み込んだパターンを使う例
string memberIdPattern = config["Patterns"]["MemberId"];
Regex memberIdRegex = new Regex(memberIdPattern);

このようにパターンを外部化することで、運用性と保守性が向上します。

ユーザー入力を即時フィードバック

ユーザーがフォームや入力欄に数字を入力する際、正規表現を使ってリアルタイムに入力内容を検証し、即時にフィードバックを返すことがUX向上に繋がります。

例えば、Webアプリケーションやデスクトップアプリで、入力欄の変更イベントに対して正規表現でマッチ判定を行い、入力が不正な場合はエラーメッセージや入力欄の色を変えるなどの対応が考えられます。

C#のWPFやWinFormsでは、TextChanged イベントで以下のように検証できます。

private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
    string input = ((TextBox)sender).Text;
    string pattern = @"^\d{3,6}$"; // 3~6桁の数字
    bool isValid = Regex.IsMatch(input, pattern);
    if (isValid)
    {
        // 正しい入力の場合の処理(例:背景色を白に戻す)
        ((TextBox)sender).Background = Brushes.White;
    }
    else
    {
        // 不正な入力の場合の処理(例:背景色を赤にする)
        ((TextBox)sender).Background = Brushes.LightPink;
    }
}

このように即時フィードバックを実装すると、ユーザーは入力ミスに気づきやすくなり、入力完了後のエラー発生を減らせます。

監査ログにマッチ結果を残す

業務システムでは、入力検証の結果や正規表現のマッチ結果を監査ログとして記録することが求められる場合があります。

特に金融や医療などの厳格なコンプライアンスが必要な分野では重要です。

監査ログにマッチ結果を残すことで、

  • 不正入力や異常値の追跡が可能になる
  • トラブル発生時の原因調査が容易になる
  • 運用ルールの遵守状況を証明できる

といったメリットがあります。

ログには以下の情報を含めると効果的です。

項目内容例
入力値ユーザーが入力した文字列
マッチ結果正規表現にマッチしたかどうか
使用したパターン適用した正規表現パターン
タイムスタンプ入力検証を行った日時
ユーザーID入力者の識別情報

C#での簡単なログ出力例は以下の通りです。

string input = "12345";
string pattern = @"^\d{3,6}$";
bool isMatch = Regex.IsMatch(input, pattern);
string logMessage = $"[{DateTime.Now}] 入力値: \"{input}\", パターン: \"{pattern}\", マッチ結果: {isMatch}";
Console.WriteLine(logMessage);
// 実際はファイルやDBに書き込むことが多い

このように監査ログを適切に残すことで、運用上の信頼性と透明性を確保できます。

参考実装の拡張アイデア

Span<char> API との連携

C# 7.2以降で導入された Span<char> は、文字列の部分的なスライスを効率的に扱うための構造体です。

Span<char> を使うことで、文字列のコピーを避けつつ高速に部分文字列を操作できます。

正規表現と組み合わせることで、パフォーマンスを向上させることが可能です。

例えば、Regex.Match の結果から取得した Match.Value は新しい文字列として生成されますが、MatchIndexLength を使い、元の文字列の Span<char> をスライスして部分文字列を参照することができます。

以下は Span<char> を使ってマッチ部分を効率的に取得する例です。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "abc123def456";
        Regex regex = new Regex(@"\d+");
        MatchCollection matches = regex.Matches(input);
        ReadOnlySpan<char> span = input.AsSpan();
        foreach (Match match in matches)
        {
            // MatchのIndexとLengthを使ってSpanをスライス
            ReadOnlySpan<char> matchedSpan = span.Slice(match.Index, match.Length);
            Console.WriteLine(matchedSpan.ToString());
        }
    }
}
123
456

この方法は大量の文字列処理やパフォーマンスが重要な場面で有効です。

Span<char> はヒープ割り当てを減らし、GC負荷を軽減します。

RegexEnumerator を使った foreach パターン

.NET 6以降では、Regexクラスに EnumerateMatchesメソッドが追加され、MatchCollection の代わりに IEnumerable<Match> を返すようになりました。

これにより、foreach ループで遅延評価しながらマッチを処理でき、メモリ効率が向上します。

以下は EnumerateMatches を使った例です。

using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main()
    {
        string input = "abc123def456";
        Regex regex = new Regex(@"\d+");
        foreach (Match match in regex.Matches(input))
        {
            Console.WriteLine(match.Value);
        }
    }
}
123
456

EnumerateMatches は大量のマッチがある場合やストリーム処理に適しており、すべてのマッチを一度にメモリに保持しないため効率的です。

非同期パイプラインでの利用

近年のC#では非同期プログラミングが主流となっており、正規表現の結果を非同期パイプラインで処理するケースも増えています。

例えば、ファイルやネットワークから大量のテキストを読み込みつつ、正規表現でマッチを抽出し、非同期に処理を進めることが考えられます。

Regex.EnumerateMatches は同期的ですが、非同期ストリームIAsyncEnumerable<T>と組み合わせて使うことで、非同期パイプラインを構築できます。

以下は非同期でテキストを読み込みつつ、正規表現マッチを非同期に処理するイメージ例です。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
class Program
{
    static async IAsyncEnumerable<string> ReadLinesAsync(string path)
    {
        using StreamReader reader = new StreamReader(path);
        string? line;
        while ((line = await reader.ReadLineAsync()) != null)
        {
            yield return line;
        }
    }
    static async Task Main()
    {
        string pattern = @"\d+";
        Regex regex = new Regex(pattern);
        await foreach (var line in ReadLinesAsync("example.txt"))
        {
            foreach (Match match in regex.Matches(line))
            {
                Console.WriteLine(match.Value);
            }
        }
    }
}

このように、非同期で読み込んだテキストを逐次処理しつつ、正規表現マッチを効率的に扱えます。

大規模データやリアルタイム処理に適した設計です。

これらの拡張アイデアを活用することで、C#の正規表現処理をより効率的かつ柔軟に実装できます。

特にパフォーマンスや非同期処理が求められる実務環境で効果を発揮します。

まとめ

この記事では、C#での数字判定や抽出に役立つ正規表現の基礎から応用、パフォーマンス最適化、Unicode対応、実務での運用ポイントまで幅広く解説しました。

正規表現の基本構文や量指定子、先読み・後読みの活用法、Regexクラスの各種メソッドの使い方を理解でき、効率的な数字処理が可能になります。

さらに、単体テストやデバッグ、実務での設定管理やログ記録の重要性も紹介し、実践的な運用ノウハウを提供しています。

これにより、堅牢で保守性の高い数字処理を実装できるようになります。

関連記事

Back to top button