C#でワイルドカード検索を実装する方法|*・?の使い方と正規表現への変換を解説
はじめに
C#で検索機能を作るとき、「test*に一致する文字列を探したい」「*.txtのようにファイル名を検索したい」「ユーザーが入力したワイルドカード条件で絞り込みたい」といった場面があります。
このような検索でよく使われるのが、ワイルドカードです。
ワイルドカード検索では、主に次の記号を使って柔軟な一致判定を行います。
* 任意の0文字以上に一致
? 任意の1文字に一致
たとえば、abc*ならabc、abcd、abc123などに一致します。file?.txtならfile1.txtやfileA.txtには一致しますが、file10.txtには一致しません。
ただし、C#の文字列には「ワイルドカード一致」をそのまま行う標準メソッドが用意されているわけではありません。そのため、実装方法としては正規表現に変換して判定する方法がよく使われます。
この記事では、C#でワイルドカード検索を実装する方法について、*と?の意味、正規表現への変換、文字列検索、List検索、ファイル検索、SQL検索との違いまで解説します。
1. C#のワイルドカード検索とは
C#におけるワイルドカード検索とは、検索条件の一部に特殊な記号を使い、完全に固定された文字列ではなく、ある程度あいまいなパターンで一致判定を行う検索方法です。
たとえば、次のような検索条件を考えてみます。
*.txt
user?
test*
*error*
これらは通常の文字列比較ではありません。*や?を特別な意味として扱い、対象文字列がそのパターンに合っているかを判定します。
C#では、文字列の一致判定にEquals、Contains、StartsWith、EndsWith、Regexなどを使えますが、*や?をワイルドカードとして解釈するには、自分で変換処理を実装する必要があります。
1-1. ワイルドカード検索でできること
ワイルドカード検索を使うと、完全一致だけでは対応しにくい柔軟な検索ができます。
たとえば、次のような条件を表現できます。
abc* abcで始まる文字列
*.csv .csvで終わる文字列
*error* errorを含む文字列
file?.txt file1.txtやfileA.txtなど
具体例を見てみましょう。
検索パターン: user*
一致する文字列:
user
user1
user_name
user@example.com
一致しない文字列:
admin_user
testuser
user*は「userで始まり、その後ろに任意の文字列が続いてもよい」という意味です。
また、次のようなパターンもあります。
検索パターン: data?.csv
一致する文字列:
data1.csv
dataA.csv
一致しない文字列:
data10.csv
data.csv
?は任意の1文字を表すため、data?.csvはdataと.csvの間にちょうど1文字ある場合だけ一致します。
1-2. 「*」と「?」の意味と違い
ワイルドカード検索でよく使われる記号は、*と?です。
*は、任意の0文字以上に一致します。
abc*
この場合、次のような文字列に一致します。
abc
abcd
abc123
abc_test
*は0文字にも一致するため、abc*はabcそのものにも一致します。
一方、?は任意の1文字に一致します。
abc?
この場合、次のような文字列に一致します。
abcd
abc1
abcA
しかし、次の文字列には一致しません。
abc
abc12
?は必ず1文字分を表すため、文字が足りない場合や多すぎる場合は一致しません。
違いをまとめると、次のようになります。
| 記号 | 意味 | 一致する文字数 |
|---|---|---|
* | 任意の文字列 | 0文字以上 |
? | 任意の1文字 | ちょうど1文字 |
C#でワイルドカード検索を実装する場合、この*と?を正規表現の記法に変換して扱うのが一般的です。
1-3. Contains・StartsWith・EndsWith・Regexとの違い
C#には文字列検索に使えるメソッドがいくつかあります。
代表的なものは次のとおりです。
C#Contains()
StartsWith()
EndsWith()
Equals()
Regex.IsMatch()
それぞれの違いを確認しておきましょう。
Containsは、指定した文字列を含むかどうかを判定します。
C#var text = "sample.txt";
bool result = text.Contains("sample");
この場合、sample.txtにsampleが含まれているためtrueになります。
StartsWithは、指定した文字列で始まるかどうかを判定します。
C#var text = "sample.txt";
bool result = text.StartsWith("sample");
EndsWithは、指定した文字列で終わるかどうかを判定します。
C#var text = "sample.txt";
bool result = text.EndsWith(".txt");
これらはシンプルで使いやすいですが、*や?をワイルドカードとして解釈する機能はありません。
たとえば、次のコードは期待通りには動きません。
C#var text = "sample.txt";
bool result = text.Contains("*.txt");
この場合、*.txtという文字列そのものを含むかを判定しているだけです。.txtで終わるかどうかを調べているわけではありません。
ワイルドカード検索を行いたい場合は、*.txtを正規表現に変換してからRegex.IsMatchで判定する方法が便利です。
C#using System.Text.RegularExpressions;
var text = "sample.txt";
var pattern = @"^.*\.txt$";
bool result = Regex.IsMatch(text, pattern);
このように、ワイルドカード検索は通常の文字列検索よりも柔軟ですが、その分、変換処理やエスケープ処理に注意が必要です。
1-4. ワイルドカード検索が使われる主な場面
C#でワイルドカード検索が使われる場面は多くあります。
代表的な例は、ファイル名検索です。
*.txt
*.csv
log_*.txt
backup_2024_??.zip
Directory.GetFilesを使えば、*.txtのようなパターンでファイルを検索できます。
また、アプリケーション内の検索条件として使うこともあります。
商品コード: A*
ユーザー名: admin?
ログ内容: *error*
ユーザーに検索パターンを入力してもらい、それに一致するデータだけを表示するようなケースです。
他にも、次のような場面でワイルドカード検索が使われます。
| 場面 | 例 |
|---|---|
| ファイル検索 | *.logに一致するログファイルを探す |
| 文字列検索 | user*に一致するユーザー名を抽出する |
| 設定ファイル | 除外条件としてtemp*を指定する |
| 入力補助 | 一部だけ分かっている名前を検索する |
| データ抽出 | 複数のパターンに一致するデータを取り出す |
ただし、ワイルドカード検索の実装方法は、文字列検索、ファイル検索、SQL検索で異なります。同じ*や?に見えても、内部での扱いが変わるため注意が必要です。
2. C#でワイルドカード検索を実装する基本方針
C#でワイルドカード検索を実装する場合、まず「何を検索したいのか」を整理する必要があります。
主な対象は次の3つです。
文字列を検索する
ファイル名を検索する
データベースを検索する
文字列を検索する場合は、ワイルドカードパターンを正規表現に変換してRegex.IsMatchで判定する方法がよく使われます。
ファイル名を検索する場合は、Directory.GetFilesやDirectory.EnumerateFilesで*.txtのような検索パターンをそのまま使える場合があります。
SQL検索の場合は、C#の*や?ではなく、SQLの%や_に変換してLIKE検索を行う必要があります。
このように、同じワイルドカード検索でも、対象によって実装方針が変わります。
2-1. C#に標準のワイルドカード一致メソッドはある?
C#のstringクラスには、*や?をワイルドカードとして解釈して一致判定する標準メソッドはありません。
たとえば、次のようなメソッドは標準では用意されていません。
C#text.WildcardMatch("*.txt");
そのため、文字列に対してワイルドカード検索を行いたい場合は、主に次のいずれかの方法を使います。
正規表現に変換してRegexで判定する
自分で文字単位のマッチング処理を書く
外部ライブラリを使う
多くの場合、最も実装しやすいのは正規表現に変換する方法です。
*を.*に変換し、?を.に変換すれば、ワイルドカードに近い動きを正規表現で表現できます。
ただし、単純に文字列置換するだけでは不十分です。ドット.、括弧()、角括弧[]など、正規表現で特別な意味を持つ文字を安全に扱うために、Regex.Escapeを使う必要があります。
2-2. 正規表現に変換して判定する方法
C#でワイルドカード検索を実装する基本的な流れは、次のとおりです。
1. ユーザーが入力したワイルドカードパターンを受け取る
2. Regex.Escapeで正規表現の特殊文字をエスケープする
3. \*を.*に変換する
4. \?を.に変換する
5. 完全一致にする場合は^と$を付ける
6. Regex.IsMatchで判定する
たとえば、*.txtを正規表現に変換すると、次のようになります。
ワイルドカード: *.txt
正規表現: ^.*\.txt$
.txtの.は、正規表現では「任意の1文字」という意味を持ちます。そのため、ファイル拡張子のドットとして扱うには\.にエスケープする必要があります。
このエスケープを手作業で行うと漏れやすいため、C#ではRegex.Escapeを使うのが安全です。
C#using System.Text.RegularExpressions;
static bool IsWildcardMatch(string input, string pattern)
{
var regexPattern = "^" + Regex.Escape(pattern)
.Replace(@"\*", ".*")
.Replace(@"\?", ".") + "$";
return Regex.IsMatch(input, regexPattern);
}
この関数を使うと、次のように判定できます。
C#Console.WriteLine(IsWildcardMatch("sample.txt", "*.txt")); // True
Console.WriteLine(IsWildcardMatch("sample.csv", "*.txt")); // False
Console.WriteLine(IsWildcardMatch("file1.txt", "file?.txt")); // True
2-3. ファイル検索・文字列検索・SQL検索で実装方法が変わる理由
ワイルドカード検索は、検索対象によって使う記号や処理方法が異なります。
文字列検索では、*と?を正規表現に変換する方法がよく使われます。
* → .*
? → .
ファイル検索では、Directory.GetFilesなどのメソッドに*.txtのような検索パターンを渡せます。
C#var files = Directory.GetFiles(@"C:\Logs", "*.txt");
SQL検索では、一般的にLIKEで使うワイルドカードが異なります。
% 任意の0文字以上
_ 任意の1文字
つまり、C#側でユーザーに*や?を入力させる場合、SQLに渡す前に次のように変換する必要があります。
* → %
? → _
同じ「ワイルドカード検索」という言葉でも、内部のルールが違うため、対象ごとに適切な実装を選ぶことが重要です。
3. 「*」と「?」を正規表現に変換する方法
C#で文字列に対するワイルドカード検索を実装する場合、基本になるのは正規表現への変換です。
ただし、ここで注意したいのは、ワイルドカードと正規表現は別物だという点です。
ワイルドカードでは*は「任意の文字列」を意味しますが、正規表現の*は「直前の文字の0回以上の繰り返し」を意味します。
たとえば、正規表現でa*と書くと、「aが0回以上繰り返される」という意味です。ワイルドカードのa*のように「aで始まる任意の文字列」という意味ではありません。
そのため、C#でワイルドカード検索を正規表現で実現するには、ワイルドカードの意味に合わせて変換する必要があります。
3-1. 「」を「.」に変換する
ワイルドカードの*は、任意の0文字以上を表します。
正規表現で任意の0文字以上を表すには、.*を使います。
ワイルドカード: abc*
正規表現: abc.*
abc*というワイルドカードは、次のような文字列に一致します。
abc
abcd
abc123
abc_test
C#では、Regex.Escapeを使った後に、エスケープされた\*を.*に置き換えるのが安全です。
C#var pattern = "abc*";
var regexPattern = Regex.Escape(pattern)
.Replace(@"\*", ".*");
ここで重要なのは、Regex.Escapeの後に置換することです。
C#Regex.Escape("abc*")
この結果は、概念的には次のようになります。
abc\*
そのため、置換対象は*ではなく\*になります。
C#.Replace(@"\*", ".*")
この順番を間違えると、正規表現の特殊文字が適切にエスケープされず、予期しない検索結果になる可能性があります。
3-2. 「?」を「.」に変換する
ワイルドカードの?は、任意の1文字を表します。
正規表現で任意の1文字を表すには、.を使います。
ワイルドカード: file?.txt
正規表現: file.\.txt
このパターンは、次のような文字列に一致します。
file1.txt
fileA.txt
file_.txt
一方で、次の文字列には一致しません。
file.txt
file10.txt
C#では、*と同じようにRegex.Escapeの後で\?を.に変換します。
C#var pattern = "file?.txt";
var regexPattern = Regex.Escape(pattern)
.Replace(@"\*", ".*")
.Replace(@"\?", ".");
?も正規表現では特別な意味を持つ記号です。そのため、ユーザー入力をそのまま正規表現として扱うのではなく、一度Regex.Escapeで安全な文字列にしてから、ワイルドカード部分だけを正規表現に戻すのが基本です。
3-3. 完全一致にするために「^」と「$」を付ける
ワイルドカード検索では、部分一致にするのか完全一致にするのかを明確にする必要があります。
たとえば、次のパターンを考えます。
abc*
これを正規表現に変換すると、次のようになります。
abc.*
このままRegex.IsMatchで判定すると、文字列の途中にabcで始まる部分があれば一致する可能性があります。
たとえば、次のような文字列です。
xxxabc123
完全一致として扱いたい場合は、正規表現の先頭に^、末尾に$を付けます。
^abc.*$
^は文字列の先頭、$は文字列の末尾を表します。
C#では次のように書きます。
C#var regexPattern = "^" + Regex.Escape(pattern)
.Replace(@"\*", ".*")
.Replace(@"\?", ".") + "$";
これにより、対象文字列全体がワイルドカードパターンに一致するかを判定できます。
C#Console.WriteLine(IsWildcardMatch("abc123", "abc*")); // True
Console.WriteLine(IsWildcardMatch("xxxabc123", "abc*")); // False
部分一致にしたい場合は、^と$を付けない、またはユーザー側に*abc*のように入力してもらう設計にするとよいでしょう。
3-4. Regex.Escapeで特殊文字を安全に扱う
正規表現には、特別な意味を持つ記号が多数あります。
代表的なものは次のとおりです。
.
+
*
?
^
$
(
)
[
]
{
}
|
\
たとえば、ファイル名の.txtに含まれる.は、正規表現では任意の1文字を表します。
そのため、次のような正規表現は意図しない一致を起こします。
.*.txt
この場合、.txtのドットが「任意の1文字」として扱われてしまいます。
ファイル拡張子のドットとして扱うには、次のようにエスケープする必要があります。
.*\.txt
この処理を手作業で行うのは危険です。ユーザーが入力した文字列に括弧や角括弧が含まれていた場合、正規表現の構文として解釈されてしまう可能性があります。
そこで、Regex.Escapeを使います。
C#var escaped = Regex.Escape("file(1).txt");
これにより、正規表現で特別な意味を持つ文字が安全にエスケープされます。
ワイルドカード検索では、次の順番が重要です。
1. Regex.Escapeで全体をエスケープする
2. \*を.*に戻す
3. \?を.に戻す
実装例は次のとおりです。
C#static string WildcardToRegexPattern(string pattern)
{
return "^" + Regex.Escape(pattern)
.Replace(@"\*", ".*")
.Replace(@"\?", ".") + "$";
}
このようにすれば、file(1).txtのような文字列も安全に検索できます。
3-5. 大文字・小文字を区別しない検索にする方法
C#のRegex.IsMatchでは、RegexOptions.IgnoreCaseを指定することで、大文字・小文字を区別しない検索ができます。
C#using System.Text.RegularExpressions;
static bool IsWildcardMatchIgnoreCase(string input, string pattern)
{
var regexPattern = "^" + Regex.Escape(pattern)
.Replace(@"\*", ".*")
.Replace(@"\?", ".") + "$";
return Regex.IsMatch(input, regexPattern, RegexOptions.IgnoreCase);
}
使用例は次のとおりです。
C#Console.WriteLine(IsWildcardMatchIgnoreCase("Sample.TXT", "*.txt")); // True
Console.WriteLine(IsWildcardMatchIgnoreCase("README.md", "readme.*")); // True
ただし、カルチャに依存しない比較を重視する場合は、RegexOptions.CultureInvariantも検討できます。
C#RegexOptions options = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
たとえば、ファイル名、ID、コード、英数字のキーなどを検索する場合は、カルチャ依存の大文字小文字変換よりも、カルチャに依存しない比較のほうが適していることがあります。
C#return Regex.IsMatch(input, regexPattern,
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
日本語の検索では大文字・小文字よりも、全角・半角、ひらがな・カタカナ、濁点の違いなどが問題になることがあります。必要に応じて、検索前に文字列を正規化する設計も検討しましょう。
4. C#でワイルドカード検索を実装するサンプルコード
ここからは、C#で実際にワイルドカード検索を実装するサンプルコードを紹介します。
基本的には、次の処理を共通化しておくと使いやすくなります。
ワイルドカードパターンを正規表現に変換する
Regex.IsMatchで一致判定する
必要に応じて大文字・小文字を無視する
まずは、文字列がワイルドカードパターンに一致するかを判定する基本コードから見ていきます。
4-1. 文字列がワイルドカードパターンに一致するか判定する
最も基本的な実装例は次のとおりです。
C#using System;
using System.Text.RegularExpressions;
class Program
{
static void Main()
{
Console.WriteLine(IsWildcardMatch("sample.txt", "*.txt")); // True
Console.WriteLine(IsWildcardMatch("sample.csv", "*.txt")); // False
Console.WriteLine(IsWildcardMatch("file1.txt", "file?.txt")); // True
Console.WriteLine(IsWildcardMatch("file10.txt", "file?.txt")); // False
}
static bool IsWildcardMatch(string input, string pattern)
{
if (input == null || pattern == null)
{
return false;
}
var regexPattern = "^" + Regex.Escape(pattern)
.Replace(@"\*", ".*")
.Replace(@"\?", ".") + "$";
return Regex.IsMatch(input, regexPattern);
}
}
この実装では、*.txtのようなワイルドカードパターンを正規表現に変換してから判定しています。
変換イメージは次のとおりです。
*.txt → ^.*\.txt$
file?.txt → ^file.\.txt$
test* → ^test.*$
Regex.Escapeを使っているため、ドットや括弧などの記号を含む文字列でも比較的安全に扱えます。
4-2. Listや配列から一致する文字列を抽出する
ワイルドカード検索は、Listや配列から条件に一致する要素を抽出したい場合にも使えます。
たとえば、ファイル名の一覧から*.txtに一致するものだけを取り出すコードは次のとおりです。
C#using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
class Program
{
static void Main()
{
var names = new List<string>
{
"sample.txt",
"readme.md",
"log.txt",
"data.csv",
"test.TXT"
};
var results = names
.Where(name => IsWildcardMatch(name, "*.txt", ignoreCase: true))
.ToList();
foreach (var result in results)
{
Console.WriteLine(result);
}
}
static bool IsWildcardMatch(string input, string pattern, bool ignoreCase = false)
{
if (input == null || pattern == null)
{
return false;
}
var regexPattern = "^" + Regex.Escape(pattern)
.Replace(@"\*", ".*")
.Replace(@"\?", ".") + "$";
var options = ignoreCase
? RegexOptions.IgnoreCase | RegexOptions.CultureInvariant
: RegexOptions.None;
return Regex.IsMatch(input, regexPattern, options);
}
}
実行結果は次のようになります。
sample.txt
log.txt
test.TXT
ignoreCase: trueを指定しているため、test.TXTも一致します。
配列でも同じように使えます。
C#string[] values =
{
"ABC001",
"ABC002",
"DEF001",
"ABC_TEST"
};
var matched = values.Where(x => IsWildcardMatch(x, "ABC*")).ToArray();
LINQと組み合わせることで、ワイルドカード条件に一致するデータを簡単に抽出できます。
4-3. 複数のワイルドカード条件で検索する
複数のワイルドカード条件のいずれかに一致するものを抽出したい場合は、Anyを使うと便利です。
C#using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
class Program
{
static void Main()
{
var files = new List<string>
{
"sample.txt",
"data.csv",
"image.png",
"report.xlsx",
"log.txt"
};
var patterns = new List<string>
{
"*.txt",
"*.csv"
};
var results = files
.Where(file => patterns.Any(pattern =>
IsWildcardMatch(file, pattern, ignoreCase: true)))
.ToList();
foreach (var result in results)
{
Console.WriteLine(result);
}
}
static bool IsWildcardMatch(string input, string pattern, bool ignoreCase = false)
{
if (input == null || pattern == null)
{
return false;
}
var regexPattern = "^" + Regex.Escape(pattern)
.Replace(@"\*", ".*")
.Replace(@"\?", ".") + "$";
var options = ignoreCase
? RegexOptions.IgnoreCase | RegexOptions.CultureInvariant
: RegexOptions.None;
return Regex.IsMatch(input, regexPattern, options);
}
}
この例では、*.txtまたは*.csvに一致するファイル名を抽出しています。
結果は次のようになります。
sample.txt
data.csv
log.txt
反対に、すべての条件に一致させたい場合はAllを使います。
C#var results = files
.Where(file => patterns.All(pattern => IsWildcardMatch(file, pattern)))
.ToList();
ただし、複数のワイルドカード条件で検索する場合、「OR条件」なのか「AND条件」なのかを明確にしておくことが重要です。
4-4. 拡張メソッドとして再利用しやすくする
ワイルドカード検索を複数の場所で使う場合は、拡張メソッドにしておくと便利です。
C#using System.Text.RegularExpressions;
public static class StringExtensions
{
public static bool IsWildcardMatch(
this string input,
string pattern,
bool ignoreCase = false)
{
if (input == null || pattern == null)
{
return false;
}
var regexPattern = "^" + Regex.Escape(pattern)
.Replace(@"\*", ".*")
.Replace(@"\?", ".") + "$";
var options = ignoreCase
? RegexOptions.IgnoreCase | RegexOptions.CultureInvariant
: RegexOptions.None;
return Regex.IsMatch(input, regexPattern, options);
}
}
このように定義しておけば、次のように自然な形で使えます。
C#var fileName = "sample.txt";
if (fileName.IsWildcardMatch("*.txt", ignoreCase: true))
{
Console.WriteLine("一致しました");
}
List検索でも使いやすくなります。
C#var files = new[]
{
"sample.txt",
"data.csv",
"readme.md"
};
var results = files
.Where(file => file.IsWildcardMatch("*.txt"))
.ToList();
拡張メソッドにしておくことで、検索条件の変換処理を毎回書かずに済み、コードの重複を減らせます。
4-5. .NET Framework/.NET 6以降で使える実装例
Regex.EscapeやRegex.IsMatchは、.NET Frameworkでも.NET 6以降でも利用できます。
そのため、基本的なワイルドカード検索であれば、次のような実装は幅広い環境で使えます。
C#using System;
using System.Text.RegularExpressions;
public static class Wildcard
{
public static bool IsMatch(
string input,
string pattern,
bool ignoreCase = false)
{
if (input == null || pattern == null)
{
return false;
}
string regexPattern = ToRegexPattern(pattern);
RegexOptions options = ignoreCase
? RegexOptions.IgnoreCase | RegexOptions.CultureInvariant
: RegexOptions.None;
return Regex.IsMatch(input, regexPattern, options);
}
public static string ToRegexPattern(string pattern)
{
return "^" + Regex.Escape(pattern)
.Replace(@"\*", ".*")
.Replace(@"\?", ".") + "$";
}
}
使用例は次のとおりです。
C#Console.WriteLine(Wildcard.IsMatch("sample.txt", "*.txt")); // True
Console.WriteLine(Wildcard.IsMatch("file1.csv", "file?.csv")); // True
Console.WriteLine(Wildcard.IsMatch("FILE1.CSV", "file?.csv", true)); // True
.NET 6以降でパフォーマンスを意識する場合は、同じパターンを何度も使うときにRegexインスタンスを再利用する設計も検討できます。
C#var regex = new Regex(
Wildcard.ToRegexPattern("*.txt"),
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
var result = regex.IsMatch("sample.txt");
大量のデータを検索する場合、毎回Regexを作成するよりも、パターンごとにRegexを使い回したほうが効率的です。
5. ファイル名をワイルドカード検索する方法
C#でファイル名をワイルドカード検索する場合は、文字列検索とは別の方法を使えます。
Directory.GetFilesやDirectory.EnumerateFilesには、検索パターンを指定できる引数があります。
たとえば、指定フォルダ内の.txtファイルを検索する場合は、次のように書けます。
C#var files = Directory.GetFiles(@"C:\Logs", "*.txt");
この場合、*.txtというワイルドカードパターンをそのまま使えます。
ただし、ファイル検索で使うワイルドカードと、文字列検索で自作するワイルドカード検索は同じではありません。ファイルシステムの検索仕様に依存する部分があるため、複雑な条件では自分で絞り込む必要があります。
5-1. Directory.GetFilesで「*.txt」などを検索する
Directory.GetFilesを使うと、指定したフォルダから検索パターンに一致するファイルパスを取得できます。
C#using System;
using System.IO;
class Program
{
static void Main()
{
string directory = @"C:\Logs";
string[] files = Directory.GetFiles(directory, "*.txt");
foreach (var file in files)
{
Console.WriteLine(file);
}
}
}
*.txtは、.txtで終わるファイル名に一致します。
error.txt
access.txt
system.txt
ファイル名だけを取得したい場合は、Path.GetFileNameを使います。
C#foreach (var file in files)
{
Console.WriteLine(Path.GetFileName(file));
}
大量のファイルを扱う場合は、Directory.GetFilesよりもDirectory.EnumerateFilesを使うと効率的な場合があります。
C#foreach (var file in Directory.EnumerateFiles(directory, "*.txt"))
{
Console.WriteLine(file);
}
GetFilesは結果を配列としてまとめて取得しますが、EnumerateFilesは列挙しながら処理できます。ファイル数が多い場合はEnumerateFilesのほうが扱いやすいです。
5-2. SearchOptionでサブフォルダも検索する
サブフォルダも含めて検索したい場合は、SearchOption.AllDirectoriesを指定します。
C#using System;
using System.IO;
class Program
{
static void Main()
{
string directory = @"C:\Logs";
var files = Directory.GetFiles(
directory,
"*.txt",
SearchOption.AllDirectories);
foreach (var file in files)
{
Console.WriteLine(file);
}
}
}
現在のフォルダだけを検索する場合は、SearchOption.TopDirectoryOnlyを使います。
C#var files = Directory.GetFiles(
directory,
"*.txt",
SearchOption.TopDirectoryOnly);
SearchOption.AllDirectoriesを使うと、配下のすべてのサブフォルダを検索します。そのため、フォルダ数やファイル数が多い場合は処理に時間がかかる可能性があります。
また、アクセス権限のないフォルダが含まれていると例外が発生することがあります。
例外を考慮する場合は、必要に応じてtry-catchを使います。
C#try
{
foreach (var file in Directory.EnumerateFiles(
directory,
"*.txt",
SearchOption.AllDirectories))
{
Console.WriteLine(file);
}
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine($"アクセスできないフォルダがあります: {ex.Message}");
}
実務では、アクセス権限、長いパス、大量ファイル、ネットワークドライブなども考慮して設計する必要があります。
5-3. 複数拡張子を検索する方法
Directory.GetFilesの検索パターンには、通常、複数の拡張子をまとめて指定できません。
たとえば、次のように書いても、*.txtと*.csvを同時に検索することはできません。
C#// このような指定はできない
var files = Directory.GetFiles(directory, "*.txt;*.csv");
複数拡張子を検索したい場合は、パターンごとに検索して結合する方法が分かりやすいです。
C#using System;
using System.IO;
using System.Linq;
class Program
{
static void Main()
{
string directory = @"C:\Logs";
string[] patterns =
{
"*.txt",
"*.csv",
"*.log"
};
var files = patterns
.SelectMany(pattern => Directory.EnumerateFiles(directory, pattern))
.ToList();
foreach (var file in files)
{
Console.WriteLine(file);
}
}
}
サブフォルダも含めたい場合は、次のようにします。
C#var files = patterns
.SelectMany(pattern => Directory.EnumerateFiles(
directory,
pattern,
SearchOption.AllDirectories))
.ToList();
または、すべてのファイルを取得してから拡張子で絞り込む方法もあります。
C#var extensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
".txt",
".csv",
".log"
};
var files = Directory.EnumerateFiles(directory, "*.*")
.Where(file => extensions.Contains(Path.GetExtension(file)))
.ToList();
拡張子の種類が多い場合や、大文字・小文字を無視したい場合は、HashSetを使った絞り込みが便利です。
5-4. ファイル名検索でRegexを使うべきケース
ファイル検索ではDirectory.GetFilesやDirectory.EnumerateFilesの検索パターンで十分な場合が多いです。
しかし、次のようなケースでは、ファイル一覧を取得した後にRegexで絞り込む方法が適しています。
複雑な条件で検索したい
複数のワイルドカード条件を組み合わせたい
ファイル名だけを対象にしたい
日付や連番の形式までチェックしたい
大文字・小文字の扱いを明確にしたい
たとえば、log_2024_01.txtのようなファイル名だけを検索したい場合は、正規表現が便利です。
C#using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
class Program
{
static void Main()
{
string directory = @"C:\Logs";
var regex = new Regex(
@"^log_\d{4}_\d{2}\.txt$",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
var files = Directory.EnumerateFiles(directory, "*.*")
.Where(file => regex.IsMatch(Path.GetFileName(file)))
.ToList();
foreach (var file in files)
{
Console.WriteLine(file);
}
}
}
ワイルドカードで表現しにくい条件は、正規表現を直接使ったほうがシンプルになることがあります。
一方で、単に.txtファイルを探すだけなら、Directory.EnumerateFiles(directory, "*.txt")で十分です。
6. SQLやデータベース検索でのワイルドカードとの違い
C#のワイルドカード検索とSQLのワイルドカード検索は、使う記号が異なります。
C#側でユーザーに*や?を入力させ、その条件でデータベースを検索したい場合は、SQLのLIKE検索用に変換する必要があります。
ただし、SQL検索ではセキュリティ上の注意点もあります。ユーザー入力をそのままSQL文字列に連結すると、SQLインジェクションの原因になります。
そのため、SQLでワイルドカード検索を行う場合は、必ずパラメータを使って値を渡すことが重要です。
6-1. C#の「*・?」とSQLの「%・_」の違い
C#のワイルドカードとしてよく使われる記号は、次の2つです。
* 任意の0文字以上
? 任意の1文字
一方、SQLのLIKE検索では、一般的に次の記号を使います。
% 任意の0文字以上
_ 任意の1文字
つまり、意味としては次のように対応します。
| C#側のワイルドカード | SQL LIKE | 意味 |
|---|---|---|
* | % | 任意の0文字以上 |
? | _ | 任意の1文字 |
たとえば、C#側で次のような検索条件を受け取ったとします。
user*
これをSQLのLIKE検索に使う場合は、次のように変換します。
user%
また、次の条件は、
file?.txt
SQLでは次のようになります。
file_.txt
ただし、SQLでは%や_そのものを検索したい場合のエスケープも考える必要があります。
6-2. LIKE検索に変換する場合の注意点
ワイルドカードをSQLのLIKE検索に変換するだけなら、単純な置換で実装できます。
C#static string WildcardToSqlLike(string pattern)
{
return pattern
.Replace("*", "%")
.Replace("?", "_");
}
しかし、この実装だけでは不十分な場合があります。
SQLのLIKEでは、%や_が特殊な意味を持つため、ユーザーが入力した%や_を通常の文字として扱いたい場合はエスケープが必要です。
たとえば、ユーザーが次のように入力した場合を考えます。
100_*
_をそのままSQLに渡すと、任意の1文字として扱われてしまいます。ユーザーが本当にアンダースコアを検索したい場合は、意図しない結果になります。
SQL Serverを例にすると、次のようにエスケープ文字を指定する方法があります。
SQLWHERE Name LIKE @pattern ESCAPE '\'
C#側では、まずSQL LIKEの特殊文字をエスケープし、その後でC#側のワイルドカードをSQL用に変換します。
C#static string WildcardToSqlLikePattern(string pattern)
{
if (pattern == null)
{
return null;
}
return pattern
.Replace(@"\", @"\\")
.Replace("%", @"\%")
.Replace("_", @"\_")
.Replace("[", @"\[")
.Replace("*", "%")
.Replace("?", "_");
}
実際のエスケープ仕様は、使用しているデータベースによって異なります。SQL Server、MySQL、PostgreSQL、SQLiteなどで細かな違いがあるため、利用するDBに合わせて実装してください。
6-3. SQLインジェクションを防ぐ書き方
SQL検索で最も注意すべきなのは、ユーザー入力をSQL文字列に直接連結しないことです。
悪い例は次のとおりです。
C#string sql = "SELECT * FROM Users WHERE Name LIKE '" + pattern + "'";
この書き方はSQLインジェクションの危険があります。ユーザー入力がSQL文の一部として解釈されてしまう可能性があるためです。
安全に書くには、パラメータを使います。
C#using var command = connection.CreateCommand();
command.CommandText = @"
SELECT *
FROM Users
WHERE Name LIKE @pattern ESCAPE '\'
";
var parameter = command.CreateParameter();
parameter.ParameterName = "@pattern";
parameter.Value = WildcardToSqlLikePattern(userInput);
command.Parameters.Add(parameter);
SQL ServerでSqlCommandを使う場合の例は次のとおりです。
C#using Microsoft.Data.SqlClient;
string userInput = "user*";
string likePattern = WildcardToSqlLikePattern(userInput);
using var connection = new SqlConnection(connectionString);
using var command = new SqlCommand(@"
SELECT Id, Name
FROM Users
WHERE Name LIKE @pattern ESCAPE '\'
", connection);
command.Parameters.AddWithValue("@pattern", likePattern);
connection.Open();
using var reader = command.ExecuteReader();
while (reader.Read())
{
Console.WriteLine(reader["Name"]);
}
重要なのは、LIKEのパターンであっても文字列連結でSQLに埋め込まないことです。必ずパラメータとして渡しましょう。
6-4. LINQでワイルドカード検索を実装する方法
メモリ上のListに対してLINQでワイルドカード検索を行う場合は、これまで紹介したIsWildcardMatchを使えます。
C#var users = new List<string>
{
"admin",
"administrator",
"user01",
"guest"
};
var results = users
.Where(user => IsWildcardMatch(user, "admin*"))
.ToList();
Entity Framework Coreなどでデータベースに対して検索する場合は、状況が変わります。
メモリ上のRegex.IsMatchは、そのままSQLに変換できないことがあります。そのため、データベース検索ではEF.Functions.Likeを使う方法が一般的です。
C#string userInput = "admin*";
string likePattern = userInput
.Replace("*", "%")
.Replace("?", "_");
var results = dbContext.Users
.Where(user => EF.Functions.Like(user.Name, likePattern))
.ToList();
ただし、この場合も%や_を通常文字として扱う必要があるかどうかを検討してください。
メモリ上のコレクションを検索するならRegex、データベース上で検索するならLIKEというように、検索対象に合わせて使い分けることが重要です。
7. ワイルドカード検索実装時の注意点
ワイルドカード検索は便利ですが、実装を間違えると予期しない一致やセキュリティ上の問題につながります。
特に注意すべきポイントは次の5つです。
正規表現の特殊文字をエスケープする
nullや空文字の扱いを決める
日本語や全角文字の扱いを考慮する
Regexのパフォーマンスに注意する
ユーザー入力をそのまま使うリスクを理解する
それぞれ詳しく見ていきましょう。
7-1. 正規表現の特殊文字をエスケープする
ワイルドカード検索を正規表現で実装する場合、最も重要なのがエスケープ処理です。
たとえば、ユーザーが次のような検索パターンを入力したとします。
file(1).txt
この文字列には、正規表現で特別な意味を持つ括弧()とドット.が含まれています。
これをそのまま正規表現として使うと、意図しない意味で解釈される可能性があります。
悪い例は次のとおりです。
C#var regexPattern = "^" + pattern
.Replace("*", ".*")
.Replace("?", ".") + "$";
このコードでは、正規表現の特殊文字がそのまま残ってしまいます。
安全な実装は次のとおりです。
C#var regexPattern = "^" + Regex.Escape(pattern)
.Replace(@"\*", ".*")
.Replace(@"\?", ".") + "$";
Regex.Escapeを使うことで、ユーザー入力の大部分を通常の文字として扱えます。そのうえで、ワイルドカードとして扱いたい*と?だけを正規表現に変換します。
この順番を守ることが、ワイルドカード検索を安全に実装する基本です。
7-2. nullや空文字をどう扱うか決める
ワイルドカード検索では、nullや空文字をどう扱うかを事前に決めておく必要があります。
たとえば、次のようなケースです。
C#IsWildcardMatch(null, "*")
IsWildcardMatch("abc", null)
IsWildcardMatch("", "*")
IsWildcardMatch("", "")
nullが渡されたときにfalseを返すのか、例外を投げるのかは設計次第です。
シンプルな実装では、nullならfalseを返すことが多いです。
C#if (input == null || pattern == null)
{
return false;
}
一方で、呼び出し側のバグを早く見つけたい場合は、例外を投げる設計もあります。
C#if (input == null) throw new ArgumentNullException(nameof(input));
if (pattern == null) throw new ArgumentNullException(nameof(pattern));
空文字についても考え方を決めておきましょう。
pattern = "*" → 空文字に一致する
pattern = "" → 空文字だけに一致する
pattern = "?" → 空文字には一致しない
*は0文字以上に一致するため、空文字にも一致します。
C#Console.WriteLine(IsWildcardMatch("", "*")); // True
Console.WriteLine(IsWildcardMatch("", "?")); // False
検索機能として提供する場合、空の検索条件を「全件表示」と扱うのか、「空文字だけに一致」と扱うのかも重要です。UIや仕様に合わせて明確に決めておきましょう。
7-3. 日本語・全角文字を検索する場合の注意点
日本語を含む文字列でも、基本的なワイルドカード検索は動作します。
C#Console.WriteLine(IsWildcardMatch("東京都新宿区", "東京*")); // True
Console.WriteLine(IsWildcardMatch("商品A", "商品?")); // True
ただし、日本語検索では次のような違いが問題になることがあります。
全角と半角
ひらがなとカタカナ
大文字と小文字
濁点・半濁点
Unicode正規化形式の違い
たとえば、次の文字列は見た目が似ていても、内部的には異なる文字として扱われることがあります。
カタカナ
カタカナ
また、濁点付き文字でも、1文字として表現される場合と、文字+結合文字として表現される場合があります。
そのため、日本語検索の精度を上げたい場合は、検索前に文字列を正規化する方法を検討します。
C#using System.Text;
var normalized = input.Normalize(NormalizationForm.FormC);
全角・半角やひらがな・カタカナを同一視したい場合は、標準のワイルドカード検索だけでは不十分です。必要に応じて、検索用の文字列を別途作成したり、形態素解析や全文検索エンジンの利用を検討したりするとよいでしょう。
7-4. パフォーマンスを意識したRegexの使い方
少量の文字列を検索するだけなら、Regex.IsMatchを毎回呼び出しても大きな問題になることは少ないです。
しかし、大量のデータに対して同じワイルドカードパターンを繰り返し使う場合は、パフォーマンスに注意が必要です。
たとえば、次のようなコードでは、要素ごとに正規表現パターンを作成しています。
C#var results = items
.Where(item => IsWildcardMatch(item, "*.txt"))
.ToList();
件数が少なければ問題ありませんが、大量データでは正規表現の生成を一度だけにしたほうが効率的です。
C#var regex = new Regex(
Wildcard.ToRegexPattern("*.txt"),
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
var results = items
.Where(item => regex.IsMatch(item))
.ToList();
また、ユーザー入力から複雑な正規表現を生成する場合は、タイムアウトの指定も検討できます。
C#var regex = new Regex(
Wildcard.ToRegexPattern(pattern),
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant,
TimeSpan.FromSeconds(1));
今回のようにRegex.Escapeを使い、*と?だけを変換する単純なワイルドカード検索では、危険な正規表現になりにくいですが、ユーザー入力を扱う以上、想定外の長い文字列や大量アクセスには注意が必要です。
7-5. ユーザー入力をそのまま使う場合のリスク
ユーザーが入力した検索条件をそのまま使う場合、次のようなリスクがあります。
正規表現として意図しない意味になる
極端に長い入力で処理が重くなる
SQLに連結してSQLインジェクションが起きる
想定外のワイルドカードで大量データに一致する
正規表現に変換する場合は、必ずRegex.Escapeを使いましょう。
SQLに渡す場合は、必ずパラメータを使いましょう。
また、検索条件の長さに上限を設けることも有効です。
C#if (pattern.Length > 100)
{
throw new ArgumentException("検索条件が長すぎます。");
}
さらに、*だけの検索を許可すると全件に一致します。
*
小規模なデータなら問題ありませんが、大量データを扱うシステムでは負荷が高くなる可能性があります。
検索機能を公開する場合は、次のような対策を検討しましょう。
検索条件の最大文字数を制限する
全件一致になる検索を制限する
ページングを行う
SQLではインデックス設計を考慮する
Regexにタイムアウトを設定する
ワイルドカード検索は便利な反面、広範囲に一致しやすい検索です。使いやすさと安全性のバランスを取ることが重要です。
8. よくあるエラーと対処法
C#でワイルドカード検索を実装していると、いくつかの典型的なトラブルが発生します。
ここでは、よくある原因と対処法を紹介します。
8-1. 「*」が期待通りに一致しない
よくある原因は、ワイルドカードの*と正規表現の*を混同していることです。
ワイルドカードでは、*は任意の0文字以上を意味します。
abc*
これは、abcで始まる任意の文字列に一致するイメージです。
一方、正規表現の*は、直前の文字の0回以上の繰り返しです。
abc*
正規表現としては、abの後にcが0回以上続くという意味になります。
そのため、ワイルドカードの*を正規表現で表すには、.*に変換する必要があります。
C#.Replace(@"\*", ".*")
また、Regex.Escapeを使った場合、置換対象は*ではなく\*になります。
C#Regex.Escape("*.txt") // \*\.txt
そのため、次のように書く必要があります。
C#.Replace(@"\*", ".*")
Replace("*", ".*")では、Regex.Escape後の文字列に対して期待通りに置換できない場合があります。
8-2. ドットや括弧などの記号で検索結果がおかしくなる
ドット.や括弧()などの記号を含むパターンで検索結果がおかしくなる場合は、正規表現の特殊文字をエスケープしていない可能性があります。
たとえば、次のようなファイル名を検索したいとします。
file(1).txt
この文字列をそのまま正規表現にすると、.や()が特殊な意味として扱われる可能性があります。
悪い例は次のとおりです。
C#var regexPattern = "^" + pattern
.Replace("*", ".*")
.Replace("?", ".") + "$";
良い例は次のとおりです。
C#var regexPattern = "^" + Regex.Escape(pattern)
.Replace(@"\*", ".*")
.Replace(@"\?", ".") + "$";
Regex.Escapeを先に使うことで、ユーザーが入力したドットや括弧を通常の文字として扱えます。
ワイルドカード検索では、「まず全体をエスケープし、その後で*と?だけを変換する」という順番を守りましょう。
8-3. 部分一致と完全一致を間違える
ワイルドカード検索でよくあるのが、部分一致と完全一致の混同です。
たとえば、次のパターンを考えます。
abc*
このパターンを完全一致として扱うなら、abcで始まる文字列全体に一致する必要があります。
abc123 一致
xxxabc123 不一致
しかし、正規表現に^と$を付けないと、文字列の途中に一致する部分があればtrueになることがあります。
完全一致にしたい場合は、次のようにします。
C#var regexPattern = "^" + Regex.Escape(pattern)
.Replace(@"\*", ".*")
.Replace(@"\?", ".") + "$";
部分一致にしたい場合は、設計として次のどちらかを選びます。
^と$を付けない
ユーザーに*abc*のようなパターンを入力してもらう
実務では、完全一致ベースにして、部分一致したい場合は*keyword*を指定してもらう設計が分かりやすいです。
8-4. 大文字・小文字の違いで一致しない
sample.txtには一致するのにSample.TXTには一致しない場合は、大文字・小文字が区別されている可能性があります。
Regex.IsMatchは、オプションを指定しない場合、大文字・小文字を区別します。
C#Regex.IsMatch("Sample.TXT", "^.*\\.txt$"); // False
大文字・小文字を区別しない場合は、RegexOptions.IgnoreCaseを指定します。
C#Regex.IsMatch(
"Sample.TXT",
"^.*\\.txt$",
RegexOptions.IgnoreCase);
より明確にするなら、次のようにCultureInvariantも指定できます。
C#RegexOptions options =
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
ファイル名やコード値の検索では、大文字・小文字を無視したいケースが多いです。検索仕様として区別するのか、区別しないのかをあらかじめ決めておきましょう。
8-5. ファイル検索と文字列検索を混同している
Directory.GetFilesの検索パターンと、自作のワイルドカード検索は同じものではありません。
ファイル検索では、次のように*.txtをそのまま指定できます。
C#var files = Directory.GetFiles(directory, "*.txt");
しかし、通常の文字列に対して次のように書いても、ワイルドカード検索にはなりません。
C#var result = text.Contains("*.txt");
これは、*.txtという文字列そのものを含むかどうかを判定しているだけです。
文字列に対してワイルドカード検索をしたい場合は、正規表現に変換して判定します。
C#var regexPattern = "^" + Regex.Escape("*.txt")
.Replace(@"\*", ".*")
.Replace(@"\?", ".") + "$";
var result = Regex.IsMatch("sample.txt", regexPattern);
ファイル名を検索する場合でも、単純な拡張子検索ならDirectory.EnumerateFilesの検索パターンで十分です。一方、複雑な条件で絞り込みたい場合は、ファイル一覧を取得してからRegexで判定するとよいでしょう。
まとめ
C#でワイルドカード検索を実装する場合、文字列検索では*や?を直接解釈する標準メソッドはないため、正規表現に変換してRegex.IsMatchで判定する方法が実用的です。
基本的な変換ルールは次のとおりです。
* → .*
? → .
ただし、正規表現の特殊文字を安全に扱うために、単純なReplaceだけで実装するのではなく、Regex.Escapeを使うことが重要です。
基本形は次のコードです。
C#static bool IsWildcardMatch(string input, string pattern, bool ignoreCase = false)
{
if (input == null || pattern == null)
{
return false;
}
var regexPattern = "^" + Regex.Escape(pattern)
.Replace(@"\*", ".*")
.Replace(@"\?", ".") + "$";
var options = ignoreCase
? RegexOptions.IgnoreCase | RegexOptions.CultureInvariant
: RegexOptions.None;
return Regex.IsMatch(input, regexPattern, options);
}
ファイル名検索では、Directory.GetFilesやDirectory.EnumerateFilesに*.txtのような検索パターンを渡せます。
C#var files = Directory.EnumerateFiles(directory, "*.txt");
SQL検索では、C#の*や?ではなく、SQLの%や_に変換してLIKE検索を行います。
* → %
? → _
ただし、SQLでは必ずパラメータを使い、SQLインジェクションを防ぐ必要があります。
ワイルドカード検索を実装するときは、次の点を意識しましょう。
Regex.Escapeで特殊文字をエスケープする
完全一致なら^と$を付ける
大文字・小文字を無視するならRegexOptions.IgnoreCaseを使う
ファイル検索と文字列検索を混同しない
SQL検索では%と_に変換し、パラメータを使う
C#のワイルドカード検索は、正規表現への変換ルールを理解すればシンプルに実装できます。*と?の意味、エスケープ処理、検索対象ごとの違いを押さえておけば、文字列検索、ファイル検索、データベース検索のいずれにも応用できます。

