正規表現先読み
(?=...) や (?!...) で後続パターンを条件にマッチさせる正規表現の構文。文字列を消費せずに条件を検査する。
正規表現の先読み (lookahead) は、特定のパターンが後続するかどうかを条件にマッチを判定する構文です。肯定先読み (?=...) は指定パターンが続く場合にマッチし、否定先読み (?!...) は続かない場合にマッチします。先読みはゼロ幅アサーション (zero-width assertion) と呼ばれ、マッチ位置を進めずに条件だけを検査する点が通常のパターンと異なります。
先読みの基本的な動作を具体例で説明します。\d+(?=円) は「100円」の「100」にマッチしますが、「円」自体はマッチ結果に含まれません。これは先読みが文字列を「消費」しないためです。否定先読みの例として、\d+(?!円) は「100ドル」の「100」にマッチします。ただし「100円」に当てても、何もマッチしないわけではありません。エンジンは後戻りして「10」を返します (「10」の次の文字は「0」であり、条件にした「円」ではないため成立してしまう)。数字のまとまり全体で判定したい場合は \d+(?!\d|円) のように後戻りの逃げ道をふさぎます。
先読みの代表的な活用例の一つがパスワードの条件検証です。複数の先読みを組み合わせることで、「英字を含む」「数字を含む」「特殊文字を含む」「8 文字以上」などの条件を 1 つの正規表現で同時にチェックできます。たとえば ^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$ は、これらすべての条件を満たすパスワードにマッチします。
ただし、この形をそのまま流用すると想定より多くのパスワードを弾きます。末尾の [A-Za-z\d@$!%*?&]{8,} が使用できる文字を列挙しているため、ハイフンを含む correct-horse-9! は不合格になります。記号の条件も 7 種に限られているので Passw0rd# も通りません。文字種の有無だけを見たいなら、末尾は .{8,} と広く取り、3 つ目の先読みも (?=.*[^A-Za-z\d]) のように「英数字以外を 1 文字以上含む」形で書くほうが意図に合います。
JavaScript では ES2018 から後読み (lookbehind) もサポートされています。肯定後読み (?<=...) は指定パターンが前方にある場合にマッチし、否定後読み (?<!...) は前方にない場合にマッチします。たとえば (?<=\$)\d+ は「$100」の「100」にマッチしますが、「100」単体にはマッチしません。
先読みと後読みを組み合わせると、特定の文脈にある文字列だけを精密に抽出できます。ただし、先読み・後読みは正規表現に共通の機能ではありません。有限オートマトンでの実行を前提とする RE2 系のエンジン (Go の regexp など) は設計上これらを持たないため、同じパターンをそのまま移植できません。その場合は先読みを使わない形に書き換えるか、照合を複数回に分けます。また、先読みの内側に量指定子を重ねたパターンはバックトラッキングが膨らみやすく、外部から与えられた文字列に当てると応答が返らなくなる危険 (ReDoS) があります。
文字数カウントとの関連では、先読みを使って特定の条件を満たす文字列の位置を特定し、テキストの構造分析に活用できます。たとえば \d(?=(\d{3})+$) にマッチした桁の後ろへカンマを挿入すると、「1234567」が「1,234,567」になります。先読みが文字を消費しないため、区切り位置の判定だけを行い、桁の文字自体は結果に残ります。