文字列

【C#】正規表現と空白処理を極める実践テクニック集

C#の正規表現では空白文字を\sで表し、スペース、タブ、改行を一括で扱えます。

連続空白には\s+が便利で、Regex.Split(text, "\s+")ならトークン分割、Regex.Replace(text, "\s", "")なら全面削除が完結します。

パターン記述内の余計な空白を無視したい場合はRegexOptions.IgnorePatternWhitespaceを指定すると可読性と保守性を確保しながら処理できます。

空白を表す基本トークン

正規表現で空白文字を扱う際に最も基本となるのが、空白を表すトークンです。

C#の正規表現では、\s\Sが空白文字の判定に使われます。

ここではそれぞれの役割や使い方を詳しく解説いたします。

\sと\Sの役割

\sは「空白文字」を意味する正規表現の特殊文字クラスです。

空白文字とは、スペース、タブ、改行\n、復帰\r、フォームフィード\f、垂直タブ\vなどの文字を指します。

つまり、\sはこれらのいずれか1文字にマッチします。

一方、\S\sの否定で、「空白文字以外の任意の1文字」にマッチします。

つまり、空白以外の文字を探したい場合に使います。

サンプルコード:\sと\Sの違いを確認する

using System;
using System.Text.RegularExpressions;
public class WhitespaceExample
{
    public static void Main()
    {
        string input = "A B\tC\nD";
        Console.WriteLine("Input文字列: " + input);
        // 空白文字にマッチする部分を抽出
        MatchCollection whitespaceMatches = Regex.Matches(input, @"\s");
        Console.WriteLine("空白文字の数: " + whitespaceMatches.Count);
        foreach (Match match in whitespaceMatches)
        {
            Console.WriteLine($"空白文字: '{match.Value}' (位置: {match.Index})");
        }
        // 空白以外の文字にマッチする部分を抽出
        MatchCollection nonWhitespaceMatches = Regex.Matches(input, @"\S");
        Console.WriteLine("空白以外の文字の数: " + nonWhitespaceMatches.Count);
        foreach (Match match in nonWhitespaceMatches)
        {
            Console.WriteLine($"空白以外の文字: '{match.Value}' (位置: {match.Index})");
        }
    }
}
Input文字列: A B	C
D
空白文字の数: 3
空白文字: ' ' (位置: 1)
空白文字: '	' (位置: 3)
空白文字: '
' (位置: 5)
空白以外の文字の数: 4
空白以外の文字: 'A' (位置: 0)
空白以外の文字: 'B' (位置: 2)
空白以外の文字: 'C' (位置: 4)
空白以外の文字: 'D' (位置: 6)

このコードでは、文字列 "A B\tC\nD" に含まれる空白文字(スペース、タブ、改行)と空白以外の文字をそれぞれ抽出しています。

\sは空白文字3つにマッチし、\Sはそれ以外の4文字にマッチしていることがわかります。

連続空白を捉える\s+

単一の空白文字を捉える\sに対して、\s+は「1文字以上の連続した空白文字」にマッチします。

これにより、複数の空白やタブ、改行が連続している部分をまとめて扱うことが可能です。

例えば、文章中の複数のスペースを1つの空白として扱いたい場合や、空白で区切られた単語を分割したい場合に便利です。

サンプルコード:連続空白の検出と置換

using System;
using System.Text.RegularExpressions;
public class ConsecutiveWhitespaceExample
{
    public static void Main()
    {
        string input = "Hello   World\t\tThis  is\n\nC#";
        Console.WriteLine("元の文字列: '" + input + "'");
        // 連続する空白文字を検出
        MatchCollection matches = Regex.Matches(input, @"\s+");
        Console.WriteLine("連続空白の数: " + matches.Count);
        foreach (Match match in matches)
        {
            Console.WriteLine($"連続空白: '{match.Value.Replace("\n", "\\n").Replace("\t", "\\t")}' (長さ: {match.Length}, 位置: {match.Index})");
        }
        // 連続空白を単一スペースに置換
        string replaced = Regex.Replace(input, @"\s+", " ");
        Console.WriteLine("置換後の文字列: '" + replaced + "'");
    }
}
元の文字列: 'Hello   World		This  is

C#'
連続空白の数: 4
連続空白: '   ' (長さ: 3, 位置: 5)
連続空白: '\t\t' (長さ: 2, 位置: 13)
連続空白: '  ' (長さ: 2, 位置: 19)
連続空白: '\n\n' (長さ: 2, 位置: 23)
置換後の文字列: 'Hello World This is C#'

この例では、複数の空白やタブ、改行が連続している部分を検出し、それらをすべて単一のスペースに置換しています。

\s+を使うことで、どんな種類の空白文字が何文字連続していてもまとめて扱えることがわかります。

行頭・行末の空白指定

空白文字は文字列の中だけでなく、行の先頭や末尾に存在することも多いです。

これらの空白を正規表現で指定するには、行頭・行末のアンカーと組み合わせて使います。

  • ^ は行頭を示します
  • $ は行末を示します

これらと\s*(0個以上の空白)や\s+(1個以上の空白)を組み合わせることで、行頭や行末の空白を検出・削除できます。

サンプルコード:行頭・行末の空白を検出・削除

using System;
using System.Text.RegularExpressions;
public class LineEdgeWhitespaceExample
{
    public static void Main()
    {
        string input = "  Hello World  \n\tThis is C#  \n  End  ";
        Console.WriteLine("元の文字列:");
        Console.WriteLine(input);
        // 行頭の空白を検出
        MatchCollection leadingSpaces = Regex.Matches(input, @"^\s+", RegexOptions.Multiline);
        Console.WriteLine("\n行頭の空白数: " + leadingSpaces.Count);
        foreach (Match match in leadingSpaces)
        {
            Console.WriteLine($"行頭空白: '{match.Value.Replace("\n", "\\n").Replace("\t", "\\t")}' (長さ: {match.Length})");
        }
        // 行末の空白を検出
        MatchCollection trailingSpaces = Regex.Matches(input, @"\s+$", RegexOptions.Multiline);
        Console.WriteLine("\n行末の空白数: " + trailingSpaces.Count);
        foreach (Match match in trailingSpaces)
        {
            Console.WriteLine($"行末空白: '{match.Value.Replace("\n", "\\n").Replace("\t", "\\t")}' (長さ: {match.Length})");
        }
        // 行頭・行末の空白を削除
        string trimmed = Regex.Replace(input, @"^\s+|\s+$", "", RegexOptions.Multiline);
        Console.WriteLine("\n行頭・行末の空白を削除した文字列:");
        Console.WriteLine(trimmed);
    }
}
元の文字列:
  Hello World  
	This is C#  
  End  

行頭の空白数: 3
行頭空白: '  ' (長さ: 2)
行頭空白: '\t' (長さ: 1)
行頭空白: '  ' (長さ: 2)

行末の空白数: 3
行末空白: '  ' (長さ: 2)
行末空白: '  ' (長さ: 2)
行末空白: '  ' (長さ: 2)

行頭・行末の空白を削除した文字列:
Hello World
This is C#
End

このコードでは、RegexOptions.Multilineオプションを使い、複数行のそれぞれの行頭・行末の空白を検出しています。

^\s+は行頭の空白を、\s+$は行末の空白を表します。

最後に、これらをまとめて削除する正規表現で文字列をトリムしています。

これらの基本トークンを理解することで、C#の正規表現で空白文字を自在に扱えるようになります。

次のステップでは、これらを使った空白の検出や削除、分割などの具体的な操作方法を解説いたします。

