正規表現とは?

正規表現(Regular Expression / Regex) は、文字列のパターンを定義する文字の並びです。特定の構造に一致するテキストの検索・抽出・置換・検証に使われます。JavaScript、Python、Java、Ruby、Go、Rust など、ほぼすべての言語と、grepsed などのコマンドラインツールで利用できます。

エンジニアのための正規表現入門:メタ文字・グループ・先読み・ReDoS

正規表現を使うと、次のような問いに答えられます。

  • この文字列に有効なメールアドレスは含まれているか?
  • このHTMLからすべてのURLを抽出する
  • すべてのISO日付文字列を別の形式に置換する
  • この電話番号は特定のパターンに従っているか?

記法は最初は難解に見えますが、一貫したルールに従っています。構成要素を理解すれば、自信を持って読み書きできるようになります。

メタ文字:構成要素

メタ文字は、正規表現の中で特別な意味を持つ文字です。

文字にマッチする

記号意味マッチ例
.改行以外の任意の1文字a.c”abc”, “aXc”, “a1c”
\d数字(0-9)\d\d”42”, “00”
\D数字以外\D+”abc”
\w単語構成文字 [a-zA-Z0-9_]\w+”hello”, “user_1”
\s空白(スペース・タブ・改行)a\sb”a b”
\S空白以外\S+”hello”

アンカー

記号意味
^行/文字列の先頭
$行/文字列の末尾
\b単語境界
/^hello/.test('hello world')  // true  — "hello"で始まる
/^hello/.test('say hello')    // false — 先頭ではない
/\bcat\b/.test('cat')         // true
/\bcat\b/.test('catch')       // false — 単語境界ではない

量指定子(クオンティファイア)

記号意味貪欲?
*0回以上はい
+1回以上はい
?0回または1回はい
{n}ちょうどn回
{n,m}n回以上m回以下はい
*? +? ??非貪欲版いいえ

貪欲 vs 非貪欲:

const html = '<b>bold</b> and <i>italic</i>';

// 貪欲 — できるだけ長くマッチ
html.match(/<.+>/)[0]   // '<b>bold</b> and <i>italic</i>'

// 非貪欲 — できるだけ短くマッチ
html.match(/<.+?>/)[0]  // '<b>'

文字クラス

文字クラス [...] は、集合内のいずれか1文字にマッチします。

[aeiou]       — 母音のいずれか
[a-z]         — 小文字のいずれか
[A-Z0-9]      — 大文字または数字
[^aeiou]      — 母音「以外」(否定)

[...] の中の ^ は否定を意味します。[...] の外では先頭へのアンカーになります。

グループとキャプチャ

キャプチャグループ (...)

括弧はキャプチャグループを作り、マッチしたテキストを後で取り出せます。

const date = '2026-01-14';
const match = date.match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(match[1]); // '2026' — 年
console.log(match[2]); // '01'   — 月
console.log(match[3]); // '14'   — 日

名前付きキャプチャグループ (?<name>...)

名前を付けると自己説明的になります。

const m = '2026-01-14'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
console.log(m.groups.year);  // '2026'
console.log(m.groups.month); // '01'

非キャプチャグループ (?:...)

選択や量指定子のためにグループ化したいが、キャプチャは不要な場合に使います。

/colou?r/  // 'color' と 'colour' の両方にマッチ

選択(OR)|

パイプ | は選択肢のORとして働きます。

/(jpg|jpeg|png|gif)$/.test('photo.jpeg') // true

先読みと後読み(Lookaround)

Lookaround を使うと、別のパターンに前後されている(いない)場合だけマッチさせ、その部分自体はマッチに含めない、ということができます。

記法名称意味
(?=...)肯定先読み…が後ろに続く
(?!...)否定先読み…が後ろに続かない
(?<=...)肯定後読み…が前にある
(?<!...)否定後読み…が前にない
// 肯定先読み:" USD"が後ろに続く場合だけ "100" にマッチ
'100 USD'.match(/\d+(?= USD)/)[0]    // '100'
'100 EUR'.match(/\d+(?= USD)/)       // null

