正規表現
文字列のパターンマッチングと置換を行うためのミニ言語
正規表現とは
正規表現 (Regular Expression, Regex) は、文字列のパターンマッチングと置換を行うためのミニ言語である。バリデーション (メールアドレス、電話番号)、テキスト検索、ログ解析で使われる。
基本構文
正規表現の記号は、何に一致するかを決めるもの (. \d [abc])、直前の要素を何回繰り返すかを決めるもの (* + ?)、文字ではなく位置に一致するもの (^ $) の 3 系統に分かれる。この 3 系統の組み合わせでパターンを組み立てる。
| パターン | 意味 | 例 |
|---|---|---|
. | 任意の 1 文字 | a.c → abc, aXc |
* | 0 回以上の繰り返し | ab*c → ac, abc, abbc |
+ | 1 回以上の繰り返し | ab+c → abc, abbc |
? | 0 回または 1 回 | colou?r → color, colour |
\d | 数字 | \d{3} → 123 |
\w | 英数字 + アンダースコア | \w+ → hello_123 |
\s | 空白文字 | \s+ → スペース、タブ |
^ | 行頭 | ^Hello |
$ | 行末 | world$ |
[abc] | a, b, c のいずれか | [aeiou] → 母音 |
(...) | グループ化 + キャプチャ | (\d{4})-(\d{2}) |
TypeScript での使用
JavaScript / TypeScript の正規表現はリテラル (/.../) か new RegExp() で書き、test は真偽値、match は一致部分とキャプチャの配列を返す。g フラグを付けた正規表現オブジェクトは前回の一致位置を内部に保持するため、同じオブジェクトで test を繰り返すと 3 回目に false が返る (/a/g を 'aa' に適用した場合)。使い回さず、その場でリテラルを書くか毎回生成するのが安全である。
// テスト (マッチするか)
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
emailRegex.test('alice@example.com'); // true
// マッチの抽出
const dateStr = '2026-04-01';
const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
// match[1] = '2026', match[2] = '04', match[3] = '01'
// 置換
'Hello World'.replace(/world/i, 'TypeScript'); // 'Hello TypeScript'
// 全置換
'a-b-c'.replaceAll(/-/g, '_'); // 'a_b_c'
Named Capture Groups
(?<name>...) と書くと一致部分に名前が付き、戻り値の groups から取り出せる。番号での参照は括弧を 1 組足すだけで番号がずれるため、パターンが育つ用途では名前付きの方が壊れにくい。なお、直前の文字列を見る後読み (?<=...) は実行環境の対応が遅れた機能で、Safari では 16.4 (2023 年 3 月) で使えるようになった。古い環境も対象に含む場合は、後読みに依存しない書き方を用意しておく。
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const { groups } = '2026-04-01'.match(regex)!;
// groups.year = '2026', groups.month = '04', groups.day = '01'
バリデーションでの注意
メールアドレスのように仕様が複雑な形式を正規表現 1 本で厳密に検証しようとすると、正しい値を弾くか誤った値を通すかのどちらかに倒れる。正規表現は粗いふるいとして使い、厳密な判定はバリデーションライブラリや実際の到達確認に委ねる。
// ❌ 正規表現だけでメールアドレスを完全にバリデーションするのは困難
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
// ✅ Zod などのバリデーションライブラリを使う
import { z } from 'zod';
const schema = z.object({ email: z.string().email() });
ReDoS (正規表現 DoS)
JavaScript・Python・Java などの正規表現エンジンは、候補を試して行き詰まったら戻って別の候補を試すバックトラッキング方式で動く。文字列の分け方が何通りもあるパターンに、最後まで一致しない入力を与えると、この試行回数が入力の長さに対して爆発する。
// ❌ 壊滅的なバックトラッキング
const evil = /^(a+)+$/;
evil.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaab'); // 数秒かかる (a が 1 文字増えるごとに約 2 倍)
// ✅ 分け方が一通りしかないので試行が爆発しない
const safe = /^a+$/;
(a+)+ のようにネストした量指定子は、同じ a の並びを内側と外側でどう分けるかが何通りも成り立つため、一致に失敗するたびに分け方を変えて試し直す。上の例は a を 1 文字増やすごとに所要時間がおよそ 2 倍になり、a 29 個で数秒に達した (Node.js 26 での 2026 年 8 月時点の計測)。外部入力からパターンを組み立てる場合や、検証対象の長さに上限を置いていない場合は ReDoS の的になる。
一方、Go の regexp パッケージのように RE2 の構文と実装を採るエンジンは、入力長に対して線形時間で動くことを保証している (バックトラッキング方式の実装はこの保証を持たないものが多い)。代償として先読み (?=...) と後読み (?<=...) を構文から外しているため、それらに依存したパターンはそのまま移植できない。ReDoS を構造的に避けたい経路では、パターンの書き換えよりエンジンの選択が効く。
正規表現を使うべきでないケース
正規表現が扱えるのは文字の並びのパターンであり、入れ子の深さを数えたり対応する括弧を突き合わせたりはできない。構造を持つデータは、その構造を理解する専用のパーサーに任せる。
| ケース | 代替 |
|---|---|
| HTML のパース | DOM パーサー |
| JSON のパース | JSON.parse() |
| コードの変換 | AST (ast-grep, jscodeshift) |
正規表現については関連書籍でも詳しく扱われている。
この記事は役に立ちましたか?