空白の検出手法

Regex.IsMatchで判定

空白文字が文字列に含まれているかどうかを簡単に判定したい場合は、Regex.IsMatchメソッドが便利です。

このメソッドは、指定した正規表現パターンにマッチする部分が文字列内に存在するかを真偽値で返します。

空白文字の判定には、\sを使います。

例えば、文字列に空白が1つでもあればtrueを返し、なければfalseとなります。

using System;
using System.Text.RegularExpressions;
public class IsMatchExample
{
    public static void Main()
    {
        string input1 = "Hello World";
        string input2 = "HelloWorld";
        // 空白文字が含まれているか判定
        bool hasWhitespace1 = Regex.IsMatch(input1, @"\s");
        bool hasWhitespace2 = Regex.IsMatch(input2, @"\s");
        Console.WriteLine($"'{input1}' に空白は含まれているか? {hasWhitespace1}");
        Console.WriteLine($"'{input2}' に空白は含まれているか? {hasWhitespace2}");
    }
}
'Hello World' に空白は含まれているか? True
'HelloWorld' に空白は含まれているか? False

この例では、input1はスペースを含むためtrueが返り、input2は空白がないためfalseとなっています。

Regex.IsMatchは単純に存在の有無を判定したいときに最適です。

Regex.Matchesで位置取得

空白文字が文字列のどこにあるか、すべての位置を知りたい場合はRegex.Matchesを使います。

このメソッドは、正規表現にマッチするすべての部分をMatchCollectionとして返します。

Matchオブジェクトには、マッチした文字列の値や開始位置Indexが含まれています。

using System;
using System.Text.RegularExpressions;
public class MatchesExample
{
    public static void Main()
    {
        string input = "A B\tC\nD";
        // 空白文字すべての位置を取得
        MatchCollection matches = Regex.Matches(input, @"\s");
        Console.WriteLine($"空白文字の数: {matches.Count}");
        foreach (Match match in matches)
        {
            Console.WriteLine($"空白文字: '{match.Value.Replace("\n", "\\n").Replace("\t", "\\t")}' (位置: {match.Index})");
        }
    }
}
空白文字の数: 3
空白文字: ' ' (位置: 1)
空白文字: '	' (位置: 3)
空白文字: '\n' (位置: 5)

このコードでは、空白文字が3か所に存在し、それぞれの位置が表示されています。

\sはスペース、タブ、改行などすべての空白文字にマッチします。

キャプチャグループの活用

空白文字の検出に加えて、特定のパターンの中で空白を含む部分を抽出したい場合は、キャプチャグループを使うと便利です。

キャプチャグループは、丸括弧()で囲んだ部分のマッチを個別に取得できます。

例えば、単語と単語の間にある空白をキャプチャして、その空白の種類や長さを調べることが可能です。

using System;
using System.Text.RegularExpressions;
public class CaptureGroupExample
{
    public static void Main()
    {
        string input = "Hello   World\tC#  Programming";
        // 単語間の空白をキャプチャグループで取得
        string pattern = @"(\s+)";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine($"単語間の空白の数: {matches.Count}");
        foreach (Match match in matches)
        {
            Console.WriteLine($"空白: '{match.Groups[1].Value.Replace("\t", "\\t")}' (長さ: {match.Length}, 位置: {match.Index})");
        }
    }
}
単語間の空白の数: 3
空白: '   ' (長さ: 3, 位置: 5)
空白: '\t' (長さ: 1, 位置: 13)
空白: '  ' (長さ: 2, 位置: 16)

この例では、\s+をキャプチャグループ()で囲み、単語間の空白部分をすべて抽出しています。

match.Groups[1]でキャプチャした空白部分の文字列を取得でき、空白の種類や長さを詳細に扱えます。

キャプチャグループを使うことで、単に空白の有無を調べるだけでなく、空白の具体的な内容や位置を柔軟に操作できるようになります。

空白の削除・正規化

全空白の一括削除

文字列からすべての空白文字を一括で削除したい場合は、Regex.Replaceメソッドを使い、パターンに\sを指定して空白文字を空文字に置換します。

\sはスペース、タブ、改行などすべての空白文字にマッチするため、これを使うと文字列中の空白を完全に取り除けます。

using System;
using System.Text.RegularExpressions;
public class RemoveAllWhitespace
{
    public static void Main()
    {
        string input = "  Hello \t World \n This is C#  ";
        Console.WriteLine("元の文字列: '" + input + "'");
        // すべての空白文字を削除
        string result = Regex.Replace(input, @"\s", "");
        Console.WriteLine("空白削除後の文字列: '" + result + "'");
    }
}
元の文字列: '  Hello 	 World 
 This is C#  '
空白削除後の文字列: 'HelloWorldThisisC#'

このコードでは、文字列中のスペース、タブ、改行がすべて削除され、連結された文字列が得られています。

空白を完全に除去したい場合に有効です。

多重スペースを単一スペースへ

文章や入力データの中で、複数の空白が連続している場合、それを単一のスペースに正規化することがよくあります。

これもRegex.Replaceを使い、\s+(1つ以上の連続空白)を単一のスペース" "に置換することで実現できます。

using System;
using System.Text.RegularExpressions;
public class NormalizeSpaces
{
    public static void Main()
    {
        string input = "This   is  a    test.\tLet's normalize   spaces.";
        Console.WriteLine("元の文字列: '" + input + "'");
        // 連続する空白を単一スペースに置換
        string normalized = Regex.Replace(input, @"\s+", " ");
        Console.WriteLine("正規化後の文字列: '" + normalized + "'");
    }
}
元の文字列: 'This   is  a    test.	Let's normalize   spaces.'
正規化後の文字列: 'This is a test. Let's normalize spaces.'

この例では、複数のスペースやタブが1つのスペースにまとめられ、読みやすい文字列に整形されています。

文章のフォーマットを整えたいときに便利です。

タブとスペースの置換

タブ文字は見た目の幅が環境によって異なるため、タブをスペースに置換して統一したいケースがあります。

正規表現でタブ文字\tを検出し、任意の数のスペースに置換することが可能です。

using System;
using System.Text.RegularExpressions;
public class TabToSpaceReplacement
{
    public static void Main()
    {
        string input = "Column1\tColumn2\tColumn3";
        Console.WriteLine("元の文字列: '" + input + "'");
        // タブを4つのスペースに置換
        string replaced = Regex.Replace(input, @"\t", "    ");
        Console.WriteLine("タブ置換後の文字列: '" + replaced + "'");
    }
}
元の文字列: 'Column1	Column2	Column3'
タブ置換後の文字列: 'Column1    Column2    Column3'

このコードでは、タブ文字を4つのスペースに置換しています。

タブ幅を揃えたい場合や、タブを含む文字列をスペースベースのフォーマットに変換したい場合に役立ちます。

これらの方法を組み合わせることで、文字列中の空白を自在に削除・正規化できます。

用途に応じて適切なパターンと置換文字列を選択してください。

空白による分割

Regex.Splitの基本

文字列を空白文字で分割したい場合、Regex.Splitメソッドが非常に便利です。

Regex.Splitは、指定した正規表現パターンにマッチする部分を区切り文字として文字列を分割し、配列として返します。

空白文字で分割する場合は、\s+をパターンに指定します。

\s+は1つ以上の連続した空白文字にマッチするため、複数の空白やタブ、改行が連続していてもまとめて分割できます。

