正規表現における「貪欲」と「怠惰」な量指定子の本質的理解
正規表現(Regular Expressions)は、文字列のパターンマッチングにおいて極めて強力なツールですが、多くの開発者が「意図した通りにマッチしない」という壁に直面します。その主な原因の多くは、量指定子(Quantifier)が持つ「貪欲(Greedy)」という性質を正しく理解していないことにあります。本記事では、正規表現エンジンがどのように文字列を走査し、貪欲な量指定子と怠惰な量指定子がどのような挙動の違いを生むのか、その深淵を解説します。
貪欲な量指定子(Greedy Quantifiers)のメカニズム
量指定子とは、文字やグループが何回繰り返されるかを指定するメタ文字(*, +, ?, {n,m} など)を指します。デフォルトでは、これらの量指定子は「貪欲」に動作します。
貪欲な量指定子の最大の特徴は、「可能な限り長い文字列をマッチさせようとする」点にあります。正規表現エンジンがマッチを試みる際、貪欲な量指定子はまず対象文字列の最後まで一気に読み込みます。その後、マッチが成立しない場合には、後ろから1文字ずつ削りながら(バックトラッキング)、マッチする箇所を探し出します。
例えば、HTMLタグを抽出するために `
` という正規表現を使用した場合、期待とは異なる結果が得られることが多いです。対象文字列が `
` であるとき、貪欲な `.*` は最初の `
` までを全て飲み込んでしまいます。結果として、文字列全体が1つのマッチとして判定されてしまうのです。これは、エンジンが「可能な限り長く」という命令を忠実に守った結果ですが、多くの実務シーンでは意図しない挙動となります。
怠惰な量指定子(Lazy/Non-Greedy Quantifiers)の導入
「可能な限り短く」マッチさせたいという要求を満たすために用意されているのが、怠惰な量指定子です。これは、貪欲な量指定子の後ろに `?` を付与することで定義されます。
具体的には、以下の対応関係があります。
* `*` (貪欲) → `*?` (怠惰)
* `+` (貪欲) → `+?` (怠惰)
* `??` (怠惰)
* `{n,m}?` (怠惰)
怠惰な量指定子は、「最小限の文字列でマッチを成立させる」ことを優先します。先ほどの `
` の例であれば、エンジンは最初の `
` を見つけることを試みます。結果として、最初の `
` と `
` がそれぞれ個別に正しくマッチします。
詳細な動作検証:バックトラッキングの挙動
貪欲と怠惰の挙動を理解する上で避けて通れないのが「バックトラッキング(Backtracking)」の概念です。
貪欲な量指定子でのバックトラッキング:
1. エンジンは量指定子によって可能な限り多くの文字を消費する。
2. 残りのパターンがマッチするか確認する。
3. マッチしなければ、消費した文字を1つ戻し、再度残りのパターンとの照合を試みる。
4. これをマッチが成功するか、これ以上戻せなくなるまで繰り返す。
怠惰な量指定子でのバックトラッキング:
1. エンジンはまず「0文字(あるいは最小数)」でマッチを試みる。
2. 残りのパターンがマッチするか確認する。
3. マッチしなければ、量指定子を1文字広げ、再度残りのパターンとの照合を試みる。
4. これをマッチが成功するか、最後まで到達するまで繰り返す。
このプロセスの違いが、パフォーマンスにも直結します。特に複雑な正規表現において、貪欲な指定子を多用すると、バックトラッキングの回数が指数関数的に増大し、いわゆる「ReDoS(正規表現によるDoS攻撃)」を引き起こすリスクが高まります。
サンプルコードによる挙動の比較
以下のJavaScriptコードを通じて、貪欲と怠惰の決定的な違いを確認しましょう。
const text = "Content AContent B";
// 貪欲な量指定子: 可能な限り長くマッチする
const greedyRegex = /<div>.*<\/div>/;
console.log(text.match(greedyRegex)[0]);
// 結果: "<div>Content A</div><div>Content B</div>"
// 怠惰な量指定子: 可能な限り短くマッチする
const lazyRegex = /<div>.*?<\/div>/g;
console.log(text.match(lazyRegex));
// 結果: ["<div>Content A</div>", "<div>Content B</div>"]
このコード例から明らかなように、`.*` は文字列の境界を無視して最後まで突っ走ろうとしますが、`.*?` は最初の終了条件(`

コメント