はじめに
正規表現(Regular Expression、略して regex または regexp)とは、検索パターンを定義する文字列のシーケンスです。正規表現は開発者のツールキットの中で最も強力なツールのひとつであり、単一のコンパクトな表現でテキストの検索・検証・抽出・変換を行うことができます。
Webフォームでのメールアドレス検証、ログからのデータ抽出、数千のファイルにわたる複雑な検索・置換など、あらゆる場面で正規表現は活躍します。ほぼすべてのプログラミング言語と主要なテキストエディタが理解できるコンパクトな記法で、必要なものを正確に表現できます。
正規表現の歴史
正規表現の歴史は理論計算機科学の基礎に遡ります:
- 1951年 — 数学者 Stephen Kleene が自動機理論の研究の一環として正規言語の概念を形式化し、Kleene スター(
*)記法を導入。 - 1968年 — Ken Thompson がQEDテキストエディタに正規表現を実装し、その後
grep、sed、awkなどの Unix ツールに導入。 - 1986年 — POSIX が BRE(基本正規表現)と ERE(拡張正規表現)の2つの方言を標準化し、Unix システム間の相互運用性を確保。
- 1997年 — Philip Hazel が PCRE(Perl Compatible Regular Expressions)ライブラリを作成。lookahead、lookbehind、名前付きキャプチャなどの強力な機能を提供。
- 1999年 — ECMAScript 3 が JavaScript の
RegExpオブジェクトを標準化。 - 2015年 — ES6 が
u(Unicode)フラグとy(sticky)フラグを追加。 - 2018年 — ES2018 が名前付きキャプチャグループ(
(?<name>...))とlookbehindアサーションを追加。
POSIX vs PCRE vs JavaScript RegExp の比較
| 機能 | BRE/ERE (POSIX) | PCRE | JavaScript RegExp |
|---|---|---|---|
| Lookahead | ✗ | ✓ | ✓ |
| Lookbehind | ✗ | ✓ | ✓(ES2018+) |
| 名前付きキャプチャ | ✗ | ✓ | ✓(ES2018+) |
| 非貪欲マッチ | ✗ | ✓ | ✓ |
| Unicode | 限定的 | ✓ | ✓(u フラグ使用時) |
| 後方参照 | ✓ | ✓ | ✓ |
コア構文リファレンス
クイックリファレンス表
| パターン | 意味 |
|---|---|
. |
改行以外の任意の文字 |
^ |
文字列の先頭 / 行頭(m フラグ使用時) |
$ |
文字列の末尾 / 行末(m フラグ使用時) |
\d |
任意の数字 [0-9] |
\D |
任意の非数字 |
\w |
単語文字 [a-zA-Z0-9_] |
\W |
非単語文字 |
\s |
空白文字(スペース、タブ、改行など) |
\S |
非空白文字 |
[abc] |
文字クラス — a、b、c のいずれかにマッチ |
[^abc] |
否定文字クラス — a、b、c 以外にマッチ |
[a-z] |
文字範囲 |
* |
0回以上(貪欲) |
+ |
1回以上(貪欲) |
? |
0回または1回(貪欲) |
{n} |
ちょうど n 回 |
{n,m} |
n 回以上 m 回以下(貪欲) |
*? +? ?? |
怠惰(非貪欲)の同等形 |
(abc) |
キャプチャグループ |
(?:abc) |
非キャプチャグループ |
(?<name>abc) |
名前付きキャプチャグループ |
| |
選択 — 左辺または右辺にマッチ |
(?=...) |
肯定的 lookahead |
(?!...) |
否定的 lookahead |
(?<=...) |
肯定的 lookbehind |
(?<!...) |
否定的 lookbehind |
文字クラス
文字クラスを使うと、文字セットから1文字にマッチさせることができます。[aeiou] は任意の母音1文字にマッチし、[a-zA-Z] は任意の英字に、[^0-9] は数字以外の任意の文字にマッチします。
よく使われる省略表記:
\dは[0-9]と同等\wは[a-zA-Z0-9_]と同等\sはスペース、タブ(\t)、改行(\n)、キャリッジリターン(\r)などの空白文字にマッチ
量詞:貪欲 vs 怠惰
デフォルトでは、量詞は貪欲であり、できるだけ多くの文字にマッチしようとします。HTML文字列 <b>太字</b> と <i>斜体</i> を例に考えます:
<.*> → <b> から </i> まで文字列全体にマッチ(貪欲)
<.*?> → <b>、</b>、<i>、</i> に個別にマッチ(怠惰)
量詞の後に ? を付けると怠惰(非貪欲)になり、全体のパターンが成功する範囲でできるだけ少なくマッチします。
アンカー
^は文字列の先頭にマッチ(mフラグ使用時は行頭)。$は文字列の末尾にマッチ(mフラグ使用時は行末)。\bは単語境界にマッチ——単語文字と非単語文字の間の位置。\Bは非単語境界にマッチ。
グループと後方参照
キャプチャグループ (...) はマッチしたテキストをキャプチャし、\1、\2 などの後方参照として後続パターンで参照したり、マッチ結果の配列からアクセスしたりできます。
非キャプチャグループ (?:...) はキャプチャを作成せずにサブパターンをグループ化します。キャプチャ値が不要な場合はこちらの方が効率的です。
名前付きキャプチャグループ (?<year>\d{4}) では、名前(JavaScriptでは match.groups.year)でキャプチャを参照でき、パターンの可読性が大幅に向上します。
Lookahead と Lookbehind
これらのゼロ幅アサーションは文字ではなく位置にマッチします:
\d+(?=円) → 後に「円」が続く場合のみ数字にマッチ
\d+(?!円) → 後に「円」が続かない場合のみ数字にマッチ
(?<=\$)\d+ → 前に「$」がある場合のみ数字にマッチ
(?<!\$)\d+ → 前に「$」がない場合のみ数字にマッチ
Lookaheadとlookbehindは文字を消費しないため、マッチ結果にはその部分は含まれません。
フラグ
| フラグ | 名前 | 効果 |
|---|---|---|
i |
大文字小文字無視 | [a-z] が [A-Z] にもマッチ |
g |
グローバル | 最初の1件だけでなくすべてのマッチを検索 |
m |
複数行 | ^ と $ が行境界にマッチ |
s |
dotAll | . が改行文字にもマッチ |
u |
Unicode | 完全なUnicodeマッチングを有効化;\p{} に必要 |
y |
スティッキー | lastIndex 位置からのみマッチ |
x |
詳細/拡張 | 空白とコメントを許可(PCRE/Python のみ) |
x(詳細)フラグは複雑なパターンのドキュメント化に特に有用です:
import re
pattern = re.compile(r"""
^ # 文字列の先頭
(?P<year>\d{4}) # 4桁の年
-
(?P<month>0[1-9]|1[0-2]) # 月 01〜12
-
(?P<day>0[1-9]|[12]\d|3[01]) # 日 01〜31
$
""", re.VERBOSE)
よく使うパターンと実例
メールアドレス検証
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
解説:
^[a-zA-Z0-9._%+-]+— ローカル部分(英数字と一部の特殊文字)@— リテラルの @ 記号[a-zA-Z0-9.-]+— ドメイン名\.[a-zA-Z]{2,}$— 2文字以上のTLD
URL マッチング
https?://(?:www\.)?[a-zA-Z0-9-]+(?:\.[a-zA-Z]{2,})+(?:/[^\s]*)?
ISO 日付(YYYY-MM-DD)
\b\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])\b
月 01〜12、日 01〜31 を検証します。月ごとの最大日数(例:2月30日)は検証しません。
IPv4 アドレス
\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b
解説:
25[0-5]— 250〜2552[0-4]\d— 200〜249[01]?\d\d?— 0〜199
米国電話番号
\+?1?\s?[\(]?\d{3}[\)]?[-.\s]?\d{3}[-.\s]?\d{4}
(555) 123-4567、555-123-4567、+1 555 123 4567 などの形式にマッチ。
16進数カラーコード
#(?:[0-9A-Fa-f]{3}){1,2}\b
3桁(#F00)と6桁(#FF0000)の両方の16進数カラーにマッチ。
各プログラミング言語での正規表現
JavaScript
// リテラル構文
const regex = /^hello\s+world$/im;
const match = "Hello World".match(regex);
// コンストラクタ構文(動的パターンに有用)
const term = "world";
const dynamic = new RegExp(`hello\\s+${term}`, "im");
// 全置換
const result = "foo bar foo".replaceAll(/foo/g, "baz");
// 名前付きキャプチャ(ES2018+)
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const { year, month, day } = "2024-03-15".match(dateRegex).groups;
Python
import re
# 再利用のためにコンパイル
pattern = re.compile(r'^hello\s+world$', re.IGNORECASE | re.MULTILINE)
match = pattern.match("Hello World")
# すべてのマッチを検索
dates = re.findall(r'\d{4}-\d{2}-\d{2}', text)
# 関数を使って置換
result = re.sub(r'\b\d+\b', lambda m: str(int(m.group()) * 2), "1 plus 2 is 3")
# 名前付きグループ
m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})', "2024-03")
print(m.group('year')) # 2024
Java
import java.util.regex.*;
Pattern p = Pattern.compile("^hello\\s+world$",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
Matcher m = p.matcher("Hello World");
boolean found = m.matches();
// グループの抽出
Pattern datePattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher dm = datePattern.matcher("今日は 2024-03-15 です");
if (dm.find()) {
String year = dm.group(1);
}
Go
import "regexp"
re := regexp.MustCompile(`(?im)^hello\s+world$`)
match := re.FindString("Hello World")
// すべてのサブマッチを検索
dateRe := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
all := dateRe.FindAllStringSubmatch(text, -1)
for _, m := range all {
year, month, day := m[1], m[2], m[3]
_ = year; _ = month; _ = day
}
// 名前付きグループ
namedRe := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})`)
match2 := namedRe.FindStringSubmatch("2024-03")
yearIdx := namedRe.SubexpIndex("year")
fmt.Println(match2[yearIdx]) // 2024
パフォーマンスと破滅的バックトラック
バックトラックの仕組み
ほとんどの正規表現エンジンは NFA(非決定性有限オートマトン)ベースのマッチングを使用します。これは、ある経路が失敗した場合にエンジンがパターン上の複数の経路を試みることができることを意味します。このバックトラッキングにより lookahead や後方参照などの機能が可能になりますが、パフォーマンスの罠にもなり得ます。
破滅的なケース
パターン (a+)+ を文字列 "aaaaaX" に適用する場合を考えます:
- 外側の
+はできるだけ多くのグループをマッチさせようとします。 - エンジンが
Xに到達して失敗すると、バックトラックし、グループの繰り返し間でa文字を異なる方法で分割しようとします。 - 長さ n の文字列では 2^(n-1) 通りの分割が存在し、指数時間の計算量につながります。
(a+)+ に "aaaaaaaaaaaaaaaaaX" を適用 → 秒単位または分単位の時間がかかることも!
他の危険なパターンとして (a|aa)+、(\w+\s*)+、重複する文字クラスに対するネストされた量詞を含むパターンなどがあります。
回避方法
- 同じ文字セットに対するネストされた量詞を避ける:
(a+)+→a+を使用。 - 原子グループ
(?>...)または所有量詞a++(PCRE)を使用して、既マッチのグループへのバックトラックを防止。 - 具体性を高める:区切り文字を除外する文字クラスで
.*を置き換える(例:引用符内文字列では[^"]*)。 - 可能な限りアンカーを使用して、エンジンが早期に失敗できるようにする。
- 本番コードではタイムアウトを設定(特に信頼できない入力を処理する場合)。
複雑な正規表現の読み方
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ を分解します:
^ → アンカー:文字列の先頭
[a-zA-Z0-9._%+-]+ → ローカル部分で使用できる1文字以上の文字
@ → リテラルの @
[a-zA-Z0-9.-]+ → 1文字以上のドメイン文字
\. → リテラルのドット(エスケープ済み)
[a-zA-Z]{2,} → TLD:2文字以上の英字
$ → アンカー:文字列の末尾
ヒント: 本サイトの正規表現テスターを使って各キャプチャグループをハイライト表示し、各トークンが入力のどの部分にマッチするかをリアルタイムで確認しましょう。最も単純な有効な部分から始めてパターンを段階的に構築し、確認してから拡張するのがコツです。
ベストプラクティス
- 一度コンパイルして何度も使う。 パターンの事前コンパイル(Pythonの
re.compile()、JavaのPattern.compile())は毎回再解析するよりはるかに効率的です。 - キャプチャ値が不要な場合は非キャプチャグループ
(?:...)を優先する。 意図を明示でき、不要なメモリ割り当ても避けられます。 - パターンには生文字列を使う。 Pythonでは
'\\d+'ではなくr'\d+'を使用して二重エスケープを避ける。JavaScriptのリテラル構文/\d+/はこれを自動的に処理します。 - キャプチャグループに名前を付ける。
(?<year>\d{4})は\1のようなインデックス参照よりはるかに保守しやすいです。 - エッジケースでテストする: 空文字列、ほぼマッチするが完全にはマッチしない文字列、Unicode文字、非常に長い入力。
- 複雑なパターンにはドキュメントを付ける:Python/PCREでは
x(詳細)フラグを使い、コード内ではインラインコメントを使う。 - 正規表現で完全なHTMLやXMLを解析しない。 適切なパーサーライブラリを使いましょう。
- サーバー側で入力を検証する。 クライアント側の正規表現検証はUXを向上させますが、唯一の防衛線であってはなりません。
よくある質問(FAQ)
Q:Pythonの match() と search() の違いは?
A:re.match() は文字列の先頭からのみマッチします。re.search() は文字列全体をスキャンしてマッチを探します。パターンが文字列全体にマッチすることを要求する場合は re.fullmatch() を使います。
Q:文字クラス [^abc] 内の ^ はなぜ意味が違うのですか?
A:文字クラス内で ^ が最初の文字として現れる場合、クラスを否定します——セット内にない任意の文字にマッチします。文字クラスの外では、^ は文字列先頭のアンカーです。
Q:正規表現でHTMLを解析できますか?
A:既知のHTML構造からのシンプルで明確な抽出であれば、正規表達式で対応できる場合があります。しかしHTMLは正規言語ではなく、任意のネストとオプションの閉じタグを許可します。堅牢な解析には適切なHTMLパーサー(PythonのBeautifulSoup、JSのDOMParser)を使いましょう。
Q:貪欲量詞と所有量詞の違いは?
A:貪欲量詞はバックトラックします——最大マッチを試み、必要に応じて文字を返します。所有量詞(PCREの a++ など)は返しません——一度マッチしたらロックされます。これにより破滅的バックトラックを防げますが、貪欲量詞なら成功するマッチが失敗することもあります。
Q:リテラルのドット . や括弧 ( にマッチさせるには?
A:バックスラッシュでエスケープします:\. はリテラルのドット、\( はリテラルの左括弧にマッチします。
Q:正規表現はデフォルトで大文字小文字を区別しますか?
A:はい。i フラグ(JavaScriptの /pattern/i、Pythonの re.IGNORECASE)を使うと大文字小文字を区別しないマッチングが有効になります。
Q:\b は何にマッチしますか?
A:\b はゼロ幅の単語境界アサーションです。単語文字(\w)と非単語文字(\W)の間の位置にマッチします。
Q:文字列全体がパターンにマッチするかテストするには?
A:^ と $ でアンカーを付けます:^pattern$。Pythonでは re.fullmatch() も使えます。JavaScriptではアンカー付きの .test() を使うか、match()[0].length === input.length を確認します。
本サイトの正規表達式テスターを使って、このガイドのすべてのパターンを試してみましょう。パターンを貼り付けてテスト文字列を入力すると、マッチ結果がリアルタイムでハイライト表示されます。