N2
NanToo
開発7 分で読める

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

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

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

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 編集部

本記事は NanToo(ナンツー)運営の株式会社ヨネマスが編集・公開しています。 ツールの開発現場で得た知見をもとに、実務で役立つ内容を発信しています。

🔧 関連ツール

📚 関連記事

AD