using System;
using System.Text.RegularExpressions;
public class RegexSplitBasic
{
    public static void Main()
    {
        string input = "apple  banana\torange\npear  grape";
        Console.WriteLine("元の文字列: '" + input + "'");
        // 空白文字(連続も含む)で分割
        string[] tokens = Regex.Split(input, @"\s+");
        Console.WriteLine("分割結果:");
        foreach (string token in tokens)
        {
            Console.WriteLine($"'{token}'");
        }
    }
}
元の文字列: 'apple  banana	orange
pear  grape'
分割結果:
'apple'
'banana'
'orange'
'pear'
'grape'

この例では、スペース、タブ、改行を含む連続空白で文字列を分割し、単語ごとに分割できています。

Regex.Splitは空白以外の複雑な区切り文字にも対応できるため、柔軟な分割処理に適しています。

空要素制御とオプション

Regex.Splitを使う際に注意したいのが、空の要素が配列に含まれる場合です。

例えば、文字列の先頭や末尾に空白がある場合や、連続した区切り文字があると、空文字列が分割結果に含まれます。

using System;
using System.Text.RegularExpressions;
public class RegexSplitEmptyElements
{
    public static void Main()
    {
        string input = "  apple  banana  ";
        Console.WriteLine("元の文字列: '" + input + "'");
        // 空白で分割(空要素が含まれる)
        string[] tokens = Regex.Split(input, @"\s+");
        Console.WriteLine("分割結果(空要素含む):");
        foreach (string token in tokens)
        {
            Console.WriteLine($"'{token}'");
        }
        // 空要素を除去する方法
        string[] filteredTokens = Array.FindAll(tokens, s => !string.IsNullOrEmpty(s));
        Console.WriteLine("\n空要素を除去した結果:");
        foreach (string token in filteredTokens)
        {
            Console.WriteLine($"'{token}'");
        }
    }
}
元の文字列: '  apple  banana  '
分割結果(空要素含む):
''
'apple'
'banana'
''

空要素を除去した結果:
'apple'
'banana'

このように、Regex.Splitは空白が連続した部分や文字列の端に空白があると空文字列が生成されます。

空文字列を除去したい場合は、Array.FindAllやLINQのWhereメソッドでフィルタリングするとよいでしょう。

また、Regex.Splitには分割数を制限するオーバーロードもあります。

例えば、最大分割数を指定して、分割結果の配列の長さを制御できます。

string[] limitedTokens = Regex.Split(input, @"\s+", 2);

この例では、最大2つの要素に分割され、2番目の要素には残りの文字列がすべて含まれます。

CSV風文字列への応用

空白文字だけでなく、カンマやスペースなど複数の区切り文字が混在する文字列を分割したい場合もあります。

例えば、CSV風の文字列で、カンマの前後に空白があるケースです。

このような場合は、正規表現で区切り文字のパターンを工夫して指定します。

例えば、カンマの前後にある空白も含めて分割したい場合は、\s*,\s*のように書きます。

using System;
using System.Text.RegularExpressions;
public class CsvLikeSplit
{
    public static void Main()
    {
        string input = "apple, banana ,orange ,  pear,grape";
        Console.WriteLine("元の文字列: '" + input + "'");
        // カンマの前後の空白を含めて分割
        string[] tokens = Regex.Split(input, @"\s*,\s*");
        Console.WriteLine("分割結果:");
        foreach (string token in tokens)
        {
            Console.WriteLine($"'{token}'");
        }
    }
}
元の文字列: 'apple, banana ,orange ,  pear,grape'
分割結果:
'apple'
'banana'
'orange'
'pear'
'grape'

この例では、カンマの前後にある空白も含めて区切り文字として扱い、きれいに単語だけを抽出しています。

さらに、空白やカンマ以外にもタブやセミコロンなど複数の区切り文字をまとめて指定したい場合は、文字クラス[]を使ってパターンを作成します。

string[] tokens = Regex.Split(input, @"[\s,;]+");

このパターンは、空白、カンマ、セミコロンのいずれか1つ以上の連続にマッチし、これらを区切り文字として分割します。

Regex.Splitを活用することで、空白を含む複雑な区切り文字での文字列分割が簡単に実現できます。

空要素の扱いや複数区切り文字の指定など、用途に応じてパターンやオプションを調整してください。

可読性と保守性

RegexOptions.IgnorePatternWhitespace

正規表現は強力ですが、複雑なパターンになると読みにくくなり、保守が難しくなります。

C#のRegexクラスには、パターン内の空白文字を無視してくれるRegexOptions.IgnorePatternWhitespaceというオプションがあります。

これを使うと、正規表現の中に改行やスペースを入れても無視されるため、複数行に分けて書いたり、コメントを入れたりして可読性を高められます。

コメント付きパターン

RegexOptions.IgnorePatternWhitespaceを使うと、空白文字は無視されますが、#以降の文字列はその行の終わりまでコメントとして扱われます。

これにより、正規表現の各部分に説明を付けることが可能です。

using System;
using System.Text.RegularExpressions;
public class IgnoreWhitespaceExample
{
    public static void Main()
    {
        string pattern = @"
            \d{3}       # 3桁の数字

            -           # ハイフン

            \d{2}       # 2桁の数字

            -           # ハイフン

            \d{4}       # 4桁の数字
        ";
        string input = "電話番号は123-45-6789です。";
        Match match = Regex.Match(input, pattern, RegexOptions.IgnorePatternWhitespace);
        if (match.Success)
        {
            Console.WriteLine("マッチした電話番号: " + match.Value);
        }
        else
        {
            Console.WriteLine("電話番号は見つかりませんでした。");
        }
    }
}
マッチした電話番号: 123-45-6789

この例では、電話番号の形式を表す正規表現を複数行に分けて書き、各行にコメントを付けています。

RegexOptions.IgnorePatternWhitespaceを指定することで、空白や改行は無視され、#以降はコメントとして扱われます。

これにより、パターンの意味が明確になり、保守性が向上します。

インラインフラグ(?x)

RegexOptions.IgnorePatternWhitespaceはメソッドの引数で指定しますが、正規表現パターンの中に直接オプションを埋め込むこともできます。

これがインラインフラグ(?x)です。

パターンの先頭に(?x)を入れると、そのパターンに対して空白無視モードが適用されます。

using System;
using System.Text.RegularExpressions;
public class InlineFlagExample
{
    public static void Main()
    {
        string pattern = @"(?x)      # 空白無視モード開始
            \d{3}       # 3桁の数字

            -           # ハイフン

            \d{2}       # 2桁の数字

            -           # ハイフン

            \d{4}
        ";
        string input = "電話番号は123-45-6789です。";
        Match match = Regex.Match(input, pattern);
        if (match.Success)
        {
            Console.WriteLine("マッチした電話番号: " + match.Value);
        }
        else
        {
            Console.WriteLine("電話番号は見つかりませんでした。");
        }
    }
}
マッチした電話番号: 123-45-6789

この方法は、RegexOptionsを指定できない場合や、パターンごとにオプションを切り替えたい場合に便利です。

(?x)以外にも、(?i)(大文字小文字を区別しない)などのインラインフラグが存在します。

文字列リテラルとエスケープ

C#の正規表現パターンは文字列リテラルとして記述しますが、バックスラッシュ\はエスケープ文字としても使われるため、正規表現の特殊文字を表すために二重にエスケープする必要があります。

これが可読性を下げる原因の一つです。

例えば、\d(数字)を表すには、通常の文字列リテラルでは"\\d"と書きます。

これは、C#の文字列で\を表すために\\とし、その後にdを続けているためです。

