正規表現は文字列のパターンを表現するもの
JavaScriptの正規表現は、文字列の「かたち」を記述するためのものです。たとえば「数字4桁」「単語の後にカンマ」「メールアドレスっぽい文字列」といった具合ですね。JavaScriptで正規表現を作る方法は、大きく分けて2つあります。
よく使うのはスラッシュで挟むリテラル記法で、フラグは閉じスラッシュの後ろに書きます。パターン自体をユーザー入力や変数から動的に組み立てたいときだけ、new RegExp(...) の出番です。
末尾の i は フラグ と呼ばれるもので、大文字・小文字を区別しないという意味です。フラグについては後ほど詳しく説明します。
test: マッチするかどうかを判定する
正規表現に対して一番シンプルに聞けるのは「この文字列にマッチする部分があるか?」ということ。これを調べるのが test です。
\d は「任意の数字」を表します。test は true か false しか返しません。入力チェックや配列のフィルタリングのように、マッチしたかどうかだけ知りたい場面ではまさにうってつけです。
match: マッチした文字列を取り出す
マッチした文字列自体が欲しいときは、文字列側の match メソッドを使います。
gフラグを付けない場合、matchは最初にマッチした結果に加えてindexやinputといったメタ情報を含む配列を返します。一方、gフラグを付けると、マッチしたすべての文字列を普通の配列として返します。ただし、何もマッチしなかったときは空配列ではなくnullが返ってくるので、その点は忘れずにガードしておきましょう:
フラグでマッチの挙動を変える
フラグは閉じスラッシュの後ろに付けて、マッチの振る舞いを調整します。よく使うものはこのあたり:
g— グローバル。最初の1件だけでなく、すべてのマッチを探します。i— 大文字・小文字を区別しません。m— マルチラインモード。^と$が文字列全体ではなく、各行の先頭・末尾にマッチするようになります。s— dotall。.が改行文字にもマッチするようになります。u— Unicode 対応。多くの絵文字や非 ASCII 文字のパターンで必要になります。
m フラグがない場合、^ は文字列全体の先頭にしかマッチしません。m を付けると各行の先頭にアンカーされるので、Roses と Violets の両方が拾われます。
文字クラスと量指定子
ほとんどのパターンはこれらの部品で組み立てます。
\dは数字、\wは単語構成文字(英数字とアンダースコア)、\sは空白文字。[abc]は a、b、c のいずれか。[^abc]はそれ 以外 。[a-z]は範囲指定。.は改行を除く任意の 1 文字。*は 0 回以上、+は 1 回以上、?は 0 回または 1 回。{3}はちょうど 3 回、{2,5}は 2〜5 回、{2,}は 2 回以上。^は先頭、$は末尾。
組み合わせるとこんな感じです。
\b は単語境界を表すメタ文字で、単語構成文字と非単語構成文字の境目にある「見えない区切り」のことです。単語単位でマッチさせたいときに重宝します。
キャプチャグループ:マッチした一部を取り出す
丸括弧で囲むと グループ ができ、そこにマッチした部分が記憶されます。exec や match は、マッチ全体と一緒にこのキャプチャ結果も返してくれます。
インデックス 0 にはマッチ全体が入り、その後ろに各グループが順番に並びます。ただ、グループが3つ以上になると番号で数えるのが途端に面倒になるので、名前を付けてしまいましょう。
名前付きキャプチャグループを使うと、呼び出し側のコードが読みやすくなりますし、パターン内の順序を入れ替えても壊れません。
replace:マッチした部分を書き換える
replace はパターンと置換内容を受け取ります。置換内容には文字列だけでなく関数も渡せます。
g フラグを付けない場合、最初にマッチした箇所しか置換されません。フラグを付け忘れて、2つ目のメールアドレスがまだ変なままだ…と悩むのはよくあるハマりどころです。
置換文字列では後方参照が使えます。$1 や $2 などはキャプチャグループを、$<name> は名前付きキャプチャグループを参照します。
単純な置換で済まない場合は、関数を渡しましょう。マッチした文字列とキャプチャが引数として渡ってきます。
アンダースコアはマッチ全体(今回は使わない部分)で、n が1つ目のキャプチャグループです。この「正規表現+置換関数」の組み合わせは、実務でのテキスト加工のほとんどをこなせる万能パターンです。
matchAll:すべてのマッチをキャプチャグループごと取得する
String.prototype.matchAll は、各マッチを キャプチャグループごと イテレータで返してくれます。g フラグ付きの match では実現できない動きです。
matchAll を使うときは g フラグが必須です。付け忘れると TypeError になるので注意してください。イテレーションではなくインデックスアクセスしたい場合は、[...text.matchAll(email)] のようにスプレッドして配列にしておくと便利です。
特殊文字のエスケープ
. * + ? ( ) [ ] { } | \ ^ $ といった文字は、正規表現の中では特別な意味を持ちます。これらをそのままの文字として一致させたいときは、バックスラッシュでエスケープします。
エスケープしていない方は examplexcom にもマッチしてしまいます。. は「任意の 1 文字」を表すからです。この手のバグは意外と多く、しかもエラーが出ないので気付きにくい。正規表現が想定より広くマッチしているときは、まずエスケープ漏れの . を疑ってみてください。
ユーザー入力からパターンを組み立てる場合は、必ずエスケープが必要です。そうしないと、正規表現の構文を差し込まれてしまいます。
置換文字列の中で使う $& は「マッチした文字列全体」を表すショートハンドです。
先読み(lookahead)と後読み(lookbehind)
「ある文字列の直後(または直前)にあるときだけマッチさせたいけれど、その部分自体はマッチに含めたくない」という場面ってありますよね。そんなときに活躍するのが lookaround(先読み・後読み)です。
(?= ...)は肯定先読み。「〜が後ろに続く」という意味です。(?<= ...)は肯定後読み。「〜が前にある」という意味です。(?! ...)と(?<! ...)はそれぞれの否定バージョンです。
先読み・後読みは文字を消費しない(マッチ位置を進めない)ので、「覗き見た」部分はパターンの次のステップでもそのまま使えます。
メール検証の正規表現について一言
「メールアドレスをバリデーションする正規表現をください」というのは本当によくある質問です。正直に言うと、やめておいた方がいいです。実際のメールアドレスの文法はかなり複雑で、読める長さに収まる正規表現はどこかしら間違っています。フォームでの簡易チェック程度なら、現実的なパターンで十分です。
こう読んでください。「空白でも @ でもない文字が続き、@ が来て、また同じような文字が続き、ドットが来て、さらに同じような文字が続く」。RFC 5322 を厳密に満たすわけではありませんが、明らかなタイポはこれで十分弾けます。本当に検証したいなら、確認メールを送りましょう。
よくある落とし穴
頭に入れておきたいハマりどころをいくつか挙げておきます。
replaceやmatchAllでgフラグを忘れる。 最初の1件しかマッチしなかったり、TypeErrorになったりします。- グローバル正規表現の
lastIndexが状態を持つ問題。gやyフラグ付きの正規表現は、testやexecの呼び出しをまたいで前回の位置を覚えています。関係のない文字列に同じ正規表現を使い回すのは NG。毎回新しく作るか、matchAllを使いましょう。 - 動的に組み立てたパターンでドットやスラッシュをエスケープし忘れる。 ユーザー入力を
new RegExp(...)に突っ込む前に、必ずエスケープしてください。 - 壊滅的バックトラッキング(catastrophic backtracking)。
(a+)+のようなネストした量指定子は、意地悪な入力でタブを丸ごとフリーズさせることがあります。正規表現が妙に遅いと感じたら、まずはシンプルにリファクタしましょう。
次回: 日付と時刻
正規表現はテキストの「形」を扱う道具ですが、実データにはパースやフォーマット、計算が必要なタイムスタンプも付いてきます。次のページでは Date と Intl.DateTimeFormat、そしてタイムゾーン由来のバグを寄せつけないための考え方を解説します。
よくある質問
JavaScriptで正規表現を作るには?
書き方は2通りあります。スラッシュで囲むリテラル記法 /hello/i と、文字列から生成するコンストラクタ new RegExp('hello', 'i') です。パターンが固定ならリテラル、変数からパターンを組み立てるなど実行時に決まるならコンストラクタ、と使い分けるのが基本です。
test、match、execの違いは?
regex.test(str) は真偽値を返すので、マッチするかどうかだけ知りたいときに一番速いです。str.match(regex) はマッチ結果の配列(なければ null)を返します。regex.exec(str) は1件ずつマッチを返し、キャプチャグループも取れます。g フラグを付けると lastIndex で位置を覚えているので、ループで順番に取り出せます。
正規表現で全件置換するには?
g フラグを付けて str.replace(/foo/g, 'bar') とします。g なしだと最初の1件しか置換されません。str.replaceAll(/foo/g, 'bar') も使えますが、replaceAll に正規表現を渡す場合は g フラグが必須で、付け忘れるとエラーになります。
キャプチャグループって何?
パターン中の括弧がキャプチャグループで、マッチした部分を覚えてくれます。たとえば /(\d{4})-(\d{2})/.exec('2024-11') の結果は、インデックス1が '2024'、インデックス2が '11' になります。(?<year>\d{4}) のように名前付きにすれば、match.groups.year で取り出せて可読性も上がります。