
正規表現 実践入門 - よく使うパターンと、ReDoS などの落とし穴
正規表現は強力ですが、「なんとなくコピペで動かしている」「複雑になると誰も読めない」になりがちな道具でもあります。この記事では、基本からよく使うパターン、そして ReDoS(正規表現による DoS)のような落とし穴と「使うべきでない場面」まで、MDN を一次ソースに整理します。
記法は JavaScript 準拠ですが、PHP・Python・Java などとおおむね共通です(差異は適宜ふれます)。
基本
文字クラス
| 記法 | 意味 |
|---|---|
[xyz] [a-c] | 列挙した文字・範囲のいずれか1文字 |
[^xyz] | 否定(列挙以外の1文字) |
. | 任意の1文字(改行を除く) |
\d \D | 数字 / 非数字 |
\w \W | 単語構成文字 [A-Za-z0-9_] / それ以外 |
\s \S | 空白 / 非空白 |
WARNING
\w や \d は基本 ASCII 基準で、日本語やアクセント付き文字を期待どおり扱いません。日本語を含めたいときは文字クラスを自分で書くか、Unicode プロパティ(後述の u フラグ + \p{...})を使います。
量指定子・アンカー
量指定子(繰り返し)は x*(0回以上)、x+(1回以上)、x?(0か1回)、x{n}、x{n,}、x{n,m}。いずれもデフォルトは貪欲です。
アンカー(位置だけにマッチ)は ^(先頭)、$(末尾)、\b(単語境界)、\B(非単語境界)。
選択(OR)は縦棒で書きますが、大きなパターンの一部にするにはグループ化が要ります。
gr(a|e)y // "gray" または "grey"グループの種類
| 記法 | 意味 |
|---|---|
(x) | キャプチャグループ(記憶して $1 等で参照) |
(?:x) | 非キャプチャグループ(グループ化のみ。可読性・速度に効く) |
(?<name>x) | 名前付きキャプチャ(match.groups.name で参照) |
\1 \k<name> | 後方参照(同じ部分文字列に再マッチ) |
(?<area>\d{3})-(?<local>\d{4}) // 名前で取り出す
(['"]).*?\1 // 同じ引用符で閉じる(後方参照)先読み・後読み(lookaround)
「マッチはするが消費しない」ゼロ幅の条件です。前後の文脈で絞り込みつつ、その部分は結果に含めないのに便利です。
| 記法 | 名称 | 意味 |
|---|---|---|
x(?=y) | 肯定先読み | y が後続する x |
x(?!y) | 否定先読み | y が後続しない x |
(?<=y)x | 肯定後読み | y が先行する x |
(?<!y)x | 否定後読み | y が先行しない x |
\d+(?=円) // "1000円" の "1000"(円は含めない)
(?<=\$)\d+ // "$50" の "50"(ドル記号は含めない)NOTE
後読み(lookbehind)は対応に差があります。JavaScript は ES2018 で対応しましたが、古い環境や一部エンジンでは使えないことがあります(要確認: 言語・バージョン別)。
貪欲 vs 非貪欲
量指定子はデフォルトで貪欲(できるだけ長くマッチ)。末尾に ? を付けると非貪欲(できるだけ短く)になります。
<.*> // "<foo> <bar> </bar> </foo>" 全体にマッチ(貪欲)
<.*?> // "<foo>" だけにマッチ(非貪欲)
[^>]* // 否定文字クラス。バックトラッキングを抑えられて高速タグや引用符の中身を1個ずつ取りたいときは、非貪欲か、[^>]* のような否定文字クラスを使うのが定石です。
フラグ(JavaScript)
| フラグ | 効果 |
|---|---|
g | 最初の一致で止めず全件を対象 |
i | 大文字小文字を区別しない |
m | ^ $ を各行の先頭/末尾にマッチ |
s | . を改行にもマッチ(dotAll) |
u | Unicode として扱い、\p{...} 等を有効化 |
y | 現在位置からのみマッチ(sticky) |
注意: フラグの指定方法は言語で異なります(Python は re.IGNORECASE 等、PHP/PCRE は /i /m /s /u)。
よく使うパターン(完全な検証には不向き)
ここは強調しておきたい点です。正規表現は「だいたい正しい形式か」を素早く見るための道具で、メールや URL のように仕様が複雑なものの厳密な妥当性検証には向きません。
メールアドレス
HTML 仕様(<input type="email">)が実際に使っている正規表現は、MDN にこう載っています。
/^[\w.!#$%&'*+/=?^`{|}~-]+@[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?(?:\.[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?)*$/iMDN はこれを「形式が正しいことしか保証しない(実在は別)」と明記しています。RFC 5322 に厳密準拠しようとすると数千文字規模になり、それでも国際化ドメインなどを取りこぼします。実務では「緩い形式チェック + 実際に確認メールを送る」のが堅実です。
日付・URL・電話番号
- 日付
^\d{4}-\d{2}-\d{2}$は2026-13-40のような不正値も通ります。実日付の妥当性は日付ライブラリで - URL はブラウザの
URLコンストラクタや各言語の URL パーサを使うのが安全。正規表現は前処理程度に - 電話番号は表記揺れが極端に多く、
libphonenumberのような専用ライブラリが正解
落とし穴
ReDoS(壊滅的バックトラッキング)
最も怖いのがこれです。OWASP によれば、バックトラッキング方式のエンジンは、特定パターン+悪意ある入力に対して入力長に応じて指数的に遅くなり、サービス停止(DoS)を招きます。原因は「繰り返しの中にさらに繰り返し・重複する選択肢」がある構造です。
(a+)+$
([a-zA-Z]+)*$
(a|aa)+$
(a|a?)+$対策は、ネストした量指定子を避ける、否定文字クラスでマッチ範囲を限定する、入力長を制限する、可能なら RE2 のようなバックトラッキングしないエンジンを使う、信頼できない正規表現を実行しないこと。
エスケープ忘れ
. * + ? ( ) [ ] { } ^ $ | \ / はメタ文字で、リテラルとして使うにはバックスラッシュでエスケープします(ドットは \.)。文字列から動的に正規表現を組むときのエスケープ漏れは、バグと脆弱性の温床です。各言語のエスケープ関数を使ってください。
HTML を正規表現でパースしない
HTML / XML は入れ子・任意属性・コメントを持ち、正規表現(基本的に正規言語)では正しく解析できません。属性内の >、ネスト、壊れたマークアップで必ず破綻します。DOMParser や各言語の HTML パーサを使ってください。
正規表現を使うべきでない場面
- 入れ子構造や HTML/XML/JSON のパース → 専用パーサ
- メール・URL・電話番号の厳密な検証 → 専用ライブラリ + 実地確認
- 信頼できない外部入力をパターンとして実行 → ReDoS リスク
- 単純な固定文字列の検索・置換 →
includes/indexOf/ 文字列のreplaceの方が速く読みやすい - 複雑すぎて誰も読めない正規表現 → 小さな処理やパーサに分割した方が保守的
まとめ
- 基本は文字クラス・量指定子・アンカー・グループ・選択。
\w\dは ASCII 基準で日本語に注意 - グループはキャプチャ / 非キャプチャ
(?:)/ 名前付き(?<name>)を使い分ける - 先読み・後読みで「消費せず文脈で絞る」、非貪欲
*?や否定文字クラスで取りすぎを防ぐ - メール・URL の完全検証は正規表現に向かない。形式チェック+実地確認が現実的
- ReDoS(ネストした量指定子)に注意。信頼できない正規表現は実行しない。HTML はパーサで
- 「正規表現を使わない方がいい場面」を見極めるのも実力のうち
正規表現は「短く書ける魔法」ではなく「慎重に扱う鋭利な道具」です。基本と落とし穴を押さえ、向かない仕事には使わない——それだけで、バグも脆弱性もぐっと減らせます。