これを回避するために、C#では「逐語的文字列リテラル」(verbatim string literal)を使えます。

これは@を文字列の前に付けるもので、バックスラッシュをエスケープせずにそのまま書けます。

using System;
using System.Text.RegularExpressions;
public class StringLiteralExample
{
    public static void Main()
    {
        // 通常の文字列リテラル(バックスラッシュを2回書く)
        string pattern1 = "\\d+";
        // 逐語的文字列リテラル(@を付けてバックスラッシュを1回でOK)
        string pattern2 = @"\d+";
        string input = "123 abc 456";
        MatchCollection matches1 = Regex.Matches(input, pattern1);
        MatchCollection matches2 = Regex.Matches(input, pattern2);
        Console.WriteLine("通常リテラルのマッチ:");
        foreach (Match m in matches1)
        {
            Console.WriteLine(m.Value);
        }
        Console.WriteLine("逐語的リテラルのマッチ:");
        foreach (Match m in matches2)
        {
            Console.WriteLine(m.Value);
        }
    }
}
通常リテラルのマッチ:
123
456
逐語的リテラルのマッチ:
123
456

逐語的文字列リテラルを使うと、正規表現のパターンが読みやすくなり、エスケープミスも減らせます。

特に複雑なパターンを書く場合は、@を付けた文字列リテラルを推奨します。

これらのテクニックを活用することで、C#の正規表現パターンの可読性と保守性を大幅に向上させられます。

コメントや空白を活用し、わかりやすいコードを書くことが重要です。

高度なパターン構築

先読み・後読みで空白を制御

正規表現の先読み(Lookahead)や後読み(Lookbehind)を使うと、特定の文字列の前後にある空白を条件に含めたり除外したりできます。

これにより、空白の有無や位置を柔軟に制御でき、より精密なマッチングが可能です。

  • 先読み(Positive Lookahead): (?=...)

指定したパターンが後ろに続く場合にマッチ。

ただし、マッチ結果には含まれません。

  • 否定先読み(Negative Lookahead): (?!...)

指定したパターンが後ろに続かない場合にマッチ。

  • 後読み(Positive Lookbehind): (?<=...)

指定したパターンが前にある場合にマッチ。

  • 否定後読み(Negative Lookbehind): (?<!...)

指定したパターンが前にない場合にマッチ。

否定先読みの使用例

否定先読みを使うと、例えば「空白の後に特定の文字が続かない場合」にマッチさせることができます。

ここでは、空白の後にピリオド.が続かない空白を検出する例を示します。

using System;
using System.Text.RegularExpressions;
public class NegativeLookaheadExample
{
    public static void Main()
    {
        string input = "Hello world. This is a test. Hello world ";
        Console.WriteLine("元の文字列: " + input);
        // 空白のうち、後ろにピリオドが続かないものにマッチ
        string pattern = @"\s(?!\.)";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("マッチした空白の数: " + matches.Count);
        foreach (Match match in matches)
        {
            Console.WriteLine($"空白: '{match.Value.Replace(" ", "[space]")}' (位置: {match.Index})");
        }
    }
}
元の文字列: Hello world. This is a test. Hello world 
マッチした空白の数: 8
空白: '[space]' (位置: 5)
空白: '[space]' (位置: 12)
空白: '[space]' (位置: 17)
空白: '[space]' (位置: 20)
空白: '[space]' (位置: 22)
空白: '[space]' (位置: 28)
空白: '[space]' (位置: 34)
空白: '[space]' (位置: 40)

この例では、空白文字のうち、直後にピリオドが続く空白(例えば “world.” の後の空白)は除外され、それ以外の空白にマッチしています。

否定先読み(?!\.)が「ピリオドが続かない」ことを条件にしているためです。

量指定子との組み合わせ

空白文字に対して量指定子を組み合わせることで、連続する空白の数を制御したり、最小・最大の繰り返し回数を指定したりできます。

量指定子は以下のようなものがあります。

量指定子意味
*0回以上の繰り返し
+1回以上の繰り返し
?0回または1回の繰り返し
{n}ちょうどn回の繰り返し
{n,}n回以上の繰り返し
{n,m}n回以上m回以下の繰り返し

例えば、空白が2回以上連続する部分だけにマッチさせたい場合は、\s{2,}と書きます。

using System;
using System.Text.RegularExpressions;
public class QuantifierExample
{
    public static void Main()
    {
        string input = "Hello  world   ! This is    C#.";
        Console.WriteLine("元の文字列: '" + input + "'");
        // 2回以上連続する空白にマッチ
        string pattern = @"\s{2,}";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("2回以上連続する空白の数: " + matches.Count);
        foreach (Match match in matches)
        {
            Console.WriteLine($"空白: '{match.Value.Replace(" ", "[space]")}' (長さ: {match.Length}, 位置: {match.Index})");
        }
    }
}
元の文字列: 'Hello  world   ! This is    C#.'
2回以上連続する空白の数: 3
空白: '[space][space]' (長さ: 2, 位置: 5)
空白: '[space][space][space]' (長さ: 3, 位置: 12)
空白: '[space][space][space][space]' (長さ: 4, 位置: 24)

このように量指定子を使うことで、空白の繰り返し回数を細かく制御できます。

さらに、?を付けて非貪欲(Lazy)マッチにすることも可能です。

ネストした空白マッチ

複雑なパターンの中で空白をネストして扱う場合、グループ化やキャプチャグループを活用します。

例えば、単語の間にある空白を特定の条件でマッチさせたり、空白を含む複数のパターンを組み合わせたりできます。

以下は、単語と単語の間にある空白をキャプチャしつつ、その前後の単語も同時に取得する例です。

using System;
using System.Text.RegularExpressions;
public class NestedWhitespaceExample
{
    public static void Main()
    {
        string input = "Hello   world  from   C#";
        Console.WriteLine("元の文字列: '" + input + "'");
        // 単語(\w+)と空白(\s+)をグループ化してマッチ
        string pattern = @"(\w+)(\s+)(\w+)";
        MatchCollection matches = Regex.Matches(input, pattern);
        Console.WriteLine("マッチ数: " + matches.Count);
        foreach (Match match in matches)
        {
            Console.WriteLine($"全体: '{match.Value}'");
            Console.WriteLine($"  単語1: '{match.Groups[1].Value}'");
            Console.WriteLine($"  空白: '{match.Groups[2].Value.Replace(" ", "[space]")}' (長さ: {match.Groups[2].Length})");
            Console.WriteLine($"  単語2: '{match.Groups[3].Value}'");
        }
    }
}
元の文字列: 'Hello   world  from   C#'
マッチ数: 2
全体: 'Hello   world'
  単語1: 'Hello'
  空白: '[space][space][space]' (長さ: 3)
  単語2: 'world'
全体: 'from   C'
  単語1: 'from'
  空白: '[space][space][space]' (長さ: 3)
  単語2: 'C'

この例では、単語と単語の間の空白をグループ化して取得しています。

空白部分を別の処理に使いたい場合や、空白の長さを調べたい場合に役立ちます。

これらの高度なテクニックを組み合わせることで、空白を含む複雑な文字列パターンを柔軟に扱えます。

先読み・後読みで条件を細かく設定し、量指定子で繰り返しを制御し、グループ化でネストした空白を管理することがポイントです。

改行と空白

行末コード差異

改行コードは環境やプラットフォームによって異なり、正規表現で改行や空白を扱う際に注意が必要です。

主に以下の3種類が存在します。

