正規表現とは?得意・不得意と.NETでの速度実測
正規表現が得意なこと・苦手なこと、HTMLやJSONを正規表現で解析すると壊れる実例、バックトラッキングとRE2系エンジンの違い、ContainsとRegex・GeneratedRegexの速度を.NET 10で実測。
2026年11月改訂: 2021年に韓国語で書いたごく短い紹介記事(約1.6KB)を、実務向けの入門記事として日本語で書き直しました。元記事の例を .NET 10(System.Text.RegularExpressions)、Python 3.9・3.12 の
re、PHP 8.5(PCRE2 10.47)、Dart 3.11(RegExpはJavaScriptと同じ文法)で実行し直しています。元記事は2つ目のマッチ位置を21としていましたが、これは改行がCRLFのときだけの値で、LFなら20です。以下の数値はすべて実測です。
この記事は、正規表現をこれから学ぶ人と、「この処理に正規表現を使ってよいのか」を判断したい人に向けたものです。先に結論を書くと、正規表現は文字列の「形」で探すのが得意で、固定文字列の検索では文字列メソッドより遅く、HTMLやJSONのような入れ子の構造は扱えません。また、エンジンごとに文法や挙動が少しずつ違うので、実際に動かすエンジンで確かめる必要があります。
正規表現は「文字列の形」を書く小さな言語
正規表現(Regular Expression、Regex)は、探したい文字列の形を書くための小さな言語です。.・*・+・?・[ ] のような特別な意味を持つメタ文字と普通の文字を組み合わせてパターンを作り、エンジンがそれに合う部分を見つけます。2021年の記事の例をそのまま実行しました。
パターン: https?://[\w./]+
テキスト: http://www.abc.com/
https://www.abc.net/
ftp://www.abc.org/
irc://www.abc.org/
s? は「sがあってもなくてもよい」、[\w./]+ は「単語文字・ドット・スラッシュが1文字以上」という意味です。4つのエンジンの結果は同じでした。
0 : http://www.abc.com/
20 : https://www.abc.net/ ← 改行がLF(\n)のとき
21 : https://www.abc.net/ ← 改行がCRLF(\r\n)のとき
1行目の http://www.abc.com/ は19文字なので、2行目はLFなら20、WindowsのCRLFなら21から始まります。元記事の 21 はCRLFのテキストで試した結果です。マッチ位置を保存したり比較したりするコードでは、この差がそのままバグになります。
正規表現が向いているのは次のような処理です。
正規表現が苦手なこと:HTMLとJSON
正規表現は、括弧の対応のように任意の深さで入れ子になる構造を数えられません。HTMLの div の中身を取り出してみます。
パターン: <div>(.*?)</div>
テキスト: <div>a<div>b</div>c</div>
結果: <div>a<div>b</div> ← 外側のdivの中身ではない
最短一致の .*? は最初に現れた </div> で止まるので、内側のdivの閉じタグで終わりました。最長一致の .* に変えると、今度は <div>a</div><div>b</div> のような兄弟要素をひとつにまとめてしまいました(4エンジンとも同じ)。どちらも「対応する閉じタグ」は見つけられません。
JSONはもっと静かに壊れます。原因は文字列の中のエスケープです。
パターン: "name":\s*"([^"]*)"
テキスト: {"name": "Kim \"K\" Lee", "age": 30}
グループ1: Kim \ ← 本当の値は Kim "K" Lee
[^"]* は \" がエスケープだとは知らないので、最初の引用符で止まります。エラーにはならず、間違った値が黙って返るのがかえって危険です。構造のあるデータはHTMLパーサーやJSONパーサーで読み、正規表現はパーサーが取り出した値ひとつを検査するのに使うのが安全です。
エンジンは2系統:バックトラッキングと線形時間
同じ「正規表現」でも、エンジンは大きく2つの系統に分かれます。
| 系統 | 代表例 | 長所 | 弱点 |
|---|---|---|---|
| バックトラッキング | .NETの既定、Pythonの re、PHP(PCRE2)、Perl、Java、JavaScript |
後方参照・先読みなど機能が豊富 | パターンによっては時間が指数的に増える |
| 線形時間(オートマトン) | Google RE2、Goの regexp、Rustの regex、.NETの RegexOptions.NonBacktracking |
入力の長さに比例する時間を保証 | 後方参照などが使えない |
違いを実際に確かめました。有名な悪いパターン ^(a+)+$ に、a をn個並べて最後に ! を付けた入力(絶対にマッチしない)を与えます。.NET 10、Releaseビルド、Apple Silicon Mac、タイムアウトは3秒です。
| n | バックトラッキング(既定) | NonBacktracking |
|---|---|---|
| 16 | 6.52 ms | 0.00 ms |
| 20 | 104.25 ms | 0.01 ms |
| 22 | 415.09 ms | 0.01 ms |
| 24 | 1,906.75 ms | 0.04 ms |
| 26 | タイムアウト | 0.01 ms |
| 10,000 | タイムアウト | 1.45 ms |
バックトラッキング型は a の分け方をすべて試すので、2文字増えるごとに約4倍になりました。NonBacktrackingは1万文字でも1.45msです。ただし、後方参照を含むパターンをNonBacktrackingで作ると NotSupportedException が発生しました。ユーザーが入力したパターンや大きな入力を扱うなら、線形時間エンジンを使うか、バックトラッキング型に matchTimeout を必ず指定してください。
実測:正規表現が文字列メソッドより遅いケース
「ログからERRORを含む行を数える」のような固定文字列の検索に正規表現を使うコードをよく見かけます。平均89文字のログ1万行(1%がERROR)を生成し、.NET 10のReleaseビルド、Apple Silicon Macで Stopwatch を使って計測しました。1万行×200回の計測を7回行い、中央値を記載しています。
lines.Count(l => l.Contains("ERROR")); // 文字列メソッド(序数比較)
lines.Count(l => Regex.IsMatch(l, "ERROR")); // 静的メソッド(内部キャッシュを使う)
var compiled = new Regex("ERROR", RegexOptions.Compiled);
lines.Count(l => P.Err().IsMatch(l)); // [GeneratedRegex("ERROR")]
| 方法 | 1行あたり(中央値) |
|---|---|
Contains("ERROR") |
12.6 ns |
Contains("error", OrdinalIgnoreCase) |
12.2 ns |
Regex.IsMatch(l, "ERROR")(静的) |
27.6 ns |
new Regex("ERROR")(インタープリター) |
27.3 ns |
RegexOptions.Compiled |
24.8 ns |
[GeneratedRegex] ソースジェネレーター |
27.1 ns |
RegexOptions.NonBacktracking |
33.7 ns |
ループ内で毎回 new Regex(...) |
352.1 ns |
2回目の実行でも 12.5 / 27.8 / 26.5 / 24.6 / 28.0 / 33.3 / 368.3ns とほぼ同じでした。正規表現はどの方式でも Contains の2倍以上かかり、行ごとに Regex を作るコードは28倍遅くなりました。
接頭辞の判定では差がもっと開きました。StartsWith("2026-11-11T09:3", StringComparison.Ordinal) は5.4ns、正規表現 ^2026-11-11T09:3 はインタープリター46.1ns、Compiled 20.9ns、ソースジェネレーター21.5nsでした。意外だったのは比較方法を指定しない StartsWith(string) で、521.3ns かかりました。引数ひとつの StartsWith は現在のカルチャの規則で比較するからです。完全一致のつもりなら StringComparison.Ordinal を必ず指定しましょう。
生成コストも測りました。プロセスで最初に作る Regex はインタープリター1,093µs、Compiled 5,888µs、NonBacktracking 11,751µsで、同じパターンを再度作るとインタープリター0.6µs、Compiled 6.2µsでした。Compiledは実行時にILを生成するため初回が重く、[GeneratedRegex] はそのコードをビルド時に生成するので実行時の生成コストがありません。パターンがコードに固定されているなら、.NET 7以降はソースジェネレーターを標準にしてよいでしょう。
Pythonも同じ傾向でした。同じ1万行で "ERROR" in line は1行あたり47.7ns、事前コンパイルした rx.search(line) は99.1ns、毎回 re.search("ERROR", line) を呼ぶと363.6ns(Python 3.9.6。3.12.14では46.8 / 105.8 / 257.9ns)。この値にはループのオーバーヘッドも含まれます。
エンジンごとに違う:このシリーズの読み方
2021年の講座はVB.NETの正規表現が基準でした。改訂版では.NET 10を中心に、Python・PHP・JavaScript(Dartで確認)の結果も併記し、実際に確かめた違いだけを書きます。この記事を書きながら見つけた違いを先に紹介します。
| パターンと入力 | .NET 10 | Python 3.9 / 3.12 | PHP 8.5 | Dart |
|---|---|---|---|---|
C++ |
エラー | 3.9はエラー、3.12は C |
C |
エラー |
\d+ と 123(全角数字) |
マッチ | マッチ | マッチ(/u) |
不一致 |
^\d+$ と "123\n" |
マッチ | マッチ | マッチ | 不一致 |
Python 3.12とPHPは C++ の2つ目の + を強欲(possessive)量指定子と解釈して C にマッチし、.NETは Nested quantifier '+' という構文エラーを出しました。全角数字は日本語の入力フォームで実際に問題になります。残りの2行はメタ文字の記事で詳しく扱います。
シリーズの順番は、単純な文字列検索 → メタ文字 → 文字集合 → 量指定子 → 繰り返し文字 → サブ式 → 後方参照 → 先読み・後読み です。
確認環境: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月。
よくある質問
固定の文字列を探すときも正規表現を使うべき?
いいえ。.NET 10でログ1万行を調べたところ、Contains("ERROR")は1行あたり12.6ns、Regex.IsMatchは27.6ns、ソースジェネレーター([GeneratedRegex])でも27.1nsでした。固定文字列なら序数(Ordinal)比較の文字列メソッドが最も速く、意図も明確です。
HTMLやJSONを正規表現で解析してはいけないのはなぜ?
入れ子とエスケープで壊れるからです。<div>(.*?)</div> を <div>a<div>b</div>c</div> に適用すると、試した4つのエンジンすべてが <div>a<div>b</div> を返しました。JSONの "Kim \"K\" Lee" では Kim \ までしか取れませんでした。構造のあるデータはパーサーで扱います。
RegexOptions.Compiled と [GeneratedRegex] はどちらを使う?
照合速度はほぼ同じでした(24.8nsと27.1ns)。違いは生成コストで、Compiledは最初の生成に約6msかかり、ソースジェネレーターはビルド時にコードを作るので実行時のコストがありません。パターンがコンパイル時に決まっているなら [GeneratedRegex] が向いています。