正規表現 実践入門 - よく使うパターンと、ReDoS などの落とし穴

正規表現 実践入門 - よく使うパターンと、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)
uUnicode として扱い、\p{...} 等を有効化
y現在位置からのみマッチ(sticky)

注意: フラグの指定方法は言語で異なります(Python は re.IGNORECASE 等、PHP/PCRE は /i /m /s /u)。

よく使うパターン(完全な検証には不向き)

ここは強調しておきたい点です。正規表現は「だいたい正しい形式か」を素早く見るための道具で、メールや URL のように仕様が複雑なものの厳密な妥当性検証には向きません

メールアドレス

HTML 仕様(<input type="email">)が実際に使っている正規表現は、MDN にこう載っています。

HTML仕様のメール正規表現(形式チェックのみ)
/^[\w.!#$%&'*+/=?^`{|}~-]+@[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?(?:\.[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?)*$/i

MDN はこれを「形式が正しいことしか保証しない(実在は別)」と明記しています。RFC 5322 に厳密準拠しようとすると数千文字規模になり、それでも国際化ドメインなどを取りこぼします。実務では「緩い形式チェック + 実際に確認メールを送る」のが堅実です。

日付・URL・電話番号

  • 日付 ^\d{4}-\d{2}-\d{2}$2026-13-40 のような不正値も通ります。実日付の妥当性は日付ライブラリで
  • URL はブラウザの URL コンストラクタや各言語の URL パーサを使うのが安全。正規表現は前処理程度に
  • 電話番号は表記揺れが極端に多く、libphonenumber のような専用ライブラリが正解

落とし穴

ReDoS(壊滅的バックトラッキング)

最も怖いのがこれです。OWASP によれば、バックトラッキング方式のエンジンは、特定パターン+悪意ある入力に対して入力長に応じて指数的に遅くなり、サービス停止(DoS)を招きます。原因は「繰り返しの中にさらに繰り返し・重複する選択肢」がある構造です。

ReDoSになりやすい脆弱なパターン(OWASP例)
(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 はパーサで
  • 「正規表現を使わない方がいい場面」を見極めるのも実力のうち

正規表現は「短く書ける魔法」ではなく「慎重に扱う鋭利な道具」です。基本と落とし穴を押さえ、向かない仕事には使わない——それだけで、バグも脆弱性もぐっと減らせます。

参考リンク