開発者が知っておくべき 10 の正規表現テクニック
正規表現は、最初は手に負えないように感じる一方で、突然腑に落ちるツールの一つです。理解できた瞬間から、入力検証、ログ解析、検索と置換のパイプライン、URL ルーティングなど、あらゆる場面で使えるようになります。しかし、ほとんどの開発者は、文字クラス、量指定子、アンカーといった一握りの機能しか使わず、仕様の残りには手をつけません。
このガイドでは、基本を超えた 10 の正規表現機能を取り上げます。それぞれが、単純なパターンではきれいに処理できない実際の問題を解決します。すべての例は JavaScript の構文を使用していますが、ECMAScript 互換の環境でも有効です。
この記事のすべてのパターンは、Toova 正規表現テスターを使えば、セットアップコードを 1 行も書かずにテストできます。
1. 先読み: 消費せずにマッチさせる
先読みは、マッチエンジンを文字の先まで進めずに、現在の位置の後にパターンが続くこと(または続かないこと)を主張します。マッチしたテキストには、先読みが確認する内容は含まれません。
肯定先読みの構文: (?=...)
// 肯定先読み: 「bar」が後続する場合にのみ「foo」にマッチ
const re1 = /foo(?=bar)/;
re1.test('foobar'); // true
re1.test('foobaz'); // false 否定先読みの構文: (?!...)
// 否定先読み: 「bar」が後続しない「foo」にマッチ
const re2 = /foo(?!bar)/;
re2.test('foobaz'); // true
re2.test('foobar'); // false
実用的な使い方: 通貨記号が後続する場合にのみ価格の数字にマッチさせ、捕捉した値には記号を含めないようにする。あるいは、(?=.*\d) を使って、数字がどこに現れるべきかを指定せずに、パスワードに少なくとも 1 つの数字が含まれることを検証する。
先読みはゼロ幅です。文字を消費しません。同じ位置で複数の先読みを積み重ねて、複数の独立した条件を同時に強制できます。
2. 後読み: 前に何があったかを確認する
後読みは先読みの鏡像です。マッチに含めずに、現在の位置に先行するテキストを確認します。
肯定後読みの構文: (?<=...)
// 肯定後読み: 「foo」が先行する場合にのみ「bar」にマッチ
const re3 = /(?<=foo)bar/;
re3.test('foobar'); // true
re3.test('bazbar'); // false 否定後読みの構文: (?<!...)
// 否定後読み: 「foo」が先行しない「bar」にマッチ
const re4 = /(?<!foo)bar/;
re4.test('bazbar'); // true
re4.test('foobar'); // false
後読みは ECMAScript 2018 で導入され、すべての現代ブラウザと Node.js 10+ でサポートされています。一般的なユースケース: name=Alice のようなキーと値のペアの値部分を、name= の後のすべてをマッチさせて、キーをマッチに含めずに抽出することです。
注意: 先読みとは異なり、JavaScript の後読み式は可変長のパターンを含むことはできません。後読み式は固定長または有界の最大長を持つ必要があります。
3. 名前付きキャプチャグループ: 自己文書化パターン
標準のキャプチャグループは番号で参照されます: $1、$2 など。グループを追加または削除すると、下流の参照がすべて壊れます。名前付きグループは各グループにラベルを付けられるようにすることでこれを解決します。
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const m = '2026-05-10'.match(dateRe);
console.log(m.groups.year); // "2026"
console.log(m.groups.month); // "05"
console.log(m.groups.day); // "10"
名前付きグループは $<name> 経由で置換文字列内でも使用できます:
// パターン内の名前付きグループを使った後方参照
const quoteRe = /(?<q>['"]).*?\k<q>/;
quoteRe.test('"hello"'); // true
quoteRe.test('"hello''); // false
グループ名は有効な JavaScript 識別子である必要があります。グループが捕捉する内容を反映する分かりやすい名前 — year、port、protocol — を使うと、パターンはほぼ自己文書化されます。上記のように、パターン自体の中で \k<name> を使って名前付きグループを後方参照することもできます。
4. 非貪欲量指定子: 最小限にマッチさせる
デフォルトでは、量指定子(*、+、?)は貪欲で、可能な限り多くの文字にマッチします。量指定子の後に ? を追加すると非貪欲(lazy または reluctant とも呼ばれる)になり、可能な限り少ない文字にマッチします。
const html = '<a>click</a>';
// 貪欲(デフォルト)— 可能な限り長い文字列にマッチ
/<.+>/.exec(html)?.[0]; // '<a>click</a>'
// 非貪欲 — 可能な限り短い文字列にマッチ
/<.+?>/.exec(html)?.[0]; // '<a>' これは、HTML、XML、または同じ区切り文字が複数回現れる可能性のあるあらゆる形式を解析する際に重要です。貪欲なバージョンは、文字列全体にわたって最初の開始タグから最後の終了タグまでのすべてを飲み込みます。非貪欲なバージョンは、最初の有効な終了マッチで停止します。
同じことが +?(1 つ以上、lazy)と ??(0 または 1、lazy)にも当てはまります。非貪欲量指定子はマッチできるものを変えるのではなく、複数のオプションがあるときにどの有効なマッチが選択されるかを変えます。
5. 破滅的バックトラックの回避
バックトラックは、正規表現エンジンが失敗したマッチ試行から回復する方法です。パターンの別のパスを試します。ほとんどの場合、これは目に見えず速いです。しかし、特定のパターンはエンジンに指数関数的に増加する数のパスを探索させ、わずかな入力文字列に対しても Node.js プロセスを動けなくすることがあります。
古典的な危険パターンは、aaaaab のような文字列に適用される (a+)+ のような入れ子になった量指定子です。エンジンは、マッチがないと結論する前に、内側のグループと外側のグループの間で a 文字を分割するすべての可能な方法を試します。
アトミックグループ((?>...))は、エンジンに対してグループがマッチした後はそのグループにバックトラックしないように指示することで、これを防ぎます。JavaScript はアトミックグループをネイティブにサポートしていませんが、先読みで強欲な動作をエミュレートできます:
// アトミックグループなし — エンジンは (\d+) に対してバックトラックする
// アトミックグループあり — (\d+) がマッチした後はバックトラックしない
// JavaScript はアトミックグループをネイティブにサポートしませんが、
// 先読みのトリックでエミュレートできます:
const re5 = /(?=(\d+))\1(?!\d)/; // 強欲量指定子 \d++ をエミュレート より安全な経験則: 特定の理由がない限り、他の量指定子の直接内部に入れ子になった量指定子を避けます。マッチする内容についてより正確になるようにパターンを書き直します。リファクタリング中に、テキスト差分ツールを使って 2 つの同等なパターンの出力を並べて比較することもできます。
6. 文字クラスの集合演算 (Unicode v フラグ)
ECMAScript 2024 は v フラグを導入しました。これは文字クラス内で集合演算を有効にします。これにより、扱いにくい選択肢の代わりに、「母音以外のすべての文字」または「ASCII でもある大文字」を簡潔なクラス定義として表現できます。
// POSIX 文字クラスの減算は JS にはありませんが、
// Unicode セットモード(`v` フラグ)が集合演算を追加します:
const lettersNoVowels = /[a-z--[aeiou]]/v;
lettersNoVowels.test('b'); // true
lettersNoVowels.test('e'); // false v フラグは文字クラス内で 3 つの演算をサポートします:
- 差集合:
[A--B]— A に含まれるが B に含まれない文字 - 共通部分:
[A&&B]— A と B の両方に含まれる文字 - 合併:
[AB]— A または B に含まれる文字(標準の文字クラスと同じ)
Node.js 20+ およびすべてのエバーグリーンブラウザは v フラグをサポートしています。これは u フラグのスーパーセットです。両方を組み合わせないでください。その機能が必要な場合は v 単独で使用してください。
7. 単語境界: 単語全体のマッチ
\b アンカーは、単語文字(\w)と非単語文字の間の位置にマッチします。文字を消費せず、位置を主張するだけです。その逆である \B は、単語境界ではない任意の位置にマッチします。
const sentence = 'cat concatenate';
// \b なし — 「cat」が「concatenate」の中にも見つかる
/cat/g.exec(sentence); // 「cat」と「concatenate」の両方でマッチ
// \b あり — 単語全体の「cat」のみ
/\bcat\b/g.exec(sentence); // 独立した「cat」のみマッチ // \B は逆: 単語内でマッチし、境界ではマッチしない
/\Bcat\B/.test('concatenate'); // true — 「cat」は単語の内側にある
単語境界は、コードや散文の中で識別子を検索するときに不可欠です。境界がないと、id という名前の変数を検索すると、indexOf、invalid、grid もヒットしてしまいます。\bterm\b を使って、独立した出現にマッチを制限します。
重要な注意点: \b は JavaScript の単語文字の定義([a-zA-Z0-9_])を使用します。アクセント付き文字や非ラテン文字は非単語文字として扱われます。Unicode 対応の単語境界には、v フラグと Unicode プロパティクラスを組み合わせます。
8. マルチラインモード: 行ごとのアンカー
デフォルトでは、^ は文字列の先頭のみ、$ は文字列の末尾のみにマッチします。m(マルチライン)フラグはこれを変更します: ^ は各行の先頭に、$ は各行の末尾にマッチします。
const text = 'line one\nline two\nline three';
// m フラグなし — ^ は文字列全体の先頭のみマッチ
/^line/.test(text); // true (最初の行のみ)
// m フラグあり — ^ は各行の先頭にマッチ
const matches = text.match(/^line/gm);
console.log(matches); // ['line', 'line', 'line'] これは、ログファイル、設定ファイル、またはコードのような複数行のテキストを処理するときに不可欠です。一般的な用途には、キーワードで始まる行の抽出、行末トークンの置換、ブロック内の各行がパターンに一致することの検証が含まれます。
m フラグを s フラグ(dotAll)と混同しないでください。s フラグは . も改行文字にマッチさせます。m フラグは . にはまったく影響しません。^ と $ の動作のみに影響します。
9. Unicode プロパティエスケープ: 国際的な文字のマッチ
u フラグは Unicode プロパティエスケープを有効にし、Unicode のカテゴリ、スクリプト、その他のプロパティに基づいて文字をマッチさせられます。これは、ASCII だけでなく、すべての人間の書記体系で文字、数字、句読点をマッチさせる正しい方法です。
// u フラグは Unicode プロパティエスケープを有効化
const letters = /\p{L}+/u;
letters.test('Héllo'); // true
letters.test('你好'); // true
letters.test('12345'); // false
// すべてのスクリプトの大文字のみマッチ
const upper = /\p{Lu}+/u;
upper.test('ABC'); // true
upper.test('abc'); // false // 絵文字にマッチ(Unicode 一般カテゴリ: Symbol, Other)
const emoji = /\p{So}/u;
emoji.test('🚀'); // true 最もよく使われる Unicode プロパティは次のとおりです:
\p{L}— 任意の文字(すべてのスクリプト)\p{Lu}— 大文字\p{Ll}— 小文字\p{N}— 任意の数字\p{Nd}— 十進数字\p{P}— 句読点\p{Script=Latin}— ラテンスクリプト文字\p{Emoji}— 絵文字
指定したプロパティを持たないすべてにマッチさせるには \P{...}(大文字 P)を使って否定します。サポートされる Unicode プロパティとその値の完全なリストは、MDN ドキュメントで管理されています。
10. 文字列組み立てによる詳細パターン
多くの正規表現フレーバー(Python、Ruby、.NET、PCRE)は、パターン内に空白とコメントを許可する verbose または拡張モード(x フラグ)をサポートします。JavaScript にはこのフラグがありません。x フラグは ECMAScript では有効ではありません。
標準的な回避策は、名前付き文字列定数からパターンを組み立て、new RegExp() で結合することです:
// JavaScript にはネイティブの x フラグはありませんが、
// 読みやすいパターンを名前付き文字列定数として組み立てられます:
const YEAR = '(?<year>\\d{4})';
const SEP = '-';
const MONTH = '(?<month>\\d{2})';
const DAY = '(?<day>\\d{2})';
const datePattern = new RegExp(YEAR + SEP + MONTH + SEP + DAY); 各定数は何にマッチするかを説明し、最終的なパターンは文のように読めます。このアプローチは、コードベース内の複数の正規表現定義にまたがる共有サブパターンの構成を容易にし、各コンポーネントを独立してユニットテストできるようにします。
あまり複雑でないパターンの場合、近くのブロックコメントにインラインコメントを付けて、正規表現全体を 1 行に保つことで十分なことがよくあります。目標は、次の開発者(将来の自分を含む)がパターンをデコードせずに理解できるようにすることです。
すべてをまとめる
これら 10 のテクニックは、「なぜこの正規表現はエッジケースで失敗するのか?」という表面の大部分をカバーします。先読みと後読みは、消費せずにコンテキストを主張できるようにします。名前付きグループは、リファクタリングを通じてパターンを読みやすく保ちます。非貪欲量指定子は、偶発的な過剰マッチを防ぎます。Unicode プロパティエスケープは、ASCII を超えた入力を処理します。
流暢になる最良の方法は、実際のデータに対して実際のパターンを試すことです。正規表現テスターを使って素早く反復します。パターンが差分やクリーンアップを必要とする出力を生成する場合、テキスト差分ツールが実行間で何が変わったかを正確に示します。そして正規表現が JSON を解析している場合、JSON フォーマッタでブラウザを離れずに構造化された結果を検査できます。
すべての JavaScript 正規表現構文とフラグの包括的なリファレンスとして、MDN 正規表現チートシートはブックマークするべき最良の単一ページです。すべてのマッチを視覚化したインタラクティブなパターンデバッグには、regex101.com が JavaScript モードをサポートし、パターン内の各コンポーネントの組み込み説明を提供します。