読み込み中...
読み込み中...
読み込み中...
読み込み中...
読み込み中...
基礎編で学んだ先読み(Lookahead)に加え、後読み(Lookbehind) と 非貪欲マッチ(Lazy Quantifiers) を学びます。
後読みは「現在位置の前に特定のパターンがあるか」をチェックするゼロ幅アサーションです。
| 構文 | 名前 | 意味 |
|---|---|---|
(?<=X) | 肯定後読み | X の直後にマッチ |
(?<!X) | 否定後読み | X の直後でない位置にマッチ |
(?=X) | 肯定先読み | X の直前にマッチ(復習) |
(?!X) | 否定先読み | X の直前でない位置にマッチ(復習) |
量指定子はデフォルトで貪欲(Greedy) に動作し、できるだけ多くの文字にマッチします。量指定子の後に ? を付けると非貪欲(Lazy) になり、できるだけ少ない文字にマッチします。
| 貪欲 | 非貪欲 | 意味 |
|---|---|---|
* | *? | 0回以上(最小) |
+ | +? | 1回以上(最小) |
? | ?? | 0回または1回(最小) |
{n,m} | {n,m}? | n〜m回(最小) |
後読みと非貪欲マッチの実例
// === 後読みの例 ===
// 肯定後読み: $ の後の数値を取得
const prices = '$100 €200 $300';
console.log(prices.match(/(?<=\$)\d+/g)); // ['100', '300']
// 否定後読み: $ 以外の通貨記号の後の数値
console.log(prices.match(/(?<!\$)\d+/g)); // ['200']
// === 非貪欲マッチの例 ===
// 貪欲: 最も長いマッチ
const html = '<b>bold</b> and <b>more</b>';
console.log(html.match(/<b>.*<\/b>/)[0]);
// '<b>bold</b> and <b>more</b>'(全体にマッチ)
// 非貪欲: 最も短いマッチ
console.log(html.match(/<b>.*?<\/b>/g));
// ['<b>bold</b>', '<b>more</b>'](個別にマッチ)チャレンジ
後読みと非貪欲マッチを使って2つの関数を実装してください。
ポイント
後読み (?<=X) / (?<!X) は現在位置の「前」のパターンをチェックします。非貪欲マッチ *? / +? はできるだけ短いマッチを返します。HTMLパースでは非貪欲マッチが特に有用ですが、本格的なHTML処理にはDOMParserを使いましょう。
前提知識
この章の内容を理解するには、JavaScript基礎コースの正規表現の基礎で学ぶ正規表現リテラル、基本的なパターンマッチング、test/match/replace メソッドの基本が前提となります。
(?<name>...) で名前付きキャプチャグループを作成できます。match() の結果の groups プロパティからアクセスできます。
match() に g フラグを付けると全マッチが取得できますが、キャプチャグループの情報が失われます。matchAll() は全マッチの完全な情報(キャプチャグループ、index、groups)をイテレータで返します。
| メソッド | gフラグ | キャプチャ情報 | 戻り値 |
|---|---|---|---|
match() | なし | あり | 配列 or null |
match() | あり | なし | 文字列配列 or null |
matchAll() | 必須 | あり | イテレータ |
replace() の置換文字列で $<name> を使って名前付きキャプチャの値を参照できます。
matchAll() と replace() での名前付きキャプチャ活用
// === matchAll() の例 ===
const text = '商品A: ¥1,200 商品B: ¥3,500';
const regex = /(?<item>商品\w+): ¥(?<price>[\d,]+)/g;
for (const match of text.matchAll(regex)) {
console.log(`${match.groups.item} → ${match.groups.price}`);
}
// 商品A → 1,200
// 商品B → 3,500
// === replace() + 名前付きキャプチャ ===
const date = '2024-03-15';
const jpDate = date.replace(
/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/,
'$<y>年$<m>月$<d>日'
);
console.log(jpDate); // '2024年03月15日'
// === replace() + コールバック関数 ===
const masked = '電話: 090-1234-5678'.replace(
/(\d{3})-(\d{4})-(\d{4})/,
(_, a, b, c) => `${a}-****-${c}`
);
console.log(masked); // '電話: 090-****-5678'チャレンジ
matchAll() と名前付きキャプチャを使って、テキストからキーと値のペアを抽出する関数を実装してください。
parseKeyValues(str): 「key=value」形式のペアを全て抽出し、オブジェクトとして返す
ポイント
matchAll() は g フラグ付き正規表現で全マッチの完全な情報(キャプチャグループ含む)をイテレータで返します。名前付きキャプチャ (?<name>...) は match().groups や replace() の $<name> で活用でき、コードの可読性を大きく向上させます。
JavaScript の正規表現はデフォルトで UTF-16 コードユニット単位で動作するため、絵文字や一部の漢字(サロゲートペア)で問題が発生します。u フラグとUnicode プロパティエスケープでこれを解決できます。
| 動作 | フラグなし | u フラグあり |
|---|---|---|
| サロゲートペア | 2文字扱い | 1文字扱い |
\p{...} | エラー | 使用可能 |
. | BMP のみ | 全 Unicode |
\p{PropertyName} / \P{PropertyName} で Unicode 文字のプロパティに基づくマッチが可能です。
| パターン | マッチ対象 |
|---|---|
\p{Letter} | 全言語の文字 |
\p{Number} | 全言語の数字 |
\p{Script=Han} | 漢字(CJK統合漢字) |
\p{Script=Hiragana} | ひらがな |
\p{Script=Katakana} | カタカナ |
\p{Emoji} | 絵文字 |
u フラグの拡張版で、集合演算([A--B] 差集合、[A&&B] 積集合)が可能になります。
u フラグと Unicode プロパティエスケープ
// === u フラグの効果 ===
const emoji = '👍';
console.log(emoji.length); // 2 (サロゲートペア)
console.log(/^.$/u.test(emoji)); // true (u フラグあり)
console.log(/^.$/.test(emoji)); // false (u フラグなし)
// === Unicode プロパティエスケープ ===
const text = 'Hello 世界 こんにちは 123';
// 漢字を抽出
console.log(text.match(/\p{Script=Han}+/gu));
// ['世界']
// ひらがなを抽出
console.log(text.match(/\p{Script=Hiragana}+/gu));
// ['こんにちは']
// 全言語の文字(英字・漢字・ひらがな等)
console.log(text.match(/\p{Letter}+/gu));
// ['Hello', '世界', 'こんにちは']
// === 実践: 日本語テキストの検出 ===
function containsJapanese(str) {
return /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u.test(str);
}
console.log(containsJapanese('Hello')); // false
console.log(containsJapanese('こんにちは')); // trueチャレンジ
Unicode プロパティエスケープを使って、テキスト内の文字種別をカウントする関数を実装してください。
countCharTypes(str): テキスト内の漢字数、ひらがな数、カタカナ数をオブジェクトで返す
ポイント
u フラグを付けるとサロゲートペアを正しく1文字として扱い、\p{...} による Unicode プロパティエスケープが使えます。日本語テキストの処理には \p{Script=Han}(漢字)、\p{Script=Hiragana}(ひらがな)、\p{Script=Katakana}(カタカナ)が非常に便利です。
正規表現は強力ですが、パフォーマンスやセキュリティの面で注意すべき点があります。
ReDoS は悪意のある入力により正規表現エンジンのバックトラッキングが指数的に増大し、CPU を大量消費する攻撃です。
脆弱なパターン例:
| パターン | 問題点 |
|---|---|
(a+)+$ | ネストした量指定子 |
(a|a)+$ | 重複する選択肢 |
(a+b?)+$ | オプションを含むネスト |
対策:
/pattern/ で書く(コンパイルは1回)new RegExp() はループ外で1回だけ呼ぶ. より [a-z]、.+ より [^<]+ のように具体的に指定する^ や $ で検索範囲を限定する(?:...) を使うReDoS の脆弱パターンとパフォーマンスのベストプラクティス
// === ReDoS の例(注意: 実行しないこと!)===
// 脆弱: /(a+)+$/.test('aaaaaaaaaaaaaaaaaa!')
// → バックトラッキングが指数的に増大し、ブラウザがフリーズ
// === 安全なパターンへの書き換え ===
// 脆弱: /(a+)+$/
// 安全: /a+$/
// 脆弱: /([a-zA-Z]+)*@/
// 安全: /[a-zA-Z]+@/
// === パフォーマンス比較 ===
// NG: ループ内で毎回コンパイル
function findEmailsBad(texts) {
return texts.filter(t => new RegExp('[a-z]+@[a-z]+\\.[a-z]+').test(t));
}
// OK: ループ外でリテラルを使う
function findEmailsGood(texts) {
const regex = /[a-z]+@[a-z]+\.[a-z]+/;
return texts.filter(t => regex.test(t));
}
// === 非キャプチャグループ (?:...) ===
// キャプチャ不要なグループ化
const dateRegex = /(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2}/;
// (?:...) はグループ化のみ行い、キャプチャはしない
// → match()[1] には入らない → メモリ効率が良いチャレンジ
正規表現のベストプラクティスを適用して、安全で効率的なバリデーション関数を実装してください。
実務での活用
複雑な正規表現は「書いた本人でも1週間後には読めない」と言われるほど保守が難しいものです。名前付きキャプチャで意味を明示する、1つの巨大なパターンではなく小さなパターンを組み合わせる、テストで入出力を保証する——これらの工夫で、チームで安心して使える正規表現になります。
用語
ポイント
ReDoS を防ぐにはネストした量指定子を避け、入力長を制限します。パフォーマンスのためにはリテラル構文を使い、ループ外で定義し、具体的な文字クラスとアンカーを指定しましょう。不要なキャプチャは (?:...) で回避します。