N2
NanToo
正規表現の先読み・後読み完全ガイド — (?=...) と (?<=...) を実務で使い分ける
DDEVELOPER
開発7 分で読める

正規表現の先読み・後読み完全ガイド — (?=...) と (?<=...) を実務で使い分ける

正規表現の先読み(lookahead)後読み(lookbehind)は、マッチ位置を判定するためだけに使い、結果の文字列には含めないという特殊な構文です。「この文字の"直後"に〜があるとき」「この文字の"直前"に〜があるとき」といった条件を、キャプチャグループを汚さずに書けるのが強みです。本記事では 4 種類の lookaround を実務パターン中心に整理します。

#正規表現#regex#JavaScript#先読み#後読み

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 で書けます。

// 数字の塊のうち、直後にワード文字(\w = [A-Za-z0-9_])が続かないものだけ
"abc123xyz 789".match(/\d+(?!\w)/g);
// ["789"]
// 解説:
//   - "123" は直後に 'x' (\w) があるため、\d+ をどう短縮しても (?!\w) が成立せず除外
//   - "789" は直後が文字列末尾(文字なし=ワード文字でない)のためマッチ

同じ結果を「直後が非ワード文字または文字列末尾」と肯定形で書くこともできます。書き手の意図がより伝わる方を選んでください。

"abc123xyz789!end 456".match(/\d+(?=[^\w]|$)/g);
// ["789", "456"]
// "123" は直後 'x' で除外、"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 対応は不可能です(IE は 2022 年 6 月にサポート終了)。

互換を気にする場合の代替:

// lookbehind: /(?<=@)[\w.-]+/
// 代替: キャプチャで取る
const m = "alice@example.com".match(/@([\w.-]+)/);
m?.[1]; // "example.com"

パフォーマンスの注意

lookaround は便利ですが、lookaround 内のパターンに入れ子量化子((a+)+ など)があると、巨大な文字列で指数的に遅くなるケースがあります(catastrophic backtracking)。lookaround それ自体は zero-width assertion で文字を消費しないため直接の原因にはなりにくく、問題は中身の量化子設計にあります。

目安:

  • 固定長の lookbehind((?<=abc)): 高速
  • 可変長の lookbehind((?<=.*abc)): 実装次第で遅い(V8は最適化あり)
  • 複数の lookahead 直列: 基本的に高速(パスワード判定など)

数MB以上の長文に対して正規表現を使う場合は、必ず実測してください。代替としてパース用ライブラリ(peggy, nearley 等)文字列 split + filterのほうが高速なこともあります。

まとめ

  • 4種類の lookaround は「位置を判定する、文字は消費しない」が基本
  • 複数の (?=...) を並べると and 条件が簡潔に書ける(パスワード検証の定番)
  • lookbehind は iOS Safari 16.4 未満で動かない。互換が必要ならキャプチャグループで代替
  • 巨大文字列では catastrophic backtracking に注意し、必ず実測
  • 書いた正規表現は本記事のツールや regex101 等で必ず動作検証する

参考文献・ソース

記事作成に関する注記

本記事は AI(大規模言語モデル)を編集補助として活用して作成しています。 公開前に編集者が内容を確認していますが、事実誤認・仕様の解釈ミス・最新情報との齟齬が含まれる可能性があります。 重要な判断を行う際は、本文中の一次ソースや公式ドキュメントを必ずご自身でご確認ください。 誤りにお気づきの場合は、お問い合わせフォームよりご連絡いただけると助かります。

🔧 関連ツール

📚 関連記事