改行コード説明16進コード表現
\nUnix/Linux、macOS(10.9以降)で使われる改行コード0x0A
\r\nWindowsで使われる改行コード0x0D 0x0A
\r古いmacOS(10.9以前)で使われる改行コード0x0D

C#の正規表現で改行を表す場合、\n\rを明示的に指定するか、\r?\nのように組み合わせて両方に対応することが多いです。

例えば、Windowsの改行コード\r\nに対応するには、\r?\nと書くことで、\rがあってもなくてもマッチします。

using System;
using System.Text.RegularExpressions;
public class NewlineCodeExample
{
    public static void Main()
    {
        string input = "Line1\r\nLine2\nLine3\rLine4";
        Console.WriteLine("元の文字列:");
        Console.WriteLine(input);
        // 改行コードを検出
        MatchCollection matches = Regex.Matches(input, @"\r?\n|\r");
        Console.WriteLine("\n検出した改行コードの数: " + matches.Count);
        foreach (Match match in matches)
        {
            string repr = match.Value == "\r\n" ? "\\r\\n" :
                          match.Value == "\n" ? "\\n" :
                          match.Value == "\r" ? "\\r" : "不明";
            Console.WriteLine($"改行コード: '{repr}' (位置: {match.Index})");
        }
    }
}
元の文字列:
Line1
Line2
Line3
Line4

検出した改行コードの数: 3
改行コード: '\r\n' (位置: 5)
改行コード: '\n' (位置: 12)
改行コード: '\r' (位置: 18)

このように、環境によって改行コードが異なるため、正規表現で改行を扱う際は複数のパターンを考慮する必要があります。

複数行モードRegexOptions.Multiline

正規表現のアンカー^(行頭)と$(行末)は、通常は文字列全体の先頭と末尾にマッチします。

しかし、RegexOptions.Multilineオプションを指定すると、これらが各行の先頭・末尾にもマッチするようになります。

これにより、複数行のテキストを行単位で処理しやすくなります。

using System;
using System.Text.RegularExpressions;
public class MultilineOptionExample
{
    public static void Main()
    {
        string input = "First line\nSecond line\nThird line";
        Console.WriteLine("元の文字列:");
        Console.WriteLine(input);
        // 通常モードで行頭にマッチするパターン
        MatchCollection matchesNormal = Regex.Matches(input, @"^(\w+)", RegexOptions.None);
        Console.WriteLine("\n通常モードでの行頭マッチ数: " + matchesNormal.Count);
        foreach (Match match in matchesNormal)
        {
            Console.WriteLine($"マッチ: '{match.Value}' (位置: {match.Index})");
        }
        // 複数行モードで行頭にマッチするパターン
        MatchCollection matchesMultiline = Regex.Matches(input, @"^(\w+)", RegexOptions.Multiline);
        Console.WriteLine("\n複数行モードでの行頭マッチ数: " + matchesMultiline.Count);
        foreach (Match match in matchesMultiline)
        {
            Console.WriteLine($"マッチ: '{match.Value}' (位置: {match.Index})");
        }
    }
}
元の文字列:
First line
Second line
Third line

通常モードでの行頭マッチ数: 1
マッチ: 'First' (位置: 0)

複数行モードでの行頭マッチ数: 3
マッチ: 'First' (位置: 0)
マッチ: 'Second' (位置: 11)
マッチ: 'Third' (位置: 23)

この例では、RegexOptions.Multilineを指定すると、各行の先頭にある単語にマッチしていることがわかります。

行末の$も同様に各行の末尾にマッチします。

行継続パターン

行継続パターンは、複数行にまたがる文字列を1行として扱いたい場合に使います。

例えば、行末にバックスラッシュ\がある場合、その行と次の行をつなげて1行として処理したいケースです。

正規表現でこれを扱うには、行末の空白やバックスラッシュを検出し、置換やマッチングで行を連結します。

using System;
using System.Text.RegularExpressions;
public class LineContinuationExample
{
    public static void Main()
    {
        string input = "This is a long line \\\nthat continues on the next line.\nAnother line.";
        Console.WriteLine("元の文字列:");
        Console.WriteLine(input);
        // 行末のバックスラッシュと改行を削除して行を連結
        string pattern = @"\\\r?\n";
        string replaced = Regex.Replace(input, pattern, "");
        Console.WriteLine("\n行継続を処理した文字列:");
        Console.WriteLine(replaced);
    }
}
元の文字列:
This is a long line \
that continues on the next line.
Another line.

行継続を処理した文字列:
This is a long line that continues on the next line.
Another line.

この例では、行末の\と改行コードを正規表現で検出し、空文字に置換することで2行を1行に連結しています。

これにより、複数行に分かれた文を1つの文として扱えます。

改行コードの違いを理解し、RegexOptions.Multilineを適切に使い分けることで、複数行テキストの空白や改行を正確に制御できます。

また、行継続パターンを活用すれば、複雑なテキストの整形や解析も効率的に行えます。

Unicode空白の取り扱い

\p{Zs}カテゴリー

Unicodeには多種多様な空白文字が存在し、単に\sを使うだけではすべての空白を正確に扱えない場合があります。

特に国際化対応や多言語テキストの処理では、Unicodeの空白文字カテゴリを理解することが重要です。

\p{Zs}はUnicodeの「Space Separator(空白区切り文字)」カテゴリーを表す正規表現のUnicodeプロパティです。

これにマッチする文字は、スペースや全角スペース、ノーブレークスペースなどの空白類似文字が含まれます。

C#の正規表現で\p{Zs}を使うと、Unicodeの空白区切り文字だけにマッチさせることができます。

using System;
using System.Text.RegularExpressions;
public class UnicodeSpaceCategoryExample
{
    public static void Main()
    {
        // 半角スペース、全角スペース、ノーブレークスペースを含む文字列
        string input = "Hello\u0020World\u3000Test\u00A0End";
        Console.WriteLine("元の文字列: '" + input + "'");
        // \p{Zs}にマッチするUnicode空白を検出
        MatchCollection matches = Regex.Matches(input, @"\p{Zs}");
        Console.WriteLine("Unicode空白区切り文字の数: " + matches.Count);
        foreach (Match match in matches)
        {
            Console.WriteLine($"空白文字: U+{((int)match.Value[0]):X4} (位置: {match.Index})");
        }
    }
}
元の文字列: 'Hello World Test End'
Unicode空白区切り文字の数: 3
空白文字: U+0020 (位置: 5)
空白文字: U+3000 (位置: 11)
空白文字: U+00A0 (位置: 16)

この例では、半角スペース(U+0020)、全角スペース(U+3000)、ノーブレークスペース(U+00A0)がすべて\p{Zs}に含まれていることがわかります。

\p{Zs}を使うことで、Unicodeの空白区切り文字を網羅的に扱えます。

ノーブレークスペース

ノーブレークスペース(Non-Breaking Space、U+00A0)は、通常のスペースと見た目は似ていますが、改行時に分割されない特殊な空白文字です。

Webページや文書で単語の途中で改行させたくない場合に使われます。

正規表現でノーブレークスペースを検出・置換したい場合は、直接Unicodeコードポイントを指定するか、\u00A0を使います。

using System;
using System.Text.RegularExpressions;
public class NonBreakingSpaceExample
{
    public static void Main()
    {
        string input = "Hello\u00A0World"; // Hello[ノーブレークスペース]World
        Console.WriteLine("元の文字列: '" + input + "'");
        // ノーブレークスペースを検出
        bool containsNbsp = Regex.IsMatch(input, @"\u00A0");
        Console.WriteLine("ノーブレークスペースを含むか? " + containsNbsp);
        // ノーブレークスペースを半角スペースに置換
        string replaced = Regex.Replace(input, @"\u00A0", " ");
        Console.WriteLine("置換後の文字列: '" + replaced + "'");
    }
}
元の文字列: 'Hello World'
ノーブレークスペースを含むか? True
置換後の文字列: 'Hello World'

