正規表現とは?得意・不得意と.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のテキストで試した結果です。マッチ位置を保存したり比較したりするコードでは、この差がそのままバグになります。

正規表現が向いているのは次のような処理です。

  • 形式チェック:郵便番号 \d{3}-\d{4}、日付 \d{4}-\d{2}-\d{2}、バージョン文字列など形が決まった値。
  • 抽出:ログ1行から時刻・レベル・リクエストIDを取り出す。グループはサブ式の記事で扱います。
  • 置換:日付形式の変換、連続した空白の整理、重複した単語の削除(後方参照)。
  • 条件の組み合わせ:「数字と大文字をそれぞれ1つ以上含む」(先読み・後読み)。

正規表現が苦手なこと: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パーサーで読み、正規表現はパーサーが取り出した値ひとつを検査するのに使うのが安全です。

判断の流れ。テキスト処理から始める。最初の質問は固定文字列か。はいなら序数比較の Contains や StartsWith を使う。いいえなら次の質問は HTML や JSON のような入れ子のデータか。はいならパーサーを使う。いいえなら最後の質問は入力が信頼できないか、非常に大きいか。はいなら RE2 や .NET の NonBacktracking のような線形時間エンジンを使うか、タイムアウトを設定する。いいえなら通常のバックトラッキング型正規表現でよい。
正規表現を書く前の判断。固定文字列なら文字列メソッドが速く、入れ子の形式にはパーサー、信頼できない入力には線形時間エンジンかタイムアウトが必要。

エンジンは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 を必ず指定しましょう。

1行あたりのナノ秒の棒グラフ。StartsWith(序数)5.4、Contains(序数)12.6、接頭辞の正規表現 Compiled 20.9、GeneratedRegex 27.1、Compiled 24.8、インタープリター 27.3、静的 Regex.IsMatch 27.6、NonBacktracking 33.7、接頭辞の正規表現インタープリター 46.1。
平均89文字のログ1万行に対して、固定文字列や接頭辞を判定したときの1行あたりの中央値。.NET 10、Release、Apple Silicon Mac、7回計測の中央値。文字列メソッドはどの正規表現よりも速く、ソースジェネレーターと Compiled はほぼ同じだった。

生成コストも測りました。プロセスで最初に作る 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] が向いています。

  • #正規表現
  • #Regex
  • #.NET
  • #Python
  • #GeneratedRegex

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