正規表現チートシート:メタ文字・グループ・先読み/後読み完全リファレンス
正規表現は、テキストにマッチさせるための小さなパターン言語だ。\d+ は「1 桁以上の数字」、^Error は「Error で始まる行」を意味する。やることはこれだけ。このページには、メタ文字、量指定子、アンカー、グループ、Lookaround、フラグの構文を 1 枚に詰め込み、JavaScript と Python にそのまま貼って動く 15 個以上のパターンを置いた。
対象は、文字列とは何かを既に理解していて、入門ツアーではなくリファレンスを求める開発者だ。記号だけ確認したい場合は「クイックリファレンス表」へ。サーバーが正規表現でハングした経験があるなら、Lookaround と落とし穴の節を読んでほしい。
1. 正規表現とは何か、なぜ 2026 年でも必要なのか
正規表現とは、文字列を走査して成功か失敗を返す状態機械にコンパイルされるパターンだ。文法は小さいが、用途は小さくない。
AI にパターンを書かせることはできる。それでも、人間が手で正規表現を書く価値が今も残っている仕事は次のあたりだ。
- ログ解析。
nginxのアクセスログ 1,000 万行から、特定のユーザーエージェントで 5xx を返したリクエストを全部抜き出す。grep -Eに 40 文字の正規表現を渡せば数秒で終わるが、1 行ごとに LLM を呼ぶ方法ではそうはいかない。 - フォームとフィールドのバリデーション。電話番号、郵便番号、ISO タイムスタンプ、ライセンスキー。パターンは入力欄の隣に置かれ、ブラウザ側でキー入力ごとに走る。
- 一括 find-and-replace。1,000 ファイルにまたがるリファクタリングで、名前をキャプチャして注入し直す必要があるとき。
sed、ripgrep、エディタの「ファイル全体で置換」はどれもネイティブに正規表現を理解する。
同じツールボックスの JSON 側を扱う場合は、jq チートシート:実戦で使える 30 個の JSON コマンドライン・パターンを参照。
1.1 正規表現パターンの読み方(5 秒で身につく入門)
ほとんどのパターンは、左から右へトークンを 1 つずつ追うと読みやすい。例として ^[A-Z]\w+\d{2,4}$ を見てみよう。
^は文字列の先頭にマッチを固定する。[A-Z]は大文字 1 文字にちょうどマッチする。\w+は単語構成文字(英字、数字、アンダースコア)に 1 文字以上マッチする。\d{2,4}は 2 桁から 4 桁の数字にマッチする。$は文字列の末尾に固定する。
このパターン全体としては Order42 や Job1999 にマッチする。コツは、まずアンカー、次に文字クラス、続いて量指定子、最後に境界の順で読むことだ。
2. クイックリファレンス表
多くの読者が探しているのはこの節だ。必要な部分をコピーしてほしい。
メタ文字
| パターン | マッチするもの |
|---|---|
. | 改行以外の任意の文字(s/dotall フラグを付ければ改行も含む) |
\d | 数字([0-9]。u フラグ付きで Unicode 数字全般) |
\D | 数字以外 |
\w | 単語構成文字([A-Za-z0-9_]) |
\W | 単語構成文字以外 |
\s | 空白文字(スペース、タブ、改行など) |
\S | 空白以外 |
量指定子
| パターン | マッチするもの |
|---|---|
* | 0 回以上(貪欲) |
+ | 1 回以上(貪欲) |
? | 0 回または 1 回(貪欲) |
{n} | ちょうど n 回 |
{n,m} | n 回から m 回 |
{n,} | n 回以上 |
*?, +?, ??, {n,m}? | 各量指定子の遅延(lazy)版 |
アンカー
| パターン | マッチするもの |
|---|---|
^ | 文字列の先頭(m フラグなら各行の先頭) |
$ | 文字列の末尾(m フラグなら各行の末尾) |
\b | 単語境界 |
\B | 単語境界以外 |
\A | 文字列の絶対的な先頭(Python) |
\Z | 文字列の絶対的な末尾(Python) |
文字クラス
| パターン | マッチするもの |
|---|---|
[abc] | a、b、c のいずれか |
[^abc] | a、b、c 以外 |
[a-z] | 任意の小文字 |
[0-9] | 任意の数字 |
\p{L} | 任意の Unicode 文字(JS は u フラグ、Python re はデフォルト) |
グループ
| パターン | マッチするもの |
|---|---|
(...) | キャプチャグループ |
(?:...) | 非キャプチャグループ |
(?<name>...) | 名前付きキャプチャ(JS は ES2018+)。Python は (?P<name>...) |
\1, \2 | グループ 1、2 への後方参照 |
Lookaround
| パターン | マッチするもの |
|---|---|
(?=...) | 肯定先読み |
(?!...) | 否定先読み |
(?<=...) | 肯定後読み |
(?<!...) | 否定後読み |
フラグ
| フラグ | 効果 |
|---|---|
i | 大文字小文字を区別しない |
m | マルチライン:^ と $ を各行に適用 |
s | dotall:. が改行にもマッチ |
g | グローバル(JS)。全マッチを取得 |
u | Unicode モード |
y | sticky(JS)。lastIndex に固定 |
3. メタ文字と文字クラス
3.1 リテラルと特殊文字
ほとんどの文字はリテラルだ。それ自身としてマッチさせたいときにエスケープがいるメタ文字は次の 12 個。
. ^ $ * + ? ( ) [ ] { } | \
. のエスケープ忘れは、正規表現で最もよくあるバグだ。\. はリテラルのドットにマッチする。文字クラスの中では [.] もリテラルのドットだ。[...] の内部では、ほとんどのメタ文字は特殊な意味を失う。例外は ]、\、先頭の ^、中間に置いた - の 4 つ。
3.2 文字クラスの省略記法
省略記法は単純に見えるが、Unicode が絡むと話が変わってくる。
// JavaScript — u フラグなしでは \d は ASCII のみ
/\d/.test('5'); // true
/\d/.test('٥'); // false(アラビア・インド数字)
/\d/u.test('٥'); // false — JS では u を付けても \d は ASCII のまま
/\p{N}/u.test('٥'); // true — \p{N} が Unicode 対応の数字クラス
# Python — re モジュールは \d をデフォルトで Unicode として扱う
import re
re.match(r'\d', '٥') # <Match span=(0, 1)>
re.match(r'(?a)\d', '٥') # None — (?a) で ASCII を強制
英語 ASCII の入力しか扱わないなら、\d と [0-9] は同じものとして使える。アクセント付きの名前をユーザーが貼り付けた瞬間、\w よりも \p{L} を選びたくなる。
3.3 カスタム文字クラス
// JavaScript
/[A-Za-z][A-Za-z0-9_-]{2,29}/.test('valid_handle-1'); // true
// 否定と範囲の組み合わせ
/[^aeiou\s]/g // 母音でも空白でもない任意の文字
Unicode カテゴリでは、\p{L} が「任意の文字」、\p{N} が「任意の数字」、\p{Script=Han} が「任意の漢字」を表す。JavaScript では u フラグが必要だ。Python では \p{...} は標準ライブラリの re ではなく、PyPI の regex パッケージでのみサポートされる。
コマンドラインで作業するなら、POSIX 文字クラスを目にすることもある。
| POSIX クラス | マッチするもの | ASCII 等価 |
|---|---|---|
[[:alpha:]] | 英字 | [A-Za-z] |
[[:digit:]] | 数字 | [0-9](\d) |
[[:alnum:]] | 英字 + 数字 | [A-Za-z0-9] |
[[:space:]] | 空白文字 | \s |
[[:upper:]] | 大文字 | [A-Z] |
[[:lower:]] | 小文字 | [a-z] |
POSIX クラスは grep -E や sed -E で動く。JavaScript や Python の re では動かないので、代わりに \d、\s、\w を使う。
4. 量指定子と greedy vs lazy
4.1 基本の量指定子
/a*/.exec('aaab') // ['aaa'] — 0 回以上
/a+/.exec('aaab') // ['aaa'] — 1 回以上
/a?/.exec('aaab') // ['a'] — 0 回または 1 回
/a{2,3}/.exec('aaaab') // ['aaa'] — 2 回から 3 回
4.2 貪欲と遅延
デフォルトでは量指定子は貪欲(greedy)で、可能な限り取り込んだあと、全体がマッチするように後退する。? を付けると遅延(lazy)に切り替わる。
const html = '<p>one</p><p>two</p>';
html.match(/<p>.*<\/p>/)[0]; // '<p>one</p><p>two</p>' (貪欲なので両方飲み込む)
html.match(/<p>.*?<\/p>/)[0]; // '<p>one</p>' (遅延は最初の </p> で止まる)
タグや引用文字列を取り出すときは、遅延版がほぼ常に正解だ。さらに良いのは . を使わず、否定文字クラスにしてしまうこと。<p>[^<]*</p> は <p>.*?</p> より速い。バックトラックする余地がそもそもない。
4.3 壊滅的バックトラック
サーバーをハングさせる原因はこいつだ。量指定子を別の量指定子の内側にネストし、マッチ範囲が曖昧に重なるようにすると、エンジンは諦めるまでに指数関数的な数の経路を探索する。
// やってはいけない例
/(a+)+b/.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'); // 数秒かかる
41 個の a のあとに ! が続く場合、エンジンは b が無いと判断するまでにおよそ 2^41 通りの分割点を試す。直し方はいくつかある。
- パターンを平坦化する。
/a+b/ならネストなしで同じ仕事をする。 - アトミックグループを使う(Python の
regex、PCRE、Java、Ruby)。(?>a+)+bと書けば、a+が一度マッチしたあとはエンジンがその内側へ戻れなくなる。 - エンジンを切り替える。Go の
regexp、RE2、Rust のregexクレートは線形時間の NFA を使い、設計上、壊滅的バックトラックが起こり得ない。
JavaScript と Python の re はどちらもバックトラックを行い、標準ライブラリにはアトミックグループが無い(Python の PyPI 版 regex には備わっている)。入力長を自分で握れる場面ならこれで構わない。入力がユーザー由来なら、まず長さを検証するか、RE2 でプリコンパイルすること。
5. アンカーと単語境界
5.1 ^ と $
デフォルトでは、^ は入力全体の先頭、$ は末尾を指す。m(マルチライン)フラグを付けると、各行の先頭と末尾を指すようになる。
const log = 'INFO start\nERROR boom\nINFO done';
log.match(/^ERROR.*/); // null (単行モードでは ^ はインデックス 0 にしかマッチしない)
log.match(/^ERROR.*/m); // ['ERROR boom']
5.2 \b と \B
\b はゼロ幅のアサーションで、単語構成文字(\w)と非単語文字の境目にマッチする。完全一致の単語検索に向く。
/\bcat\b/.test('the cat sat'); // true
/\bcat\b/.test('concatenate'); // false
単語境界の判定は \w に基づいており、デフォルトでは ASCII 準拠だ。中国語、日本語、韓国語のテキストには単語間にスペースが入らないため、\b は単語の切れ目を検出できない。形態素解析器(jieba、MeCab)を正規表現の代わりではなく、正規表現の前段として用意する必要がある。
5.3 マルチラインモード
import re
text = "INFO ok\nERROR fail\nINFO done\n"
re.findall(r'^ERROR.*$', text) # []
re.findall(r'^ERROR.*$', text, re.MULTILINE) # ['ERROR fail']
JavaScript で同じことをするなら text.match(/^ERROR.*$/gm) になる。m と g を組み合わせれば、該当する行をすべて取得できる。
6. グループ、キャプチャ、後方参照
6.1 キャプチャグループ
丸括弧は 2 つの役割を兼ねる。一つはサブパターンをまとめて量指定子を効かせること、もう一つはマッチ結果をあとで使うためにキャプチャすることだ。
'2026-05-13'.match(/(\d{4})-(\d{2})-(\d{2})/);
// ['2026-05-13', '2026', '05', '13', index: 0, ...]
グループには、開き括弧の出現順に左から右へ 1 から番号が振られる。
6.2 非キャプチャグループ
グループ化だけしたくてキャプチャは要らないときは (?:...) を使う。こちらの方が速いし、番号付きグループの一覧が散らからない。
/(?:https?):\/\/(\S+)/.exec('see https://go-tools.org');
// ['https://go-tools.org', 'go-tools.org']
// プロトコルはグループ化されるがキャプチャされない。グループ 1 はホスト名
6.3 名前付きグループ
グループに名前を付けると、パターンが読みやすくなり、リファクタリングにも強くなる。
// JavaScript (ES2018+)
const m = '2026-05-13'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
m.groups.year; // '2026'
# Python — 構文は (?P<...>) であることに注意
import re
m = re.match(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', '2026-05-13')
m.group('year') # '2026'
6.4 後方参照
後方参照を使うと、パターン内で前のキャプチャと同じ文字列をもう一度マッチさせられる。
// 連続して繰り返される任意の文字を見つける
'bookkeeper'.match(/(\w)\1/g); // ['oo', 'kk', 'ee']
// 同名で対になる HTML タグをマッチさせる
const tag = /<(\w+)>(.*?)<\/\1>/;
'<b>bold</b>'.match(tag);
// ['<b>bold</b>', 'b', 'bold']
Python では、\1 はパターンと置換文字列の両方で使える。名前付き参照はパターン内では (?P=name)、re.sub の置換側では \g<name> と書く。
7. Lookaround:先読みと後読み
Lookaround はゼロ幅アサーションだ。文字を消費せずに条件を確かめるので、複数つなげて使える。
7.1 先読み
// パスワード:8 文字以上、数字 1 つ、大文字 1 つ、小文字 1 つを含む
const strong = /^(?=.*\d)(?=.*[A-Z])(?=.*[a-z]).{8,}$/;
strong.test('Hunter2!'); // true
strong.test('hunter2!'); // false (大文字なし)
// 否定先読み:.tmp でないファイル名
/^[\w-]+(?!\.tmp$)\.[a-z]+$/.test('report.csv'); // true
7.2 後読み
後読みはその裏返しで、現在位置の手前にあるものをアサートする。
// 通貨記号の後ろの価格を抽出。数字だけ残して $ は捨てる
'price: $42.50'.match(/(?<=\$)\d+(\.\d+)?/); // ['42.50', '.50']
// 否定後読み:Bond にはマッチするが James Bond にはマッチしない
'Mr. Bond'.match(/(?<!James )Bond/); // ['Bond']
'James Bond'.match(/(?<!James )Bond/); // null
7.3 JavaScript と Python の後読みの違い
両エンジンの食い違いが大きく、パターンをそのまま移植すると壊れる代表的なポイントだ。
| エンジン | 後読みの幅 |
|---|---|
| JavaScript (V8、SpiderMonkey、JSC 16.4+) | ES2018 以降は可変幅対応。(?<=\d+) は有効。 |
Python 標準ライブラリ re | 固定幅のみ。(?<=\d+) は error: look-behind requires fixed-width pattern を投げる。 |
Python の PyPI regex パッケージ | 可変幅対応。import regex; regex.search(r'(?<=\d+)abc', '12abc')。 |
Python での回避策は 2 通り。固定の繰り返し回数で書き直す((?<=\d{3}))か、プレフィックスをキャプチャしてマッチ後にスライスで切り落とす。
8. フラグと修飾子
8.1 i — 大文字小文字を区別しない
/error/i.test('FATAL ERROR'); // true
re.search(r'error', 'FATAL ERROR', re.IGNORECASE) # <Match span=(6, 11)>
8.2 m と s
m は ^ と $ を各行のアンカーに切り替える。s(dotall)は . を改行にもマッチさせる。両者は独立した設定なので、両方欲しければ並べて指定すればいい。
/<script>(.*?)<\/script>/s.exec('<script>\nalert(1)\n</script>')[1];
// '\nalert(1)\n' // s がないと . が改行を拒む
8.3 g — グローバル
JavaScript では、g はマッチ自体ではなく API の挙動を変える。g 無しの String.match はキャプチャグループを返すが、g 付きでは全マッチ文字列の配列を返す。全マッチでキャプチャグループも維持したい場合は matchAll を使う。
const text = 'a=1 b=2 c=3';
text.match(/(\w)=(\d)/); // 最初のマッチ、グループ付き
text.match(/(\w)=(\d)/g); // ['a=1', 'b=2', 'c=3'] (グループなし)
[...text.matchAll(/(\w)=(\d)/g)]; // 全マッチ、グループ付き
Python に g はない。re.findall、re.finditer、re.sub がグローバル版にあたる。
8.4 u — Unicode と \p{...}
// 漢字(中国語、日本語の漢字)にマッチ
/\p{Script=Han}+/gu.test('Hello 世界'); // true
// 絵文字(拡張ピクトグラフィック)にマッチ
/\p{Extended_Pictographic}/u.test('👋'); // true
Python では Unicode がデフォルトで有効だ。漢字の範囲なら re.findall(r'[一-鿿]+', text) が等価。Unicode プロパティエスケープを丸ごと使いたい場合は、PyPI の regex パッケージで regex.findall(r'\p{Script=Han}+', text) と書く。
9. 日常的に使う頻出パターン
9.1 メールアドレスの検証
どのバージョンが必要か、最初に正直に決めること。
// 95% パターン。フォームバリデータの大半が使うやつ
const email = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
email.test('a@b.co'); // true
// 「RFC 5322 にできる限り近づけたい」パターン
const rfc = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
正直に言うと、RFC 5322 のフル仕様を純粋な正規表現で書くと 6,000 文字近くなる上、それでもエッジケースで間違える。95% パターンを使い、あとは確認メールを送る。実用で通る検証はそれだけだ。
9.2 URL の抽出
const urlPattern = /https?:\/\/[^\s<>"]+/g;
const found = 'See https://example.com/a?b=1 and http://x.io'.match(urlPattern);
// ['https://example.com/a?b=1', 'http://x.io']
URL を抽出すると、たいていクエリ文字列を覗きたくなる。URLエンコード・デコード オンラインツールに貼り付ければ、パーセントエンコードされたパラメータをそのまま読める。エンコードとデコードの使い分けについてはURLエンコード・デコード実践ガイドを参照。
9.3 電話番号
// E.164:国際形式、+ は任意、国番号は 1〜3 桁
const e164 = /^\+?[1-9]\d{1,14}$/;
e164.test('+14155551234'); // true
// 北米番号計画(区切り記号あり)
const nanp = /^(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/;
nanp.test('(415) 555-1234'); // true
「形式として妥当か」以上のことをやるなら libphonenumber を使う。市外局番が実在するかどうかまでは正規表現で検証できない。
9.4 IPv4 と IPv6
// IPv4:各オクテットを 0〜255 で厳密に
const ipv4 = /^((25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(25[0-5]|2[0-4]\d|1?\d?\d)$/;
ipv4.test('192.168.1.1'); // true
ipv4.test('999.0.0.1'); // false
// IPv6:簡略版。RFC 4291 完全形だと約 600 文字になる
const ipv6simple = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
ipv6simple.test('2001:0db8:85a3:0000:0000:8a2e:0370:7334'); // true
:: の省略形、埋め込み IPv4、ゾーン識別子まで含めた本物の IPv6 を扱うなら、node:net の isIP() か Python の ipaddress.ip_address() に任せる。純粋な正規表現で書くのは一種の通過儀礼で、その後はだいたい保守の重荷になる。
9.5 ISO 8601 の日付とタイムスタンプ
// 日付のみ:YYYY-MM-DD
const isoDate = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
isoDate.test('2026-05-13'); // true
// 日付 + 時刻 + 任意の小数秒 + Z またはオフセット
const iso = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
iso.test('2026-05-13T09:30:00.123Z'); // true
ISO 8601 は単純に見えて罠だらけだ。うるう秒、週日付(2026-W19)、年内通算日(2026-133)あたりがすぐ顔を出す。エポック秒対ミリ秒やタイムゾーンのずれについては、Unixタイムスタンプ完全ガイドを参照。
10. 正規表現を使った検索・置換ワークフロー
10.1 JavaScript — String.replace と $1
// 米国式日付を組み替え:MM/DD/YYYY -> YYYY-MM-DD
'05/13/2026'.replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$1-$2');
// '2026-05-13'
// 置換が条件付きならコールバックを使う
'price 42 dollars'.replace(/(\d+) dollars/, (_, n) => `$${n}`);
// 'price $42'
$1、$2、… は番号付きグループへの参照だ。$<name> は名前付きグループ、$& はマッチ全体、$$ はリテラルの $ を表す。
10.2 Python — re.sub と \1 とコールバック
import re
# 同じ日付の組み替え
re.sub(r'(\d{2})/(\d{2})/(\d{4})', r'\3-\1-\2', '05/13/2026')
# '2026-05-13'
# コールバック:文字列内のメールアドレスをすべて大文字化
def upper_email(m):
return m.group(0).upper()
re.sub(r'[\w.-]+@[\w.-]+', upper_email, 'mail me at hi@go-tools.org')
# 'mail me at HI@GO-TOOLS.ORG'
置換側では、Python は \1 または \g<name> を使う。raw 文字列の r'...' プレフィックスを忘れると \1 がリテラル文字になってしまうので注意。
10.3 CLI:sed、grep、ripgrep、jq
コマンドラインでの一括リファクタリングでは、正規表現はスクリプトからシェルへ場所を移す。
# ripgrep:名前付きの TODO をすべて見つける
rg -n '\bTODO\(([^)]+)\)' --replace 'TODO(\1)'
# grep -E にアンカー:auth.log のログイン失敗行
grep -E '^[A-Z][a-z]{2} +[0-9]+ .*Failed password' /var/log/auth.log
# sed:ツリー全体に対して末尾空白を一括削除
find . -name '*.md' -print0 | xargs -0 sed -i -E 's/[[:space:]]+$//'
ripgrep は Rust の regex クレート(RE2 系、線形時間、後読みなし)を使う。grep -E と sed -E は POSIX 拡張正規表現で、\d が無いので [0-9] や [[:digit:]] を使う。データが JSON のときは正規表現ではなく jq に切り替える。並列リファレンスとしてjq チートシートを参照。
11. よくある落とし穴
11.1 . のエスケープ忘れ
実際に出荷してしまったバグの話。ログのリダクタが IP アドレスをマスクするはずだった。
// 間違い:'192a168b1c1' にもマッチしてしまう
/(\d+).(\d+).(\d+).(\d+)/.test('192a168b1c1'); // true
// 正しい
/(\d+)\.(\d+)\.(\d+)\.(\d+)/.test('192a168b1c1'); // false
文字クラスの中では . はすでにリテラル扱いなので、[.] と \. のどちらでも動く。それ以外の場所では必ずエスケープする。
11.2 貪欲な .* が食べすぎる
'<a href="x"><b>bold</b></a>'.match(/<(.*)>/)[1];
// 'a href="x"><b>bold</b></a' // 丸ごと飲み込まれる
貪欲な .* は文字列の末尾まで走ってから、> がマッチするところまで後退する。後退先は入力の最後の > になる。遅延(.*?)にするか、速くて意図も明瞭になる否定文字クラス([^>]*)を使う。
11.3 マルチラインアンカー
よくある混乱:^ と $ はデフォルトでは改行文字にはマッチしない。マッチするのは入力全体の先頭と末尾の位置だ。これを各行のアンカーに変えるのが m フラグ、. を改行越しに使えるようにするのが s フラグ。この 2 つは独立した設定なので、ログ解析ではたいてい両方欲しくなる。
11.4 ReDoS とその防ぎ方
ReDoS(regex denial of service、正規表現サービス拒否)は、壊滅的バックトラックの本番環境版だ。対策は次のあたり。
- 静的解析。
safe-regex、recheck、ESLint のno-misleading-character-classなどのツールが、危険なパターンが出荷される前に検出する。 - アトミックグループ(Python の
regex、PCRE、Ruby、Java)。(?>...)を使うとエンジンがグループに後戻りで再入できなくなる。 - 強欲(possessive)量指定子(PCRE/Java の
*+、++、?+)。考え方は同じで、より短い構文。 - 非バックトラック型エンジンに切り替える。Go の
regexp、RE2、Rust のregexクレート、Python バインディングのre2はすべて線形時間で動く。野生で最も普及している RE2 デプロイは ripgrep だ。 - まず入力長を検証する。10 KB の正規表現爆弾はバグだが、入力に 10 バイト上限を掛けるのは 1 行のコードだ。
正規表現と組み合わせて使う日常ツール(フォーマッター、デコーダ、コンバータ)の一覧は、初心者ガイド:開発者ツールで作業効率を劇的に向上させる方法を参照。
複雑なパターンを出荷する前に、インタラクティブにテストしておくこと。regex101.com は PCRE、JavaScript、Python、Go の方言を切り替えられ、各トークンの意味を解説し、バックトラックの様子まで表示してくれる。壊滅的なパターンの発見にも役立つ。
12. FAQ
正規表現の * と + の違いは?
* は 0 回以上の繰り返しにマッチする(空文字列にもマッチしうる)。+ は 1 回以上の繰り返しで、少なくとも 1 回は要る。a* は ''、'a'、'aaaa' にマッチする。a+ は 'a' と 'aaaa' にはマッチするが '' にはマッチしない。
正規表現で複数行をまたいでマッチさせるには?
マルチラインフラグを有効にする。JavaScript なら /.../m、Python なら re.MULTILINE だ。これで ^ と $ が各行に効くようになる。. も改行をまたぐようにするには、dotall フラグを追加する(JavaScript は s、Python は re.DOTALL)。
JavaScript と Python の正規表現は同じ?
コア構文(量指定子、アンカー、文字クラス、基本のグループ)の 90% は共通だ。実質的な差は 2 点ある。JavaScript(ES2018+)は可変幅の後読みをサポートし、名前付きグループを (?<name>...) と書く。Python 標準ライブラリの re は固定幅の後読みのみで、(?P<name>...) と書く。Python で可変幅の後読みが欲しい場合は、PyPI の regex パッケージをインストールする。
正規表現で壊滅的バックトラックが起きるのはなぜ?
(a+)+ や (a|a)* のように、量指定子をネストしてマッチ範囲が重なっているからだ。末尾付近で「ほぼマッチするが失敗する」入力に当たると、エンジンは内側の量指定子の分割を片端から試す。経路数は指数関数的に膨らむ。アトミックグループ (?>a+)+、強欲量指定子 a++、または RE2 や Go の regexp のような非バックトラック型エンジンへの切り替えで解決する。
JavaScript で後読みは使える?
使える。肯定後読み (?<=...) と否定後読み (?<!...) は、ES2018 以降 V8(Chrome、Node.js)、SpiderMonkey(Firefox)、JavaScriptCore(Safari 16.4+)に搭載されている。可変幅後読みもサポートされる。古い Safari 向けには、Babel でトランスパイルするか、new RegExp を try/catch で囲んで機能検出する。
正規表現でリテラルのドット . にマッチさせるには?
バックスラッシュでエスケープする。\. がリテラルのドットにマッチする。文字クラスの中ではドットはすでにリテラル扱いなので、[.] と [\.] のどちらでも動く。クラスの外でエスケープしない . は「改行以外の任意の文字」を意味するメタ文字だ(dotall フラグ付きなら任意の文字すべて)。
正規表現の \s は何を意味する?
\s は任意の空白文字、つまりスペース、タブ(\t)、改行(\n)、復帰(\r)、垂直タブ、フォームフィードにマッチする。Unicode モードでは NBSP にもマッチする。\S はその反対だ。
正規表現は大文字小文字を区別する?
デフォルトでは区別する。/cat/ は Cat にはマッチしない。JavaScript では i フラグ(/cat/i)、Python では re.IGNORECASE または (?i) を使う。