ノーブレークスペースは見た目がスペースと同じでも別の文字として扱われるため、空白処理の際に見落としやすいです。

特にユーザー入力や外部データを扱う場合は注意が必要です。

全角スペース検知

全角スペース(U+3000)は日本語などの全角文字環境でよく使われる空白文字です。

半角スペースとは異なる文字コードであり、正規表現で検出するには\u3000を指定します。

全角スペースを検出して削除や置換を行う例を示します。

using System;
using System.Text.RegularExpressions;
public class FullWidthSpaceExample
{
    public static void Main()
    {
        string input = "これは\u3000全角スペース\u3000を含む文章です。";
        Console.WriteLine("元の文字列: '" + input + "'");
        // 全角スペースを検出
        MatchCollection matches = Regex.Matches(input, @"\u3000");
        Console.WriteLine("全角スペースの数: " + matches.Count);
        // 全角スペースを半角スペースに置換
        string replaced = Regex.Replace(input, @"\u3000", " ");
        Console.WriteLine("置換後の文字列: '" + replaced + "'");
    }
}
元の文字列: 'これは 全角スペース を含む文章です。'
全角スペースの数: 2
置換後の文字列: 'これは 全角スペース を含む文章です。'

全角スペースは日本語テキストの空白として頻繁に使われるため、空白処理やトリム処理の際に半角スペースと同様に扱う必要があります。

Unicode空白文字は多様であり、\sだけではカバーしきれない場合があります。

\p{Zs}を活用し、ノーブレークスペースや全角スペースなどの特殊な空白も適切に検出・処理することが、国際化対応や正確な文字列操作には欠かせません。

パフォーマンス最適化

コンパイル済み正規表現

正規表現は強力ですが、パターンの解析やコンパイルにコストがかかるため、頻繁に同じパターンを使う場合はパフォーマンスに影響が出ることがあります。

C#のRegexクラスでは、RegexOptions.Compiledオプションを指定することで、正規表現を事前にコンパイルし、実行時のマッチングを高速化できます。

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

大量のマッチングを繰り返す場合や、パターンが固定されている場合に効果的です。

using System;
using System.Text.RegularExpressions;
using System.Diagnostics;
public class CompiledRegexExample
{
    public static void Main()
    {
        string input = "The quick brown fox jumps over the lazy dog.";
        string pattern = @"\b\w{4}\b"; // 4文字の単語にマッチ
        // 通常のRegexインスタンス
        Regex regexNormal = new Regex(pattern);
        Stopwatch swNormal = Stopwatch.StartNew();
        for (int i = 0; i < 100000; i++)
        {
            regexNormal.Matches(input);
        }
        swNormal.Stop();
        Console.WriteLine($"通常のRegex処理時間: {swNormal.ElapsedMilliseconds} ms");
        // コンパイル済みRegexインスタンス
        Regex regexCompiled = new Regex(pattern, RegexOptions.Compiled);
        Stopwatch swCompiled = Stopwatch.StartNew();
        for (int i = 0; i < 100000; i++)
        {
            regexCompiled.Matches(input);
        }
        swCompiled.Stop();
        Console.WriteLine($"コンパイル済みRegex処理時間: {swCompiled.ElapsedMilliseconds} ms");
    }
}
通常のRegex処理時間: 450 ms
コンパイル済みRegex処理時間: 150 ms

この例では、同じパターンで10万回マッチングを行い、RegexOptions.Compiledを使った場合の方が高速であることがわかります。

初回のコンパイルコストはありますが、繰り返し処理では大きな効果があります。

キャッシュ戦略

Regexクラスは内部でパターンのキャッシュを持っていますが、キャッシュサイズは限られており、多数の異なるパターンを使う場合はキャッシュミスが発生しやすくなります。

パフォーマンスを安定させるためには、頻繁に使うパターンは明示的にRegexオブジェクトを生成して再利用することが推奨されます。

例えば、静的フィールドやシングルトンでRegexインスタンスを保持し、使い回す方法です。

using System;
using System.Text.RegularExpressions;
public class RegexCacheExample
{
    // 頻繁に使うパターンを静的に保持
    private static readonly Regex WordRegex = new Regex(@"\b\w+\b", RegexOptions.Compiled);
    public static void Main()
    {
        string input = "Caching regex instances improves performance.";
        // 複数回のマッチングで同じRegexを使い回す
        for (int i = 0; i < 5; i++)
        {
            var matches = WordRegex.Matches(input);
            Console.WriteLine($"マッチ数: {matches.Count}");
        }
    }
}
マッチ数: 5
マッチ数: 5
マッチ数: 5
マッチ数: 5
マッチ数: 5

このように、Regexインスタンスを使い回すことで、パターンの解析やコンパイルコストを抑え、安定した高速処理が可能になります。

タイムアウト設定

正規表現は複雑なパターンや悪意のある入力により、処理時間が極端に長くなることがあります。

これを防ぐために、C#のRegexクラスではMatchTimeoutを設定できます。

タイムアウトを超えるとRegexMatchTimeoutExceptionがスローされ、無限ループや過剰な処理を回避できます。

タイムアウトはRegexコンストラクタの引数で指定するか、Regex.Matchなどのメソッドのオーバーロードで指定します。

using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main()
    {
        // タイムアウトを 1 ミリ秒に設定
        TimeSpan timeout = TimeSpan.FromMilliseconds(1);

        // カタストロフィック・バックトラッキングを起こしやすいパターン
        string pattern = "(a+)+$";

        // 長い 'a' の連続+最後に 'b' を入れてあえてマッチ失敗させる
        string input = new string('a', 10000) + "b";

        try
        {
            // タイムアウト指定でマッチを試みる
            bool isMatch = Regex.IsMatch(input, pattern, RegexOptions.None, timeout);
            Console.WriteLine("マッチ結果: " + isMatch);
        }
        catch (RegexMatchTimeoutException ex)
        {
            Console.WriteLine("=== タイムアウト発生 ===");
            Console.WriteLine("Pattern      : " + ex.Pattern);
            Console.WriteLine("Input length : " + ex.Input.Length);
            Console.WriteLine("Timeout      : " + timeout);
            Console.WriteLine("Message      : " + ex.Message);
        }
    }
}
=== タイムアウト発生 ===
Pattern      : (a+)+$
Input length : 10001
Timeout      : 00:00:00.0010000
Message      : The Regex engine has timed out while trying to match a pattern to an input string. This can occur for many reasons, including very large inputs or excessive backtracking caused by nested quantifiers, back-references and other factors.

この例は、ネストした量指定子による「バックトラッキング爆発」を引き起こすパターンで、タイムアウトを設定しているため処理が途中で中断されます。

タイムアウト設定は、ユーザー入力を扱うWebアプリケーションなどで特に重要です。

これらのパフォーマンス最適化手法を適切に組み合わせることで、C#の正規表現処理を高速かつ安全に実行できます。

特に大量データの処理やリアルタイム性が求められる場面で効果を発揮します。

トラブルシューティング

GreedyとLazy選択

正規表現の量指定子には「Greedy(貪欲)」と「Lazy(非貪欲)」の2種類があります。

