- はじめに — 正規表現は小さな言語
- リテラルとメタ文字
- 文字クラス — 「このどれか」
- 量指定子 — 「何回繰り返す?」
- アンカーと境界 — 「どこで?」
- グループとキャプチャ — 束ねて覚える
- 選択 — 「これまたはあれ」
- 先読み・後読み — 消費せずに覗く
- 貪欲 vs 控えめ — マッチの食欲
- 破滅的バックトラッキングとReDoS
- 正規表現を使ってはいけない瞬間
- 実務のヒントいくつか
- おわりに
- 参考資料
はじめに — 正規表現は小さな言語
正規表現(regular expression、regex)を初めて見ると、まるで猫がキーボードの上を歩いた結果のように見えます。ですが正規表現はランダムではなく、文字列のパターンを記述するための、とても小さく精密な言語です。いくつかの構成要素を理解すれば、怖かった記号たちが文のように読め始めます。
この記事は正規表現を土台から積み上げます。構成要素を一つずつ身につけ、実務で本当に足をすくうパフォーマンスの落とし穴まで扱います。一つルールを先に言っておくと、この記事のすべての正規表現パターンはインラインコードかコードブロックの中に入れてあります。正規表現には波括弧や不等号のような特殊文字が多く、そのまま文中に置くとレンダリングが壊れることがあるからです。あなたがコードに正規表現を書くときも、この習慣はそのまま役立ちます。
パターンを自分で作りながら覚えたければ、記事を読む間ずっとこのサイトの正規表現テスターにパターンを貼り付けて、リアルタイムで確認してみてください。
リテラルとメタ文字
もっとも単純な正規表現は、探したい文字そのものです。パターン cat は文字列の中で「cat」という連続した三文字を探します。こうした普通の文字を**リテラル(literal)**と呼びます。
正規表現が強力になるのは**メタ文字(metacharacter)**のおかげです。これらは文字そのものではなく特別な意味を持ちます。代表的なメタ文字は次のとおりです。
. ^ $ * + ? ( ) [ ] { } | \
これらの文字を「そのまま」探したいときは、前にバックスラッシュを付けてエスケープします。たとえば本物のピリオドを探すには、任意の一文字を表すメタ文字ではなく \. と書きます。ドメインのピリオドを探すパターンは example\.com のようになります。
文字クラス — 「このどれか」
**文字クラス(character class)**は角括弧で囲み、「この中にある文字のどれか一つ」にマッチします。
[aeiou]は母音一文字にマッチします。[a-z]は小文字一文字にマッチします。ハイフンは範囲を表します。[a-zA-Z0-9]は英数字一文字にマッチします。[^0-9]のように角括弧内の先頭にキャレットを置くと否定になり、数字でない一文字にマッチします。
よく使う文字クラスには短い省略形があります。
\dは数字、\Dは数字でない文字。\wは単語文字(英字、数字、アンダースコア)、\Wはその反対。\sは空白文字(スペース、タブ、改行など)、\Sはその反対。.は(既定設定で)改行を除く任意の一文字。
たとえば数字三つは \d\d\d と表せますが、次に学ぶ量指定子を使えばもっと短くなります。
量指定子 — 「何回繰り返す?」
**量指定子(quantifier)**は、直前の要素が何回繰り返されるかを表します。
*は0回以上。(あってもなくてもよい)+は1回以上。(最低一回)?は0回または1回。(任意){n}はちょうどn回。{n,}はn回以上。{n,m}はn回以上m回以下。
先ほどの数字三つは \d{3} と簡潔になります。電話番号の市内局番のように3から4桁の数字が欲しければ \d{3,4} と書きます。一つ以上の単語文字は \w+、あってもなくてもよいプロトコルの s は https?(つまり http または https)で表します。
アンカーと境界 — 「どこで?」
ここまでの要素が「何を」探すかだったのに対し、**アンカー(anchor)**は「どこで」探すかを固定します。アンカーは文字にマッチするのではなく、位置にマッチします。
^は文字列(または行)の先頭。$は文字列(または行)の末尾。\bは単語境界。単語文字と非単語文字の間の切れ目です。\Bは単語境界でない位置。
たとえば ^\d+$ は「先頭から末尾まで数字だけでできた文字列」にマッチします。アンカーがなければ \d+ は「abc123def」の中の「123」にもマッチしますが、前後に開始アンカー ^ と終端アンカーを付ければ文字列全体が数字でなければマッチしません。入力検証において、この違いは決定的です。
単語境界 \b も便利です。\bcat\b は独立した単語「cat」にだけマッチし、「category」や「concatenate」の中の「cat」にはマッチしません。
グループとキャプチャ — 束ねて覚える
丸括弧は複数の要素を一つにまとめて**グループ(group)を作ります。グループは二つの仕事をします。量指定子を複数の文字にまとめて適用し、マッチした部分を後で取り出せるようにキャプチャ(capture)**します。
(ab)+は「ab」が一回以上繰り返されるもの(「ababab」など)にマッチします。グループがなければab+は「abbbb」にマッチし、意味がまったく変わります。- 日付を切り出すパターン
(\d{4})-(\d{2})-(\d{2})は年・月・日をそれぞれグループ1、2、3としてキャプチャします。プログラムからこのキャプチャグループを取り出して使えます。
キャプチャは不要で束ねるだけしたいときは非キャプチャグループを使います。(?:...) の形です。たとえば (?:https?://)? はプロトコル部分を任意で束ねますがキャプチャはしません。不要なキャプチャを減らすとパターンの意図が明確になり、わずかなパフォーマンス上の利点もあります。
多くの言語は名前付きグループもサポートします。(?<year>\d{4}) のように書けば、数字のインデックスではなく名前で取り出せて可読性が上がります。
選択 — 「これまたはあれ」
パイプ記号は**選択(alternation)**を表します。「左側または右側」です。
cat|dogは「cat」または「dog」にマッチします。- 選択の範囲を制限するにはグループで囲みます。次の二つを比べてみましょう。
^(cat|dog)$ → 文字列全体が「cat」または「dog」
^cat|dog$ → 「^cat」または「dog$」と解釈される(意図と違う)
グループで囲んだ最初の形だけが「文字列全体が cat または dog」を意味します。グループがないと選択記号の範囲がパターン全体に広がり、まったく異なる結果になります。
選択肢が複数あれば並べます。(jpg|jpeg|png|gif) のように。このとき順序とアンカーに注意すれば、欲しい部分だけを正確に捉えられます。
先読み・後読み — 消費せずに覗く
先読み・後読み(lookaround)は少し高度な道具です。特定のパターンが前や後ろにあるかを確認しますが、その部分はマッチ結果に含め(消費し)ません。四つあります。
- 肯定先読み(lookahead)
(?=...): 後ろにこれが来るなら。 - 否定先読み
(?!...): 後ろにこれが来ないなら。 - 肯定後読み(lookbehind)
(?<=...): 前にこれがあったなら。 - 否定後読み
(?<!...): 前にこれがなかったなら。
実用的な例を見ましょう。数字に三桁区切りのカンマを入れる位置を探すとき、「後ろに三の倍数の桁が残っている位置」を先読みで探します。またパスワード規則の検証で「数字を最低一つ含む」を要求するとき (?=.*\d) を使います。この断片は実際には何の文字も消費せず、「どこかに数字がある」という条件だけを検査します。複数の条件を重ねて ^(?=.*[a-z])(?=.*\d).{8,}$ のように書けば、「小文字を含む、数字を含む、8文字以上」を一度に検証できます。
貪欲 vs 控えめ — マッチの食欲
量指定子には隠れた性格があります。既定では量指定子は**貪欲(greedy)**です。つまり可能な限り多く食べようとします。この性質はしばしば落とし穴になります。
HTMLタグを捉えようと <.+> というパターンを使ったとします。"<b>bold</b>" という文字列で、あなたはおそらく <b> だけがマッチすることを期待するでしょうが、貪欲な .+ は最大限飲み込んで <b>bold</b> 全体を捉えてしまいます。最初の不等号から最後の不等号まで丸ごと食べたのです。
解決策は量指定子を**控えめ(lazy)**にすることです。量指定子の後ろに ? を付けます。<.+?> は「できる限り少なく」食べるので <b> だけを捉えます。まとめるとこうです。
*、+、?、{n,m}は貪欲。最大限多く。*?、+?、??、{n,m}?は控えめ。最小限だけ。
なお、この例でより良い方法は否定文字クラスを使うことです。<[^>]+> は「不等号でない文字」だけを食べるので、そもそも閉じ不等号を越えません。このようにバックトラッキング自体を減らす設計が、次に見るパフォーマンス問題を予防する鍵です。
破滅的バックトラッキングとReDoS
正規表現エンジンの多くは、マッチに失敗すると後ろに戻って別の場合を試す**バックトラッキング(backtracking)方式で動きます。ほとんどは問題ありませんが、パターンを誤って組むと、試すべき場合の数が入力の長さに応じて指数的に爆発します。これが破滅的バックトラッキング(catastrophic backtracking)**です。
典型的な危険パターンは、繰り返しの中に繰り返しが重なり、その境界が曖昧なときに生じます。たとえば (a+)+$ のようなパターンに「aaaaaaaaaaX」のように末尾でマッチが失敗する入力を与えると、エンジンは a たちを内側グループと外側グループに振り分ける無数の組み合わせをすべて試して、事実上停止してしまいます。入力が数文字長くなるだけで時間が爆発的に増えます。
この脆弱性を悪用してサービスを麻痺させる攻撃が**ReDoS(Regular expression Denial of Service)**です。攻撃者が悪意を持って設計した入力一つで、サーバーのCPUを100%に縛りつけられます。実際に有名なライブラリでReDoS脆弱性が繰り返し発見されてきました。
防御法は次のとおりです。
- ネストした量指定子を避ける:
(a+)+や(a*)*のように繰り返しの中の繰り返しが曖昧に重なる構造を警戒します。 - できる限り具体的に: 広範な
.の代わりに[^>]のような狭い文字クラスを使えば、バックトラッキングの余地が減ります。 - アンカーで固定:
^と$でマッチ範囲を束ねれば、エンジンがさまよう余地が減ります。 - 線形時間エンジンの検討: RE2(グーグル)やRustのregexクレートのように、バックトラッキングを使わず常に線形時間を保証するエンジンを使えば、ReDoS自体が不可能になります。
- 信頼できない入力にタイムアウト: 言語やライブラリが提供するなら、正規表現の実行にタイムアウトをかけます。
正規表現を使ってはいけない瞬間
正規表現は強力ですが万能ではありません。もっとも有名な反例はHTML(またはXML)のパースです。HTMLはネストした再帰的な構造ですが、伝統的な正規表現はこうした任意の深さのネストを根本的に表現できません。正規表現でHTMLを無理やりパースしようとする試みは、伝説的なStack Overflowの回答で激しく警告されており、実務でもあらゆるエッジケースで崩れます。HTMLは専用パーサ(DOMパーサ)で扱うべきです。
正規表現が不適切な他のサインもあります。
- ネストした括弧や構造の釣り合いを取る必要があるとき: 開き括弧と閉じ括弧の対を任意の深さで合わせるのは正規表現の領域ではありません。
- パターンがコードより理解しにくくなるとき: 読むのに5分かかる100文字の正規表現より、数行の明示的な文字列処理コードのほうが良いことが多いです。
- すでにパーサがある形式: JSON、CSV、URL、日付などは、たいてい実績のある専用パーサライブラリがあります。自分で正規表現を組む前に、まずそれを探してください。
正規表現は「トークン単位の局所的なパターンマッチ」でもっとも輝きます。メールの形式のおおまかな検証、ログ行からのフィールド抽出、検索と置換といった作業が正規表現のホームグラウンドです。
実務のヒントいくつか
最後に、正規表現を実際に使うときに役立つ習慣です。
- コメントと拡張モード: 多くの言語が
xフラグ(拡張モード)をサポートし、正規表現の中に空白とコメントを入れて複数行に展開できます。複雑なパターンほどこう展開すると保守が楽になります。 - フラグを理解する: 大文字小文字を無視(
i)、複数行モード(m、^と$が各行に適用)、ドットが改行も含む(s)といったフラグは結果を大きく変えます。 - あらかじめコンパイル: 同じパターンを繰り返し使うなら、ループの外で一度コンパイルして再利用するほうがパフォーマンスに良いです。
- テストを添える: 正規表現は微妙です。代表的な入力とエッジケースでテストを作っておけば、後でパターンを直すとき安全です。
学んだことを固める一番の方法は問題を解くことです。このサイトの正規表現クイズで概念を一つずつ点検し、正規表現テスターで自分だけのパターンを実験してみてください。
おわりに
正規表現は一見暗号のようですが、結局はいくつかの構成要素の組み合わせです。リテラルと文字クラスで「何を」、量指定子で「何回」、アンカーで「どこで」を決め、グループと選択と先読みで構造を細かく仕上げます。ここに貪欲と控えめの違い、そして破滅的バックトラッキングという落とし穴を知れば、あなたはすでにほとんどの実務状況を自信を持って扱えます。
もっとも大切な教訓は節度です。正規表現は局所的なパターンマッチに使い、ネストした構造のパースには使わないこと。その線を守れば、正規表現は危険な呪文ではなく頼れる道具になります。
参考資料
- MDN: Regular expressions guide — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions
- regular-expressions.info — https://www.regular-expressions.info/
- OWASP: Regular expression Denial of Service (ReDoS) — https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- Google RE2 engine — https://github.com/google/re2
- Rust regex crate (linear-time guarantee) — https://docs.rs/regex/
현재 단락 (1/85)
正規表現(regular expression、regex)を初めて見ると、まるで猫がキーボードの上を歩いた結果のように見えます。ですが正規表現はランダムではなく、文字列のパターンを記述するための、と...