Prompt
Atomic Groupingっていつ使うの!?
グループ内の文字がユニークであり、その後に出現する文字についてチェックしたいとき?
例えば、文字列状態のJSONの、キーに対する値を調べるとき?
まさにその通り! 君の直感は完璧だね。Atomic Grouping(独占的グループ化: (?>...)) は、「一度マッチした事実は、何があっても譲らない」 という強い意志を正規表現に持たせるための機能なんだ。
JSON の解析を例に出したのは非常に鋭いよ。まさに 「構造的に後戻りが無意味な場所」 で真価を発揮するんだ。
1. なぜ Atomic Grouping が必要なのか?
通常のグループ (...) は「親切すぎる」んだ。
もしグループ内のパターンにマッチした後、その後のパターンが失敗すると、正規表現エンジンは 「ごめん、さっきのマッチを少し削ったら、後半部分がうまくいくかも?」 と考えて、グループの中にまで戻って(バックトラッキングして)やり直しを始めてしまう。
Atomic Grouping (?>...) は、この 「無駄な親切(後戻り)」を禁止 する。
2. JSON のキー探索を例に考えてみよう
例えば、JSON 文字列から "status" というキーの値を調べたいとするよね。
JSON
{"id": 123, "status": "active", "type": "admin"}
正規表現で "status":\s*"([^"]+)" のように書いたとき、もし "active" の後のダブルクォートが何かの手違いでマッチしなかった場合、エンジンは以下のように動く:
-
通常のグループだと:
([^"]+)が"active"にマッチした後、後ろが失敗すると「activeをactivに減らしてみたらどうかな?」と無駄な試行錯誤を繰り返す。 -
Atomic Grouping だと:
(?>( [^"]+))と書けば、「一度ダブルクォート以外の文字を全部拾ったら、そこから 1 文字も返さない!」と決める。
「キーに対する値の文字列」は一度決まったらそれ以外あり得ない から、後戻りして削る必要なんて全くない。こういう時に Atomic Grouping を使うと、失敗時の判定が爆速になるんだ。
3. Atomic Grouping を使うべき 2 つの条件
君が言った「グループ内がユニーク」という視点を、もう少し情報工学的に整理するとこうなるよ:
① 後続のパターンが失敗したとき、グループ内を微調整しても「成功する見込みがない」とき
JSON の値、クォートで囲まれた文字列、HTML タグの中身など、「境界線」がハッキリしているもの を探すときだね。
② 指数関数的なバックトラッキング(破滅的な後戻り)を防ぎたいとき
例えば (a+)+b というパターンで、aaaaa...(末尾に b がない)という文字列を検索すると、+ が重なっているせいで組み合わせ爆発が起きる。ここを (?>a+)+b にすれば、エンジンは迷わず「失敗」と判断できる。
4. プログラミングでの書き方
多くの言語(Java, Ruby, PHP, .NET など)では (?>...) をサポートしているよ。
※ JavaScript は長らく非対応だったけれど、最近の ES2018 以降では 「独占的量化子(Possessive Quantifiers)」 という似た機能(a*+ など)が使えるようになっているよ。
Java
// Java での例
// ".*" ではなく "(?>.*)" とすることで、マッチしなかった時の後戻りを防ぐ
String regex = "\"(?>[^\"]*)\"";
豆知識:独占的量化子との違い
実は Atomic Grouping (?>a*) と、独占的量化子 a*+ は、意味的にはほとんど同じなんだ!
-
a*+は、量化子(*や+)そのものを独占的にするショートカット。 -
(?>...)は、中身が複雑なパターン(OR を含むものなど)をひとかたまりで固定したいときに使う。
見落とされがちな視点:メンテナンス性
Atomic Grouping は強力だけど、「正しく書かないと、本来マッチするはずのものまで捨ててしまう」 という諸刃の剣でもある。
「ここは絶対に後戻りが発生しない、構造的に独立したパーツだ」と確信を持てる場所(トークン)から使い始めるのが、プロへの第一歩だね!
結論
君の考えた 「JSON のキーや値のように、一度範囲が決まれば後戻りが無意味な構造」 をチェックするときこそ、Atomic Grouping の出番だ!
次は、「実際に自分の書いた正規表現がどれだけバックトラッキングを発生させているか、ステップ数を可視化して確認する方法」 をやってみる?それとも、「JavaScript で Atomic Grouping の代わりに独占的量化子を使って高速化するテクニック」 について深掘りしてみる?
Would you like me to analyze a specific regex you’re working on to see if we can optimize it with Atomic Grouping?
Prompt
理解が曖昧なままで使うと事故るな。これは
次に、(?:xxx)は何?
まさにその通り!正規表現の 「独占的グループ化(Atomic Grouping)」 は、いわば 「一度決めたら離婚できない契約」 みたいなものだから、うかつに使うと「実はマッチするはずだった別の可能性」をすべて切り捨ててしまう。慎重になるのはエンジニアとして正しい姿勢だよ!
さて、次に挙げてくれた (?:xxx) は、「非キャプチャグループ(Non-capturing group)」 と呼ばれるものだね。これは Atomic Grouping とは違って、「実行速度」よりも「メモリ効率」と「コードの整理」 のために使われる非常に紳士的な機能だよ。
還元主義的に、普通のカッコ () との違いを解剖してみよう!
1. 「記憶」するか「無視」するか
正規表現の普通のカッコ () には、実は 「2つの役割」 があるんだ。
-
グループ化:
(abc)+のように、ひとかたまりとして扱う。 -
キャプチャ(記憶): マッチした部分を後で再利用できるように、メモリに保存する。
でも、「グループ化はしたいけど、後でその中身を使う予定はない」 という時があるよね。その時に (?:xxx) を使うんだ。
-
(): 「これはグループだ!しかも後で使うから大事にメモ(変数$1とかに保存)しておけよ!」 -
(?:): 「これは単なるグループだ。中身は覚えなくていいから、そのまま通り過ぎてくれ。」
2. なぜ (?:xxx) を使うのか?(メリット)
① メモリの節約
大きなテキストを解析するとき、不要なキャプチャを大量に作ると、それだけメモリを消費し、ガベージコレクション(GC)の負荷も上がる。?: をつければ、エンジンは「覚える」という仕事をサボれるから、全体的に効率が良くなるんだ。
② 「後方参照」の番号がズレない(これが一番重要!)
普通のカッコを多用すると、あとで「何番目のカッコを抜き出したいんだっけ?」と混乱することがあるよね。
JavaScript
// 例:日付をパースしたい
// (?:) を使わないと、(月|火|...) が「第2グループ」になってしまう
const regex = /(\d{4})年((?:月|火|水|木|金|土|日))曜日/;
// 結果を抽出
const month = result[1]; // 2026
const day = result[2]; // ここが (?:) のおかげで「曜日」を飛ばして次の要素を狙いやすい
3. Atomic Grouping (?>) との違い
ここが混同しやすいポイントだけど、役割は全く違うよ。
-
(?:xxx)(非キャプチャ): 「覚えない」 だけ。バックトラッキング(後戻り)は普通にする。 -
(?>xxx)(独占的): 「後戻りさせない」。結果を覚えるかどうかは関係なく、一度マッチしたら道を譲らない。
たとえ話:
(?:)は、旅行中に写真を撮らずに景色だけ楽しむ人。(思い出の記録=メモリは残さないが、道に迷ったら引き返せる)
(?>)は、一度通った道にバリケードを築いて進む、絶対に引き返さない探検家。
豆知識:エンジニアの「マナー」としての (?:)
プロの正規表現使いは、「キャプチャする必要がないなら、常に (?:) を使う」 という習慣を持っていることが多いよ。
これは、後でコードを読む人に「このカッコは単に OR の範囲を指定するために使っているだけで、抽出するためのものじゃないよ」というメッセージを伝える セマンティクス(意味論) 的な役割も果たしているんだ。
見落とされがちな視点:ネストの深さ
複雑な正規表現を書くと、カッコが ((?:...)|(...)) のように入れ子(ネスト)になることがある。
このとき、すべてを普通の () にしてしまうと、キャプチャ番号の管理が地獄になる($1, $2, $3…)。 (?:) を適切に混ぜることで、「本当に欲しいデータだけを $1 や $2 に集約する」 という設計ができるようになるんだ。
結論
(?:xxx) は 「後で使わないグループに付ける、親切でエコな印」 だと思えば OK!
Atomic Grouping のような「事故」の心配はないから、積極的に使って大丈夫だよ。
次は、「実際に Java や JavaScript で、キャプチャした $1 や $2 を使って文字列を置換(replace)する具体的なテクニック」 についてやってみる?それとも、「正規表現の可読性を上げるための『拡張モード(コメント付き正規表現)』の書き方」 を覗いてみる?
Would you like me to show you a complex regex refactored with (?:) to see how it clarifies the capturing logic?
Prompt
こっちはものすごく使いやすい!
正規表現には想像よりも多くの構文があるね。そんなマニアック正規表現について他に面白いネタはある?
(?:xxx) を気に入ってくれて嬉しいよ!これを使えるようになると、正規表現が「ただの呪文」から「精密な設計図」に進化するよね。
正規表現の深淵はまだまだ深くて、情報工学的に「そんなことまでできるの!?」というマニアックな構文がいくつかあるんだ。君のような好奇心旺盛なエンジニアが喜びそうな 「パズル的」で「超実用的」なネタ を 3 つ厳選して紹介するよ!
1. 肯定先読み ((?=xxx)):未来を予言する
Atomic Grouping や Non-capturing Group よりもさらに戦略的なのが 「先読み(Lookahead)」 だよ。
-
意味: 「この先に
xxxが続く場合だけ、その手前の地点で マッチしたとみなす」 -
凄み: マッチしても 「カーソルが進まない」 という特殊な性質を持っているんだ。
【活用例】強力なパスワードバリデーション
「英小文字、英大文字、数字をすべて 1 文字以上含む 8 文字以上の文字列」という条件、普通に書くと if 文が連なるけれど、先読みなら 1 行で書けるよ。
コード スニペット
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$
2. 再帰正規表現 ((?R)):マトリョーシカを解く
「正規表現では括弧の対応(ネスト)は取れない」という通説があるけれど、実は一部のエンジン(PHP, Ruby, Perl など)では 「自分自身を呼び出す」 ことで解決できるんだ。
-
構文:
(?R)または(?0) -
凄み:
( (?: [^()] | (?R) )* )のように書くと、「無限にネストされた括弧」 を正しくパースできる。もはや正規表現の枠を超えて、構文解析器(パーサー)に近い挙動だね。
3. 条件分岐 ((?(condition)true|false)):if 文を組み込む
なんと、正規表現の中に if-then-else を持ち込むことができるんだ。
-
意味: 「もしグループ 1 がキャプチャされていたら A にマッチさせ、そうでなければ B にマッチさせる」
-
活用例: 「電話番号で、市外局番の
(があったら、必ず閉じ括弧)も存在しなければならない」という整合性チェックが 1 行で書ける。
コード スニペット
^(\()?\d{3}(?(1)\)|-)\d{3}-\d{4}$
※
(1)は「1番目のカッコが存在するか」を判定しているよ。
豆知識:正規表現の「方言」問題
これらマニアックな構文を使うときに一番気をつけなきゃいけないのが 「方言(フレーバー)」 だよ。
-
PCRE (PHP, Apache): ほぼ全部入り。
-
Python (
re): 基本的だけど、再帰などはregexライブラリが必要。 -
JavaScript: 最近かなり進化している(後方参照や名前付きキャプチャ等)けれど、まだ PCRE には及ばない。
「自分の環境のエンジンは何ができるのか」を調べるのも、エンジニアの楽しみの一つだね。
見落とされがちな視点:可読性の崩壊
これらマニアックな構文を使いすぎると、「書いた本人すら 3 日後には読めない」 という暗号が出来上がる。
複雑な正規表現を書くときは、必ず x フラグ(拡張モード) を使って、改行とコメントを入れるのが「デキるエンジニア」の作法だよ。
Plaintext
# これなら何をやっているか一目瞭然!
/
^ # 行頭
(?=.*[a-z]) # 小文字チェック
(?=.*[A-Z]) # 大文字チェック
.{8,} # 8文字以上
$ # 行末
/x
次のステップ
正規表現の「パズルとしての面白さ」が伝わったかな?
次は、「実際に君が困っている『複雑な文字列抽出』を、これらのマニアック構文で鮮やかに解いてみる」 コーナーをやってみる?それとも、「JavaScript でも使えるようになった最新の正規表現テクニック」 について深掘りしてみる?
Would you like me to demonstrate how to refactor a messy nested loop into a single, elegant Lookahead regex?
Prompt
この世で最も絶望的な正規表現を知ってる?
正規表現の深淵を覗こうとする君に、情報工学の歴史に刻まれた「最凶」の正規表現を紹介するよ。それは、かつて RFC 822(メールアドレスの規格)を完璧にパースするために生み出された、通称 「Perlによるメールアドレス正規表現」 だ。
まずは、その姿を見て絶望してほしい。
(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t]
)+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t]
)*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t]
)+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t]
)*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t]
)+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t]
)*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t]
)+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t]
)*))*|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;
:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[
\t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>
@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n
)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>
@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r
\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()
<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r
\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()
<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*)))*:(?:(?:\r\n
)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()
<>@,;:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r
\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()
<>@,;:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n
)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>
@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n
)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>
@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*)|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t])*)*:(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*)))*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*))(?:,(?:(?:\r\n)?[ \t])*(?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*)))*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\\" \r\n]|\\[^\r\n])*" (?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\] \r\n]|\\[^\r\n])*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*))*)?;(?:(?:\r\n)?[ \t])*なぜこんなことになったのか?(絶望の理由)
これを見て「頭がおかしい」と思うかもしれないけれど、これでも 「RFC 822」という規格を忠実に守ろうとした結果 なんだ。情報工学的な絶望ポイントを3つ挙げるね。
1. コメントの入れ子 ((...))
当時のメール規格では、アドレスの中に (comment) を入れ子で入れることが許されていたんだ。以前話した通り、正規表現は基本的に入れ子構造が苦手。それを無理やりフラットな文字列で解決しようとした結果、この肥大化を招いたんだ。
2. 引用符とエスケープ
名前部分にダブルクォートを使ったり、その中でバックスラッシュを使ってエスケープしたり……。あらゆる例外処理を OR (|) で繋ぎ合わせた結果、もはや人間には解読不能な「書き込み専用コード」になってしまった。
3. 可読性という概念の完全な崩壊
この正規表現をメンテナンスすることは不可能に近い。もし規格が少しでも変わったら、これを修正するより、最初から構文解析器(パーサー)を C 言語や Java で書いたほうが 100 倍マシだよね。
教訓:正規表現は「万能薬」ではない
この正規表現は、「正規表現で解決できること」と「プログラミング言語で解析すべきこと」の境界線 を教えてくれる反面教師なんだ。
-
現代のベストプラクティス:
メールアドレスのバリデーションは、
/.+@.+/程度の緩いチェックにとどめるか、標準ライブラリのValidatorを使うのが正解。「完璧な正規表現」を求めると、この怪物のようなコードが生まれてしまう。
豆知識:ReDoS(正規表現によるDoS攻撃)
これだけ複雑な正規表現に、悪意のある長い文字列を流し込んだらどうなると思う?
さっき話した 「バックトラッキング」 が無限に発生して、CPU の使用率が 100% に張り付き、サーバーが死ぬ。これを ReDoS と呼ぶよ。この絶望的な正規表現は、まさに攻撃者にとっての「ご馳走」でもあるんだ。
見落とされがちな視点:正規表現の「限界」
チョムスキー階層という理論を知っているかな?正規表現が扱えるのは「正規言語」というレベル。入れ子構造(文脈自由言語)を扱うには、本来は「プッシュダウン・オートマトン」が必要なんだ。
このメールアドレスの正規表現は、「正規言語の道具で、文脈自由言語を無理やり解こうとした」 ゆえの悲劇とも言えるね。
結論
この世で最も絶望的な正規表現は、「人間の執念が、道具の限界を突破しようとしたときに生まれる怪物」 のことだったんだ。
次は、「この怪物に立ち向かうために、もっとシンプルで現実的なメールアドレスのバリデーション設計」 について考えてみる?それとも、「ReDoS を防ぐために、正規表現の実行時間にタイムアウトを設ける実装方法」 について話してみる?
Would you like me to analyze a “simpler” version of an email regex and explain why it’s usually “good enough” for 99% of web apps?