03 / ガイド
正規表現を安全にテストするワークフロー
JavaScript 正規表現のための write-test-refactor ループ:現実的なサンプルデータ、壊滅的バックトラッキングの予防、アンカーの規律、フラグの意味論、そしてブラウザでテストすることがパターンのプライバシーを守る理由。
ループ:書く、テストする、直す
信頼できる正規表現が一発で書かれることはありません。実際の例 1 つにマッチする最小のパターンから始め、実行し、次の例に照らして広げたり締めたりします。サンプルテキストが仕様であり、パターンが実装です。
フィードバックが即座ならループは速く回ります。キー入力のたびに再評価するテスターは、各編集を即答のある実験に変えます——手動でスクリプトを再実行するより専用ワークスペースが勝る理由です。
- サンプルから始める記憶ではなく具体的なサンプルに対してパターンを書きます。解析したいログやドキュメントの実際の 1 行を貼り付けましょう。
- 毎回テスト変更のたびにテストします——正規表現にコンパイラ警告はなく、唯一のフィードバックは何にマッチし何を取りこぼすかです。
- 読者のために直す通ったら読みやすさのためにリファクタリングします。名前付きキャプチャグループと非キャプチャグループが、動くパターンを次の読者にも生き残らせます。
壊滅的バックトラッキングを侮らない
JavaScript の正規表現エンジンはバックトラックします。マッチに失敗すると、各量詞の長さを変えて再試行します。(a+)+ のような入れ子の量詞が惜敗文字列に作用すると、再試行回数が指数関数的に爆発します——良好な入力には即答するパターンが、悪意ある入力には数年かかることもあります。攻撃者はこれを ReDoS と呼びます。
JavaScript 正規表現テスターは爆発半径を制限します。パターンは専用 Web Worker 内で 650 ミリ秒の予算で実行され、暴走した式はタブのフリーズではなく明示的なタイムアウトとして報告されます。現実的な入力でその上限に達したら、それは発見として扱いましょう——同じ入力が本番ならイベントループを停止させます。
- 入れ子の量詞典型的な形は量詞付きグループの中に量詞付きトークンがあるもの——(a+)+ や (\w+)*——で、同じ文字列の多くの分割がすべて内部パターンを満たします。
- 曖昧な選択曖昧な選択肢も爆発させます。(a|aa)+ は余分な各文字に 2 通りのマッチ方法を与え、探索木を文字ごとに倍にします。
- 構造的な修正修正は構造的です。内部マッチを一意にし、明示的な長さ制限で拘束し、有効な入力だけでなく必ず惜敗入力でもテストします。
マッチを信用する前にアンカーを
部分文字列を見つけるパターンは、値を検証するパターンではありません。/\d+/ は abc123xyz の内部にマッチし、/^\d+$/ は数字のみを受け付けます。検証バグの大半はアンカー不足、抽出バグの大半はあるべきでないアンカーです。
- 先頭と末尾^ と $ はマッチを文字列の先頭と末尾に固定します——マルチラインフラグが有効なら各行の先頭と末尾に固定され、「入力全体」の意味が静かに変わります。
- 単語境界\b の単語境界は短いパターンが長い単語の内部にマッチするのを防ぎます。\bcat\b は動物を見つけ、concatenate の中の cat には当たりません。
- 検索か検証か検索か検証かを最初に決めましょう。抽出パターンはたいていアンカーを付けず、バリデーターはほぼ常に両方を付けます。
現実的なサンプルデータでテストする
きれいな例 3 つで証明されたパターンは、本番の 4 つ目の実例で失敗します。入力分布を映すサンプルセットを作りましょう。最長の正当な値、空文字列、空白のバリエーション、Unicode の名前、そしてマッチしそうでしてはいけない惜敗ケースです。
- 惜敗ケース惜敗ケースを含めます。メールパターンは整形式のアドレスだけでなく、a@b や a b@c.d でもテストすべきです。
- Unicodeデータに Unicode があるなら Unicode を含めます。\w と . は u フラグで意味が変わり、アクセント付きの名前は ASCII 時代の前提を打ち破ります。
- セット全体を再実行サンプルセットはパターンの隣に保管し、パターンを変えたら動機となったケースだけでなくセット全体を再実行します。
フラグは検索だけでなく言語を変える
フラグはパターンの意味の一部です。g は最初のマッチか全部かを決め、i は大文字小文字を畳み、m は ^ と $ を行に再アンカーし、s はドットを改行越えにし、u はエンジンをより厳格な構文の Unicode 対応モードに切り替えます。
ワークスペースは選択したフラグでパターンを評価し、各マッチの開始・終了オフセット、それぞれの位置を持つ番号付き・名前付きキャプチャグループ、ライブの置換プレビューを一覧します——フラグの変更が理論ではなく結果の具体的な差として現れます。
- g と lastIndexg がなければ test() と match() は最初のヒットだけを見ます。g があると正規表現オブジェクトは lastIndex 状態を持ち、繰り返しの test() 呼び出しが結果を交互に返します——悪名高い本番バグです。
- s はドットを広げるs(dotAll)は . を改行にマッチさせ、グローバルに有効化すると古いパターンの食いつきを静かに広げることがあります。
- u と仲間u はクラスとエスケープを Unicode 準拠にし、曖昧なレガシー構文を拒否します。テスターは d(マッチインデックス)、y(スティッキー)、v(Unicode セット)もサポートします。
パターンとサンプルをローカルに保つ
テストデータはしばしば本物のデータです。顧客のメールアドレス、社内のログ行、インシデントレポートの識別子。ホストされたテスターに貼り付けることは、それを他人のインフラに送ることです。ブラウザベースのテスターはセッションをタブ内に留めます——パターンとサンプルテキストは自分のデバイスのエンジンで評価され、送信されません。
ローカルランタイムは上限を明示しています。パターンは最大 4,096 文字、サンプルテキストは最大 100,000 文字、置換は 20,000 まで、1 実行あたり最大 1,000 件のマッチ報告です。これらの上限は開発とデバッグ向けのサイズであり、大量ログ解析はブラウザタブではなくローカルスクリプトの仕事です。
必要なもの——完成したパターンと置換文字列——だけをコピーして取り出し、サンプルデータは始まった場所、あなたのマシンに残しましょう。
ローカルでパターンをテスト抽出されたデータはたいてい JSON に行き着く。
パターンが生テキストから値を引き出したら、決定論的なローカル手順で結果のペイロードを検査・差分比較しましょう。