正規表現の先読み・後読み完全ガイド — (?=...) と (?<=...) を実務で使い分ける
正規表現の先読み(lookahead)と後読み(lookbehind)は、マッチ位置を判定するためだけに使い、結果の文字列には含めないという特殊な構文です。「この文字の"直後"に〜があるとき」「この文字の"直前"に〜があるとき」といった条件を、キャプチャグループを汚さずに書けるのが強みです。本記事では 4 種類の lookaround を実務パターン中心に整理します。
4種類の lookaround 構文
| 構文 | 名称 | 意味 |
|---|---|---|
(?=X) | 肯定先読み | 直後にXがある |
(?!X) | 否定先読み | 直後にXがない |
(?<=X) | 肯定後読み | 直前にXがある |
(?<!X) | 否定後読み | 直前にXがない |
重要な特徴: lookaround は文字を"消費"しません。マッチ位置はチェック前のまま進みません。そのため、連続した lookaround を並べて and 条件のように使えます。
パターン1: 特定の文字の前だけ抜き出す
「価格の数字だけ取り出したい。後ろに"円"が付いているもの限定」というケース。
// "商品A 500円, 送料 300円, コード 1234" から円の前の数字だけ
const text = "商品A 500円, 送料 300円, コード 1234";
text.match(/\d+(?=円)/g);
// ["500", "300"] — "1234" はヒットしない
キャプチャグループ (\d+)円 で 円 までマッチさせてから [1] で抜き出すのは定番ですが、lookahead なら match 結果がそのまま数字だけになります。
パターン2: 特定の文字の後だけ抜き出す
「@ の後のドメインだけ」「$ の後の金額だけ」といったケースに後読みが便利です。
// メールアドレスからドメインだけ抽出
"alice@example.com, bob@nantoo.jp".match(/(?<=@)[\w.-]+/g);
// ["example.com", "nantoo.jp"]
// $ の後の金額
"price $99.99, tax $9.50".match(/(?<=\$)\d+(?:\.\d+)?/g);
// ["99.99", "9.50"]
パターン3: 特定の条件を避けるため否定 lookahead
「HTMLタグ以外のテキストを取得」「スペースを挟まない単語だけ」といった"除外"系は否定 lookaround で書けます。
// "abc123" の数字だけ、ただし文字列末尾(= 直後に文字なし)のものは除外
"abc123xyz 789".match(/\d+(?!\w)/g);
// ["123"] ← "789" は直後にワード文字がないのでヒット… あれ違う
// 正しく "単独の数字の塊で、末尾が特殊文字や末尾" を取るなら:
"abc123xyz789!end 456".match(/\d+(?=[^\w]|$)/g);
// ["789", "456"]
否定 lookahead は直感と逆の挙動になりがちです。正規表現テスターで条件をひとつずつ検証するのが安全です。
パターン4: 複数条件の and 結合
「英数字混在のパスワード(数字と英字が両方含まれ、8文字以上)を一括判定」は lookahead の代表的な使い方です。
// 8文字以上・数字と英字を両方含む
const passwordRe = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;
passwordRe.test("abc12345"); // true
passwordRe.test("abcdefgh"); // false (数字なし)
passwordRe.test("12345678"); // false (英字なし)
passwordRe.test("abc123"); // false (短い)
(?=...) は位置を進めずに条件だけチェックするため、連続して並べることで and 条件を組めます。3つ以上の条件でも同じパターンで拡張可能です。
パターン5: 区切り文字で分割せず置換したい
3桁ごとにカンマを入れる「1234567 → 1,234,567」という処理は、JavaScriptでは toLocaleString で済みますが、lookaround を使えば正規表現1行で書けます。
"1234567890".replace(/\B(?=(\d{3})+(?!\d))/g, ",");
// "1,234,567,890"
解説: \B は単語境界以外、(?=(\d{3})+(?!\d)) は「この位置以降に "3桁の倍数 + 数字でない境界" が続く」位置を見つけます。つまり右から数えて3桁ごとに区切りを入れる、という挙動です。
ブラウザ・Node.js の互換性
先読み (lookahead)
すべてのモダンブラウザ・Node.js で古くから動作します。互換性の懸念はありません。
後読み (lookbehind)
歴史的経緯から対応が少し遅れていました。
- Chrome 62+ (2017年), Edge 79+, Firefox 78+ (2020年), Safari 16.4+ (2023年)
- Node.js 10+ で対応
iOS Safari を広くサポートするサイトでは、2023年春以前のiOSユーザーでは lookbehind が動かないことに注意。IE対応は不可能です。
互換を気にする場合の代替:
// lookbehind: /(?<=@)[\w.-]+/
// 代替: キャプチャで取る
const m = "alice@example.com".match(/@([\w.-]+)/);
m?.[1]; // "example.com"
パフォーマンスの注意
lookaround は便利ですが、ネストした lookaround や巨大な文字列での複雑な lookbehind は指数的に遅くなるケースがあります(catastrophic backtracking)。
目安:
- 固定長の lookbehind(
(?<=abc)): 高速 - 可変長の lookbehind(
(?<=.*abc)): 実装次第で遅い(V8は最適化あり) - 複数の lookahead 直列: 基本的に高速(パスワード判定など)
数MB以上の長文に対して正規表現を使う場合は、必ず実測してください。代替としてパース用ライブラリ(peggy, nearley 等)や文字列 split + filterのほうが高速なこともあります。
まとめ
- 4種類の lookaround は「位置を判定する、文字は消費しない」が基本
- 複数の
(?=...)を並べると and 条件が簡潔に書ける(パスワード検証の定番) - lookbehind は iOS Safari 16.4 未満で動かない。互換が必要ならキャプチャグループで代替
- 巨大文字列では catastrophic backtracking に注意し、必ず実測
- 書いた正規表現は本記事のツールや regex101 等で必ず動作検証する
参考文献・ソース
本記事は NanToo(ナンツー)運営の株式会社ヨネマスが編集・公開しています。 ツールの開発現場で得た知見をもとに、実務で役立つ内容を発信しています。