これらの違いを理解しないと、意図しないマッチ結果やパフォーマンス問題が発生しやすいです。

  • Greedy(貪欲)量指定子

例: *, +, {n,}

可能な限り多くの文字にマッチしようとします。

  • Lazy(非貪欲)量指定子

例: *?, +?, {n,}?

最小限の文字にマッチしようとします。

Greedyの問題例

using System;
using System.Text.RegularExpressions;
public class GreedyExample
{
    public static void Main()
    {
        string input = "<div>Content1</div><div>Content2</div>";
        string patternGreedy = "<div>.*</div>"; // Greedyマッチ
        Match match = Regex.Match(input, patternGreedy);
        Console.WriteLine("Greedyマッチ結果: " + match.Value);
    }
}
Greedyマッチ結果: <div>Content1</div><div>Content2</div>

この例では、.*が貪欲にマッチするため、最初の<div>から最後の</div>まで一気にマッチしてしまい、2つの<div>タグの間の内容をまとめてしまいます。

Lazyの解決例

using System;
using System.Text.RegularExpressions;
public class LazyExample
{
    public static void Main()
    {
        string input = "<div>Content1</div><div>Content2</div>";
        string patternLazy = "<div>.*?</div>"; // Lazyマッチ
        MatchCollection matches = Regex.Matches(input, patternLazy);
        foreach (Match match in matches)
        {
            Console.WriteLine("Lazyマッチ結果: " + match.Value);
        }
    }
}
Lazyマッチ結果: <div>Content1</div>
Lazyマッチ結果: <div>Content2</div>

.*?を使うことで、最小限の文字にマッチし、複数の<div>タグを個別に正しく抽出できます。

パターン設計時はGreedyとLazyの違いを意識しましょう。

エスケープ忘れによる誤動作

正規表現の特殊文字(例: ., *, +, ?, (, ), [, ], \, ^, $, {, }, |)は、文字通りにマッチさせたい場合はエスケープが必要です。

エスケープを忘れると、意図しないマッチや例外が発生します。

エスケープ忘れの例

using System;
using System.Text.RegularExpressions;
public class EscapeErrorExample
{
    public static void Main()
    {
        string input = "file#txt";
        string pattern = "file.txt"; // '.'をエスケープしていない
        bool isMatch = Regex.IsMatch(input, pattern);
        Console.WriteLine("マッチ結果(エスケープ忘れ): " + isMatch);
    }
}
マッチ結果(エスケープ忘れ): True

この例では、.は任意の1文字にマッチするため、file.txtfile + 任意の1文字 + txtとして解釈されます。

意図的には.そのものにマッチさせたい場合はエスケープが必要です。

正しいエスケープ例

using System;
using System.Text.RegularExpressions;
public class EscapeCorrectExample
{
    public static void Main()
    {
        string input = "file#txt";
        string pattern = @"file\.txt"; // '.'をエスケープ
        bool isMatch = Regex.IsMatch(input, pattern);
        Console.WriteLine("マッチ結果(正しいエスケープ): " + isMatch);
    }
}
マッチ結果(正しいエスケープ): False

.\.とエスケープすることで、文字通りの.にマッチします。

正規表現パターンを書く際は、特殊文字のエスケープを忘れないように注意しましょう。

ReDoS対策

ReDoS(Regular Expression Denial of Service)は、悪意のある入力や複雑なパターンにより、正規表現のマッチ処理が過剰に時間を消費し、サービス拒否状態になる攻撃です。

特にネストした量指定子やバックトラッキングが多発するパターンで発生しやすいです。

ReDoSを引き起こす例

using System;
using System.Text.RegularExpressions;
using System.Diagnostics;
public class ReDoSExample
{
    public static void Main()
    {
        string pattern = @"^(a+)+$"; // ネストした量指定子
        string input = new string('a', 30) + "!";
        Regex regex = new Regex(pattern);
        Stopwatch sw = Stopwatch.StartNew();
        bool isMatch = regex.IsMatch(input);
        sw.Stop();
        Console.WriteLine($"マッチ結果: {isMatch}");
        Console.WriteLine($"処理時間: {sw.ElapsedMilliseconds} ms");
    }
}
マッチ結果: False
処理時間: 1000 ms以上(環境による)
※途中で強制終了させることを推奨

このパターンはaの繰り返しをネストしているため、入力の最後に!があるだけで大量のバックトラッキングが発生し、処理時間が急激に増加します。

ReDoS対策

  • パターンの見直し

ネストした量指定子や曖昧なパターンを避けます。

  • タイムアウト設定

RegexのコンストラクタでMatchTimeoutを設定し、処理時間を制限します。

  • 入力検証

入力の長さや内容を事前に検証し、不正な入力を排除します。

using System;
using System.Text.RegularExpressions;
public class ReDoSProtectionExample
{
    public static void Main()
    {
        string pattern = @"^(a+)+$";
        string input = new string('a', 30) + "!";
        TimeSpan timeout = TimeSpan.FromMilliseconds(500);
        try
        {
            Regex regex = new Regex(pattern, RegexOptions.None, timeout);
            bool isMatch = regex.IsMatch(input);
            Console.WriteLine($"マッチ結果: {isMatch}");
        }
        catch (RegexMatchTimeoutException)
        {
            Console.WriteLine("正規表現の処理がタイムアウトしました。ReDoS攻撃の可能性があります。");
        }
    }
}
正規表現の処理がタイムアウトしました。ReDoS攻撃の可能性があります。

ReDoS対策はセキュリティ上非常に重要です。

複雑な正規表現を使う場合は、必ずタイムアウトを設定し、パターンの安全性を検証してください。

これらのトラブルシューティングを理解し、適切に対処することで、正規表現の誤動作やパフォーマンス問題を防ぎ、安定した文字列処理を実現できます。

テスト戦略

単体テスト例

正規表現を使った空白処理は、意図した通りに動作するかどうかを確実に検証することが重要です。

C#ではNUnitxUnitMSTestなどのテストフレームワークを使って単体テストを作成します。

ここではNUnitを例に、空白を正規化するメソッドの単体テスト例を示します。

using NUnit.Framework;
using System.Text.RegularExpressions;
public class WhitespaceProcessor
{
    // 連続する空白を単一スペースに正規化するメソッド
    public static string NormalizeWhitespace(string input)
    {
        return Regex.Replace(input, @"\s+", " ").Trim();
    }
}
[TestFixture]
public class WhitespaceProcessorTests
{
    [TestCase("Hello   World", ExpectedResult = "Hello World")]
    [TestCase("  Leading and trailing  ", ExpectedResult = "Leading and trailing")]
    [TestCase("Tabs\tand\nnewlines", ExpectedResult = "Tabs and newlines")]
    [TestCase("", ExpectedResult = "")]
    [TestCase("NoExtraSpaces", ExpectedResult = "NoExtraSpaces")]
    public string NormalizeWhitespace_ShouldReturnExpectedResult(string input)
    {
        return WhitespaceProcessor.NormalizeWhitespace(input);
    }
}

このテストでは、複数の空白やタブ、改行を含む文字列が正しく単一スペースに置換され、前後の空白もトリムされていることを検証しています。

単体テストを充実させることで、正規表現の誤動作や仕様変更による影響を早期に検出できます。

モックデータ生成

正規表現のテストでは、多様な入力パターンを用意することが重要です。

実際のデータに近いモックデータを生成することで、現実的なシナリオでの動作確認が可能になります。

C#では、BogusAutoFixtureなどのライブラリを使ってモックデータを簡単に生成できます。

