正規表現で文字列検索:大文字小文字・トルコ語のI・エスケープ
普通の文字だけの正規表現検索、大文字小文字の無視とカルチャの問題(トルコ語のIの実際の出力)、RegexOptions、ユーザー入力をRegex.Escapeでエスケープする方法を4エンジンで検証。
2026年11月改訂: 2021年に韓国語で書いた講座を日本語で書き直しました。例はすべて .NET 10(System.Text.RegularExpressions)、Python 3.9・3.12 の
re、PHP 8.5(PCRE2 10.47)、Dart 3.11(RegExpはJavaScriptと同じ文法)で実行し直しています。訂正が2つあります。「パターンを小文字のtextにしてもTextが見つかる」「R\.はMr.のr.にもマッチする」という説明は、どちらも大文字小文字を無視する設定のときだけ正しい内容でした。あわせて、カルチャの問題(トルコ語のI)、RegexOptions、ユーザー入力のエスケープを追加しました。
この記事は、正規表現で「この単語がある場所」を探したい人と、検索欄に入力された文字をそのままパターンに入れて例外を出してしまった人に向けたものです。押さえるべき点は3つです。普通の文字もそのまま正規表現になるが既定では大文字小文字を区別すること、大文字小文字の無視はカルチャで結果が変わること、そしてユーザー入力は必ずエスケープすることです。正規表現そのものについては正規表現とは?で扱いました。
普通の文字も正規表現になる
特別な記号を含まないパターンは、その文字列そのものを探します。2021年の記事の例です。
パターン: Text
テキスト: ABCText Text ITexty
ioTextm Textll abcdety
結果: 位置 3, 8, 14, 22, 28 の "Text" が5つ
単語の途中(ABCText、ITexty)でも見つかります。単語単位で探したいなら \bText\b のように単語境界を付けます。
元記事は続けて「パターンのTを小文字にしても、大文字小文字を区別せずに見つかる」と書いていましたが、既定の動作は違います。4エンジンとも text では0件で、大文字小文字を無視する設定にして初めて5件見つかりました。
| エンジン | 大文字小文字を無視する設定 | パターン内での指定 |
|---|---|---|
| .NET | RegexOptions.IgnoreCase |
(?i)text、(?i:t)ext |
| Python | re.I(re.IGNORECASE) |
(?i)text(パターンの先頭のみ) |
| PHP | /text/i |
(?i)text |
| Dart | RegExp('text', caseSensitive: false) |
非対応(Invalid group) |
インラインの指定にも違いがありました。x(?i)text のように途中に書いた (?i) は .NET と PHP では動きましたが、Python 3.9 は DeprecationWarning: Flags not at the start of the expression を出したうえで動作し、Python 3.12 では global flags not at the start of the expression というエラーになりました。Dart は (?i) 自体を受け付けませんでした。
ドット(.)は「任意の1文字」、空白も含む
. は改行以外の任意の1文字にマッチします。2021年の記事の h.s を英文に適用すると、4エンジンとも has、has、his、his の4件でした。
h.. は18件でした。元記事は強調表示が崩れて hashur のような塊に見えていましたが、実際のマッチは has、hur、her、hat、he、h i、hab … と3文字ずつです。he や h i のとおり、. は空白にもマッチします。タブにもマッチしましたが、改行 \n にはマッチしませんでした(テキスト h s h\ts h\ns では h s と h\ts だけ)。改行まで含める Singleline についてはメタ文字で扱います。
. そのものを探すときは \. と書きます。
パターン: R\.
テキスト: I think I should do well to name him, Mr. Alfred R. Phillips.
既定: R. (1件)
大小無視: r.(Mr. の r.)、R. (2件)
元記事は r. と R. の両方が見つかったとしていましたが、これも大文字小文字を無視した結果です。エスケープを忘れた場合も確かめました。パターン 1.5 は version 105 and 1.5 の 105 と 1.5 の両方にマッチしました。エラーにならず余計に見つかるだけなので、レビューでも見逃しやすいバグです。
大文字小文字の無視とカルチャ:トルコ語のI
「Aとaを同じとみなす」だけでは済みません。トルコ語には点付きの大文字 İ(U+0130)と点なしの小文字 ı(U+0131)があり、トルコ語の規則では I の小文字は ı、i の大文字は İ です。.NET 10で CurrentCulture を切り替えながら、テキスト I İ ı i にパターン i(大文字小文字を無視)を適用しました。
en-US /i/i -> [I,İ,i] +CultureInvariant -> [I,i]
tr-TR /i/i -> [İ,i] +CultureInvariant -> [I,i]
tr-TR /I/i -> [I,ı] +CultureInvariant -> [I,i]
tr-TR "FILE".ToLower() = fıle, ToLowerInvariant = file
tr-TR Contains("file", CurrentCultureIgnoreCase) = False, OrdinalIgnoreCase = True
tr-TR Regex.IsMatch("FILE", "file", IgnoreCase) = False, +CultureInvariant = True
built under tr-TR, run under en-US: False
トルコ語環境では Regex.IsMatch("FILE", "file", RegexOptions.IgnoreCase) が False になりました。「トルコのユーザーだけ拡張子チェックが失敗する」というバグはこうして起きます。最後の行も重要です。tr-TRで生成した Regex は、あとでen-USに切り替えて実行してもFalseのままでした。.NETの Regex は大文字小文字の規則を実行時ではなく生成時のカルチャで決めます。アプリ起動時に static フィールドで作ったパターンは、そのときのカルチャに縛られるということです。
ほかのエンジンはカルチャ設定を使いませんが、結果は互いに異なりました。
テキスト I İ ı i に i+大小無視 |
結果 |
|---|---|
| .NET 10(en-US) | I、İ、i |
| .NET 10 + CultureInvariant | I、i |
| Python 3.9 / 3.12 | I、İ、ı、i |
PHP 8.5 /iu |
I、i |
| Dart | I、i |
ケルビン記号 K(U+212A)は .NET・Python・PHP で k にマッチし、Dart は unicode: true のときだけマッチしました。ドイツ語の ß はどのエンジンでも strasse にマッチしませんでした。正規表現の大文字小文字の無視は、1文字対1文字の比較だからです。
判断基準はシンプルです。拡張子、HTTPヘッダー、コマンド、設定キーのように言語に依存しない値には RegexOptions.CultureInvariant(文字列メソッドなら StringComparison.OrdinalIgnoreCase)を使います。カルチャの規則を考慮するのは、ユーザーに見せる自然言語の検索だけです。
RegexOptions 早見表
.NET 10で各オプションを実際に動かして確認した結果です。
| オプション | 効果 | 確認した例 |
|---|---|---|
IgnoreCase |
大文字小文字を無視 | 上のトルコ語のIを参照 |
CultureInvariant |
カルチャに依存しない大小規則 | tr-TRでも FILE と file が一致 |
Multiline |
^・$ が行ごとに一致 |
メタ文字 |
Singleline |
. が \n にも一致 |
メタ文字 |
IgnorePatternWhitespace |
パターン中の空白を無視、# コメント可 |
(\d{4}) - (\d{2}) # year-month → 2026-11 |
ExplicitCapture |
名前なしの ( ) はキャプチャしない |
(\d{4})-(?<m>\d{2}) のグループキー:0、m |
RightToLeft |
右から左へ検索 | a1 b2 c3 で \w\d の最初のマッチは c3 |
ECMAScript |
JavaScriptの規則(\d はASCIIなど) |
ECMAScript | Singleline は ArgumentOutOfRangeException |
Compiled |
ILにコンパイル、生成コスト大 | 初回生成に約6ms(実測) |
NonBacktracking |
線形時間エンジン | 後方参照と併用すると NotSupportedException |
静的メソッド Regex.IsMatch(input, pattern) は最近使ったパターンを内部キャッシュに保持し、既定の Regex.CacheSize は15でした。数十個のパターンを使い回すコードなら、Regex インスタンスをフィールドに持つか [GeneratedRegex] を使いましょう。
ユーザー入力は Regex.Escape で
検索欄にユーザーが C++ (beta) と入力し、それをそのままパターンに入れたとします。
.NET : RegexParseException: Invalid pattern 'C++' at offset 3. Nested quantifier '+'.
.NET : Invalid pattern '(beta' at offset 5. Not enough )'s.
Python 3.9 : multiple repeat at position 2
PHP : Compilation failed: missing closing parenthesis at offset 5
Dart : FormatException: Nothing to repeat
例外になるのはまだ運が良いほうです。$5.00 は4エンジンともエラーなしで何も見つからず($ が行末を意味するため)、1.5 は前述のとおり 105 まで拾いました。解決策はエスケープです。
同じ入力 C++ (beta) $5.00? a#b c.d [x] を各エンジンのエスケープ関数に通した実際の出力です。
| 関数 | 出力 |
|---|---|
.NET Regex.Escape |
C\+\+\ \(beta\)\ \$5\.00\?\ a\#b\ c\.d\ \[x] |
Python re.escape(3.9、3.12) |
C\+\+\ \(beta\)\ \$5\.00\?\ a\#b\ c\.d\ \[x\] |
PHP preg_quote |
C\+\+ \(beta\) \$5\.00\? a\#b c\.d \[x\] |
Dart RegExp.escape |
C\+\+ \(beta\) \$5\.00\? a#b c\.d \[x\] |
形は少しずつ違いますが(.NETは ] をエスケープしない、空白や # の扱いが異なる)、どれも元の文字列に正確にマッチしました。PHPには落とし穴がひとつあります。preg_quote は区切り文字 / を既定ではエスケープしないので、"/" . preg_quote("a/b") . "/" は Unknown modifier 'b' という警告とともに失敗し、preg_quote("a/b", "/") のように区切り文字を渡すと動きました。
string user = "C++ (beta)";
var rx = new Regex(Regex.Escape(user), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
置換文字列は別の話です。Regex.Escape はパターン用で、.NETの置換文字列で $ を文字として書くには $$ とします。Regex.Replace("price: X", "X", "$5.00") はグループ5が存在しないので偶然 price: $5.00 になりましたが、"[$0]" を渡すと price: [X] のようにマッチ全体が入りました。置換文字列にユーザー入力を入れるなら $ を $$ に置き換えるか、MatchEvaluator を使うのが安全です。置換でのグループ参照は後方参照の記事にエンジン別の表があります。
まとめ
| やりたいこと | 方法 |
|---|---|
| 固定文字列が含まれるかだけ確認 | 正規表現より Contains(..., StringComparison.Ordinal) |
| 大小無視(識別子・拡張子) | IgnoreCase | CultureInvariant |
. や $ などの記号そのものを探す |
\.、\$(メタ文字) |
| ユーザー入力をパターンに | Regex.Escape / re.escape / preg_quote($s, '/') / RegExp.escape |
| ユーザー入力を置換文字列に | $ を $$ に、または MatchEvaluator |
複数の文字のどれかを選ぶ [abc] は文字集合、繰り返しの回数は量指定子に続きます。
確認環境:macOS(Apple Silicon Mac)、.NET 10.0.14、Python 3.9.6・3.12.14、PHP 8.5.6(PCRE2 10.47)、Dart 3.11.5、2026年11月。
よくある質問
正規表現は既定で大文字と小文字を区別する?
区別します。試した4つのエンジン(.NET・Python・PHP・Dart)すべてで、パターン text は Text にマッチしませんでした。.NETは RegexOptions.IgnoreCase、Pythonは re.I、PHPは /i、Dartは caseSensitive: false が必要です。2021年の記事は小文字にしても見つかると書いていましたが、大文字小文字を無視する設定のツールで見た結果でした。
トルコ語環境では IgnoreCase の結果が変わる?
変わります。.NET 10で CurrentCulture を tr-TR にすると、Regex.IsMatch("FILE", "file", RegexOptions.IgnoreCase) は False でした。RegexOptions.CultureInvariant を加えると True になりました。識別子・拡張子・プロトコルのキーワードのように言語に依存しない値には CultureInvariant を使います。
ユーザーが入力した文字列をパターンに入れるには?
必ずエスケープします。.NETは Regex.Escape、Pythonは re.escape、PHPは preg_quote(第2引数に区切り文字)、Dartは RegExp.escape です。エスケープしない C++ は .NET で Nested quantifier '+' エラーになりました。