// 肯定後読み:"$"が前にある数字にマッチ
'Price: $42'.match(/(?<=\$)\d+/)[0]  // '42'

実践的なパターン

メールアドレス(簡易版)

^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$

注:RFC 5321に完全準拠したメール正規表現は非常に複雑です。フォーム入力の検証なら上記の簡易版で十分です。メールの有効性は最終的に確認メールの送信で担保しましょう。

ISO 8601 の日付(YYYY-MM-DD)

^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$

日本の郵便番号

^\d{3}-\d{4}$

IPv4 アドレス

^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$

フラグ(修飾子)

フラグ意味
gグローバル — 最初の1件だけでなく全件を検索
i大文字小文字を区別しない
mマルチライン — ^/$ が各行の先頭・末尾にマッチ
sdotall — . が改行にもマッチ
uUnicode — パターンと文字列をUnicodeとして扱う
'Hello WORLD'.match(/hello/i)[0] // 'Hello'
'a\nb\nc'.match(/^\w/gm)         // ['a', 'b', 'c']

ReDoS:致命的なバックトラッキング

ReDoS(正規表現によるサービス拒否攻撃) は、悪意を持って作られた入力により、正規表現が想定より指数関数的に長く実行されてしまう脆弱性です。

典型的な危険パターンは、入れ子になった量指定子です。

^(a+)+$     — "aaaaaaaaaaaaaaab" にマッチさせようとすると爆発的に遅くなる

正規表現エンジンは「マッチしない」と結論づける前に、a のあらゆるグループ化を試します。長さ n の文字列に対して O(2ⁿ) の時間がかかることがあります。

なぜ問題なのか

アプリがユーザー入力を正規表現に渡す場合、悪意あるユーザーは1つの巧妙な文字列でサーバーをハングさせたり、CPUを過剰消費させたりできます。

ReDoSを防ぐには

  1. 入れ子の量指定子を避ける: (a+)+(a*)* などを書かない。
  2. アトミックグループや絶対最大量指定子を、エンジンが対応していれば使う。
  3. タイムアウトを設定する: 信頼できない入力に対して実行時間を制限する。
  4. 長い入力でテストする: マッチしないはずの長さ20〜50の文字列で試す。
  5. 静的解析ツールを使う: safe-regex(npm)などで危険なパターンを検出する。
// 危険
/^(a+)+$/.test('aaaaaaaaaaaaaaab'); // ハングする可能性

// 安全な代替 — 入れ子の量指定子なし
/^a+$/.test('aaaaaaaaaaaaaaab');    // 高速

DevToolKitsでテストする

頭の中で考えた正規表現が正しく動くか確認するのは大変です。正規表現テストツール を使えば、入力テキストに対してマッチ箇所をリアルタイムでハイライトし、キャプチャグループの内容を一覧表示し、フラグ(g, i, m)の切り替えによる挙動の変化を確認できます。

よくある質問

JavaScriptの matchexec の違いは?

String.prototype.match() は結果の配列を返します。g フラグ付きなら全マッチを返し、なしなら exec() と同じ動作になります。exec()g フラグ使用時のループで便利で、lastIndex で最後のマッチ位置を記憶します。

正規表現で特殊文字をエスケープするには?

. * + ? ( ) [ ] { } ^ $ | \ などの特殊文字は、リテラルとしてマッチさせるにはバックスラッシュでエスケープします。

function escapeRegex(str) {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

HTMLやJSONのパースに正規表現は使える?

基本的に使うべきではありません。HTMLやJSONは正規表現では正しく扱えない再帰構造を持ちます。HTMLには DOMParser、JSONには JSON.parse() など、専用のパーサーを使いましょう。正規表現は、パース済みのテキストフィールド内の単純なパターン抽出に向いています。

y(sticky)フラグは何をする?

y フラグは lastIndex の位置からのみマッチを試みます。g と違い、マッチを探して文字列を進むことはせず、現在位置でマッチするか失敗するかのどちらかです。