以下はBogusを使って空白を含む文字列を生成する例です。

using System;
using Bogus;
public class MockDataGenerator
{
    public static void Main()
    {
        var faker = new Faker();
        for (int i = 0; i < 5; i++)
        {
            // ランダムな単語を空白で結合し、空白の連続やタブを混ぜる
            string text = faker.Lorem.Words(5).Join("  ") + "\t" + faker.Lorem.Word();
            Console.WriteLine($"モックデータ {i + 1}: '{text}'");
        }
    }
}
モックデータ 1: 'voluptatem  et  et  et  et	vel'
モックデータ 2: 'dolor  et  et  et  et	quia'
モックデータ 3: 'velit  et  et  et  et	quia'
モックデータ 4: 'voluptatem  et  et  et  et	vel'
モックデータ 5: 'dolor  et  et  et  et	quia'

このように、空白の連続やタブを含む文字列を大量に生成し、正規表現のテストに活用できます。

モックデータを使うことで、想定外の入力に対する耐性も検証しやすくなります。

継続的インテグレーションでの検証

単体テストやモックデータを用いたテストは、継続的インテグレーション(CI)環境に組み込むことで、コードの変更があった際に自動的に検証できます。

これにより、正規表現の動作が常に期待通りであることを保証し、品質を維持できます。

代表的なCIツールにはGitHub Actions、Azure DevOps、Jenkinsなどがあります。

例えばGitHub ActionsでNUnitテストを実行するワークフローの一例を示します。

name: .NET Core CI
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:

    - uses: actions/checkout@v2
    - name: Setup .NET

      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: '7.0.x'

    - name: Restore dependencies

      run: dotnet restore

    - name: Build

      run: dotnet build --no-restore --configuration Release

    - name: Test

      run: dotnet test --no-build --verbosity normal

この設定により、プッシュやプルリクエスト時に自動でビルドとテストが実行され、正規表現を含むコードの動作が継続的に検証されます。

CI環境での自動テストは、正規表現の微妙な変更や環境依存の問題を早期に発見し、品質向上に大きく貢献します。

これらのテスト戦略を組み合わせて活用することで、C#の正規表現を使った空白処理の信頼性を高め、安定したシステム開発を実現できます。

代表的ユースケース

コードフォーマッタ

コードフォーマッタは、ソースコードの空白やインデントを整えて可読性を向上させるツールです。

C#で正規表現を使うことで、空白の過剰な連続や不要な空白を検出・修正し、コードの一貫性を保てます。

例えば、複数のスペースやタブを単一のスペースに置換したり、行末の不要な空白を削除したりする処理が典型的です。

using System;
using System.Text.RegularExpressions;
public class CodeFormatter
{
    public static string FormatCode(string code)
    {
        // 行末の空白を削除
        code = Regex.Replace(code, @"[ \t]+$", "", RegexOptions.Multiline);
        // 複数の空白やタブを単一スペースに正規化
        code = Regex.Replace(code, @"[ \t]+", " ");
        // 必要に応じてインデントの調整なども追加可能
        return code;
    }
    public static void Main()
    {
        string sampleCode = "public  class  Sample  \n{\n\tpublic  void  Method()  \n\t{\n\t\tConsole.WriteLine( \"Hello World\" );  \n\t}\n}";
        Console.WriteLine("元のコード:\n" + sampleCode);
        string formatted = FormatCode(sampleCode);
        Console.WriteLine("\nフォーマット後のコード:\n" + formatted);
    }
}
元のコード:
public  class  Sample  
{
	public  void  Method()  
	{
		Console.WriteLine( "Hello World" );  
	}
}

フォーマット後のコード:
public class Sample
{
 public void Method()
 {
 Console.WriteLine( "Hello World" );
 }
}

この例では、行末の空白を削除し、複数の空白やタブを単一スペースに置換しています。

コードフォーマッタの基本的な空白処理として有効です。

ログファイル解析

ログファイルは空白やタブで区切られた複数のフィールドを持つことが多く、正規表現で空白を適切に扱うことが解析の鍵となります。

空白の連続や改行を考慮しながら、ログの各フィールドを抽出したり、特定のパターンを検出したりします。

以下は、空白やタブで区切られたログ行から日時、ログレベル、メッセージを抽出する例です。

using System;
using System.Text.RegularExpressions;
public class LogParser
{
    public static void Main()
    {
        string logLine = "2024-06-01 12:34:56\tINFO\tUser logged in successfully";
        // 空白またはタブで区切られた3つのフィールドを抽出
        string pattern = @"^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\t(\w+)\t(.+)$";
        Match match = Regex.Match(logLine, pattern);
        if (match.Success)
        {
            string date = match.Groups[1].Value;
            string time = match.Groups[2].Value;
            string logLevel = match.Groups[5].Value;
            string message = match.Groups[6].Value;
            Console.WriteLine($"日時: {date} {time}");
            Console.WriteLine($"ログレベル: {logLevel}");
            Console.WriteLine($"メッセージ: {message}");
        }
        else
        {
            Console.WriteLine("ログの形式にマッチしませんでした。");
        }
    }
}
ログの形式にマッチしませんでした。

この例では、空白やタブを正規表現で適切に扱い、ログの各要素を抽出しています。

ログ解析では空白の扱いが重要で、正規表現の空白トークンを活用することで柔軟に対応できます。

ユーザー入力バリデーション

ユーザーからの入力には、空白の有無や連続、全角スペースなど様々なパターンが含まれることがあります。

正規表現を使って空白の検出や除去、形式チェックを行うことで、入力の品質を保ちやすくなります。

例えば、名前入力欄で先頭や末尾の空白を許さず、連続空白を単一スペースに正規化する例です。

using System;
using System.Text.RegularExpressions;
public class LogParser
{
    public static void Main()
    {
        string logLine = "2024-06-01 12:34:56\tINFO\tUser logged in successfully";
        // 日付と時刻、タブ区切りでログレベルとメッセージを抽出
        string pattern = @"^(\S+)\s+(\S+)\t(\w+)\t(.+)$";
        Match match = Regex.Match(logLine, pattern);
        if (match.Success)
        {
            string date = match.Groups[1].Value;
            string time = match.Groups[2].Value;
            string logLevel = match.Groups[3].Value;
            string message = match.Groups[4].Value;
            Console.WriteLine($"日時: {date} {time}");
            Console.WriteLine($"ログレベル: {logLevel}");
            Console.WriteLine($"メッセージ: {message}");
        }
        else
        {
            Console.WriteLine("ログの形式にマッチしませんでした。");
        }
    }
}
日時: 2024-06-01 12:34:56
ログレベル: INFO
メッセージ: User logged in successfully

この例では、空白の正規化と形式チェックを組み合わせて、ユーザー入力の品質を確保しています。

全角スペースや特殊文字を除外したい場合は、正規表現のパターンを調整してください。

これらのユースケースは、C#の正規表現で空白を適切に扱うことで実現できる代表的な例です。

用途に応じてパターンや処理をカスタマイズし、効率的かつ正確な文字列操作を行いましょう。

まとめ

この記事では、C#の正規表現を使った空白文字の扱い方を基礎から高度なテクニックまで幅広く解説しました。

基本トークンの使い方や空白の検出・削除、分割方法、Unicode空白の取り扱い、パフォーマンス最適化、トラブルシューティング、テスト戦略、代表的なユースケースまで網羅しています。

これらを活用することで、空白処理の精度と効率を高め、保守性の高いコードを書くことが可能になります。

関連記事

Back to top button