Regex
正規表現 (Regular Expressions) 完全ガイド
目次
- はじめに
- 正規表現の歴史と背景
- 正規表現の基本概念
- 基本的なメタ文字とシンタックス
- 文字クラスと定義済みクラス
- 量指定子(Quantifiers)
- アンカーと境界
- グループ化とキャプチャ
- 先読みと後読み(Lookahead / Lookbehind)
- バックリファレンスと置換
- 正規表現エンジンの仕組み
- 正規表現のフラグとオプション
- 各プログラミング言語での正規表現
- コマンドラインツールでの正規表現
- 実践的なパターン集
- パフォーマンスと最適化
- セキュリティとReDoS攻撃
- 正規表現のデバッグとテスト
- Unicode と国際化対応
- 正規表現のベストプラクティス
- まとめ
1. はじめに
正規表現(Regular Expressions、略称: regex または regexp)は、文字列のパターンマッチングと操作のための強力な形式言語である。テキスト検索、データバリデーション、文字列変換など、ソフトウェア開発のあらゆる場面で活用されている。
正規表現は一見すると暗号のように見えるが、その構文と仕組みを体系的に理解すれば、テキスト処理における最も強力なツールの1つとなる。本記事では、正規表現の基礎から高度なテクニック、エンジンの内部動作、各言語での実装の違い、パフォーマンス最適化、セキュリティ上の注意点まで網羅的に解説する。
1.1 正規表現が解決する問題
正規表現は以下のような場面で威力を発揮する。
- パターンマッチング: 特定のパターンに合致する文字列の検索
- バリデーション: 入力データの形式チェック(メールアドレス、電話番号、URLなど)
- テキスト抽出: 大量のテキストから必要な情報を抜き出す
- 文字列置換: パターンに基づく高度な文字列変換
- 構文解析: 簡易的なパーサーとしての利用
- ログ解析: サーバーログやアプリケーションログからの情報抽出
1.2 本記事の構成
本記事は大きく4つのパートで構成されている。
- 基礎編(セクション2〜8): 正規表現の歴史、基本構文、メタ文字、量指定子など
- 応用編(セクション9〜12): 先読み・後読み、エンジンの仕組み、フラグなど
- 実践編(セクション13〜15): 各言語での実装、コマンドラインツール、実践パターン
- 運用編(セクション16〜20): パフォーマンス、セキュリティ、デバッグ、ベストプラクティス
2. 正規表現の歴史と背景
2.1 数学的起源
正規表現の概念は、1950年代の数学者スティーブン・クリーネ(Stephen Kleene)による「正規集合」の理論に遡る。クリーネは、神経科学者ウォーレン・マカロックとウォルター・ピッツが提唱したニューラルネットワークモデルを代数的に記述するために、正規表現の理論を発展させた。
2.2 コンピュータサイエンスへの導入
1968年、ケン・トンプソン(Ken Thompson)がUnixのテキストエディタ ed に正規表現を実装した。これが正規表現のコンピュータへの実用的な導入の始まりとなった。その後、grep(Global Regular Expression Print)コマンドとして独立し、Unixの世界で広く普及した。
歴史的な流れ:
1950s: クリーネによる正規集合の理論
1968: ケン・トンプソンがedエディタに実装
1973: grepコマンドの誕生
1986: POSIX正規表現の標準化
1987: Larry WallがPerlを開発(正規表現を言語の中核に)
1997: PCRE(Perl Compatible Regular Expressions)の登場
2002: .NET正規表現エンジン(強力な機能セット)
2006: RE2エンジン(Googleによる安全な正規表現エンジン)
2.3 正規表現の方言
正規表現には複数の「方言」が存在する。主要なものは以下の通りである。
| 方言 | 特徴 | 代表的な使用環境 |
|---|---|---|
| BRE (Basic Regular Expressions) | 基本的な正規表現。メタ文字にエスケープが必要 | grep, sed |
| ERE (Extended Regular Expressions) | BREの拡張。エスケープなしでメタ文字を使用可能 | egrep, awk |
| PCRE (Perl Compatible) | Perl由来の豊富な機能 | PHP, R, Apache |
| PCRE2 | PCREの後継。Unicode対応強化 | PHP 7.3+, Haiku OS |
| JavaScript RegExp | ECMAScript仕様に基づく | ブラウザ, Node.js |
| Java java.util.regex | PCRE風だが独自の拡張あり | Java, Kotlin |
| Python re / regex | PCRE風。regexモジュールは拡張版 | Python |
| .NET System.Text.RegularExpressions | バランスグループなど独自機能 | C#, VB.NET |
| RE2 | 線形時間保証。後方参照なし | Go, Google検索 |
| Oniguruma / Onigmo | Unicode完全対応 | Ruby, PHP(mbregex) |
3. 正規表現の基本概念
3.1 リテラル文字
正規表現の最もシンプルな形は、リテラル文字のマッチングである。文字そのものがパターンとなる。
パターン: hello
対象文字列: "say hello to the world"
マッチ結果: "say [hello] to the world"
リテラル文字のマッチングは大文字小文字を区別する(デフォルト動作)。
パターン: Hello
対象文字列: "say hello to the world"
マッチ結果: マッチなし
パターン: Hello
対象文字列: "say Hello to the world"
マッチ結果: "say [Hello] to the world"
3.2 メタ文字の概要
正規表現では、特殊な意味を持つ文字を「メタ文字」と呼ぶ。以下が主要なメタ文字である。
. 任意の1文字(改行を除く)
^ 行頭
$ 行末
* 直前の要素の0回以上の繰り返し
+ 直前の要素の1回以上の繰り返し
? 直前の要素の0回または1回
| OR(選択)
\ エスケープ文字
() グループ化
[] 文字クラス
{} 量指定子(回数指定)
3.3 エスケープシーケンス
メタ文字をリテラルとして扱いたい場合は、バックスラッシュ \ でエスケープする。
パターン: \$100\.00
対象文字列: "Price is $100.00 today"
マッチ結果: "Price is [$100.00] today"
パターン: file\.txt
対象文字列: "Open file.txt now"
マッチ結果: "Open [file.txt] now"
パターン: C:\\Users\\Documents
対象文字列: "Path: C:\Users\Documents"
マッチ結果: "Path: [C:\Users\Documents]"
3.4 パターンマッチングの基本動作
正規表現エンジンは、対象文字列の左端から右端に向かって、パターンに一致する部分を探索する。
パターン: cat
対象文字列: "concatenation"
マッチ結果: "con[cat]enation"
(最初に見つかった "cat" にマッチ)
グローバルフラグ(g)を使用すると、すべての一致を検索する。
パターン: cat (グローバル)
対象文字列: "the cat sat on the caterpillar"
マッチ結果: "the [cat] sat on the [cat]erpillar"
4. 基本的なメタ文字とシンタックス
4.1 ドット(.)— 任意の1文字
ドットは改行文字を除く任意の1文字にマッチする。
パターン: c.t
対象文字列: "cat cot cut c t c3t"
マッチ結果: "[cat] [cot] [cut] [c t] [c3t]"
パターン: h.t
対象文字列: "hat hot hut hit"
マッチ結果: "[hat] [hot] [hut] [hit]"
パターン: ..\...\.....
対象文字列: "IP: 10.20.30000"
(これは正しいIPアドレスのマッチには不十分 — より精密なパターンが必要)
4.2 パイプ(|)— 選択(OR)
パイプは「または」を表す。
パターン: cat|dog
対象文字列: "I have a cat and a dog"
マッチ結果: "I have a [cat] and a [dog]"
パターン: red|green|blue
対象文字列: "The red car and green house"
マッチ結果: "The [red] car and [green] house"
選択の範囲に注意する。グループ化と組み合わせることが多い。
パターン: gr(a|e)y
対象文字列: "gray and grey"
マッチ結果: "[gray] and [grey]"
パターン: (Mon|Tues|Wednes|Thurs|Fri|Satur|Sun)day
対象文字列: "Monday Tuesday Wednesday"
マッチ結果: "[Monday] [Tuesday] [Wednesday]"
4.3 組み合わせの例
パターン: .at
対象文字列: "The cat sat on a mat in a vat"
マッチ結果: "The [cat] [sat] on a [mat] in a [vat]"
パターン: ..tion
対象文字列: "action nation lotion"
マッチ結果: "[action] [nation] [lotion]"
5. 文字クラスと定義済みクラス
5.1 文字クラス([...])
角括弧内に列挙した文字のいずれか1文字にマッチする。
パターン: [aeiou]
意味: 母音のいずれか1文字
パターン: [abc]
対象文字列: "a big cat"
マッチ結果: "[a] [b]ig [c][a]t"
パターン: gr[ae]y
対象文字列: "gray grey grid"
マッチ結果: "[gray] [grey] grid"
5.2 範囲指定
ハイフンを使って文字の範囲を指定できる。
パターン: [a-z] 小文字のアルファベット1文字
パターン: [A-Z] 大文字のアルファベット1文字
パターン: [0-9] 数字1文字
パターン: [a-zA-Z] アルファベット1文字(大小不問)
パターン: [a-zA-Z0-9] 英数字1文字
パターン: [A-Z][a-z]+
対象文字列: "Hello World Foo"
マッチ結果: "[Hello] [World] [Foo]"
5.3 否定文字クラス([^...])
キャレット ^ を角括弧の先頭に置くと、指定した文字以外にマッチする。
パターン: [^aeiou]
意味: 母音以外の1文字
パターン: [^0-9]
意味: 数字以外の1文字
パターン: [^a-zA-Z]
対象文字列: "Hello, World! 123"
マッチ結果: "Hello[,][ ]World[!][ ][1][2][3]"
5.4 定義済み文字クラス(ショートハンド)
よく使われる文字クラスにはショートハンドが用意されている。
\d 数字 [0-9] と同等
\D 数字以外 [^0-9] と同等
\w 単語文字 [a-zA-Z0-9_] と同等
\W 単語文字以外 [^a-zA-Z0-9_] と同等
\s 空白文字 [ \t\n\r\f\v] と同等
\S 空白文字以外 [^ \t\n\r\f\v] と同等
具体例:
パターン: \d{3}-\d{4}-\d{4}
対象文字列: "電話番号は090-1234-5678です"
マッチ結果: "電話番号は[090-1234-5678]です"
パターン: \w+@\w+\.\w+
対象文字列: "連絡先: user@example.com まで"
マッチ結果: "連絡先: [user@example.com] まで"
パターン: \S+
対象文字列: "hello world foo"
マッチ結果: "[hello] [world] [foo]"
5.5 POSIX文字クラス
POSIX互換の環境(grep, sedなど)では、名前付き文字クラスが使用できる。
[:alpha:] アルファベット文字 → [a-zA-Z]
[:digit:] 数字 → [0-9]
[:alnum:] 英数字 → [a-zA-Z0-9]
[:space:] 空白文字
[:upper:] 大文字 → [A-Z]
[:lower:] 小文字 → [a-z]
[:punct:] 句読点
[:print:] 印字可能文字
[:cntrl:] 制御文字
[:xdigit:] 16進数字 → [0-9a-fA-F]
使用例(grep):
# 数字のみを含む行を検索
grep '[[:digit:]]\+' file.txt
# アルファベットで始まる行
grep '^[[:alpha:]]' file.txt
6. 量指定子(Quantifiers)
量指定子は、直前の要素が何回繰り返されるかを指定する。
6.1 基本的な量指定子
* 0回以上(貪欲)
+ 1回以上(貪欲)
? 0回または1回
{n} ちょうどn回
{n,} n回以上
{n,m} n回以上m回以下
具体例:
パターン: ab*c
マッチ: "ac", "abc", "abbc", "abbbc", ...
(bが0回以上)
パターン: ab+c
マッチ: "abc", "abbc", "abbbc", ...
非マッチ: "ac"
(bが1回以上)
パターン: ab?c
マッチ: "ac", "abc"
非マッチ: "abbc"
(bが0回または1回)
パターン: a{3}
マッチ: "aaa"
対象文字列: "aa aaa aaaa"
マッチ結果: "aa [aaa] [aaa]a"
パターン: \d{2,4}
対象文字列: "1 12 123 1234 12345"
マッチ結果: "1 [12] [123] [1234] [1234]5"
6.2 貪欲マッチ(Greedy)と怠惰マッチ(Lazy)
デフォルトの量指定子は「貪欲(Greedy)」で、可能な限り多くの文字にマッチしようとする。量指定子の後に ? を付けると「怠惰(Lazy)」になり、可能な限り少ない文字にマッチする。
貪欲マッチ(デフォルト):
パターン: ".+"
対象文字列: 'He said "hello" and "goodbye"'
マッチ結果: 'He said ["hello" and "goodbye"]'
(最初の " から最後の " までマッチ)
怠惰マッチ:
パターン: ".+?"
対象文字列: 'He said "hello" and "goodbye"'
マッチ結果: 'He said ["hello"] and ["goodbye"]'
(最短の " から " までマッチ)
各量指定子の貪欲・怠惰バージョン:
貪欲 怠惰 意味
* *? 0回以上
+ +? 1回以上
? ?? 0回または1回
{n,} {n,}? n回以上
{n,m} {n,m}? n回以上m回以下
6.3 独占的量指定子(Possessive Quantifiers)
独占的量指定子はバックトラックを行わない。量指定子の後に + を付ける。
独占的量指定子:
*+ 0回以上(バックトラックなし)
++ 1回以上(バックトラックなし)
?+ 0回または1回(バックトラックなし)
{n,m}+ n〜m回(バックトラックなし)
貪欲マッチ:
パターン: ".*"
対象文字列: '"hello"'
動作: .* が "hello" 全体を消費 → " にマッチしないのでバックトラック
→ 1文字ずつ戻って最終的に "hello" にマッチ
独占的マッチ:
パターン: ".*+"
対象文字列: '"hello"'
動作: .*+ が "hello" 全体を消費 → バックトラックしない → マッチ失敗
独占的量指定子は、パフォーマンス最適化やReDoS防止に活用できる。
6.4 実践例
# 日本の郵便番号
パターン: \d{3}-\d{4}
対象: "〒100-0001 東京都千代田区"
マッチ: [100-0001]
# 日本の電話番号(固定電話)
パターン: 0\d{1,4}-\d{1,4}-\d{4}
対象: "TEL: 03-1234-5678"
マッチ: [03-1234-5678]
# IPアドレス(簡易版)
パターン: \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}
対象: "Server IP: 192.168.1.1"
マッチ: [192.168.1.1]
# HTMLタグ(貪欲 vs 怠惰の違い)
パターン: <.+> (貪欲)
対象: "<b>bold</b>"
マッチ: [<b>bold</b>] ← タグ全体を1つのマッチとして取得
パターン: <.+?> (怠惰)
対象: "<b>bold</b>"
マッチ: [<b>]bold[</b>] ← 各タグを個別にマッチ
7. アンカーと境界
アンカーは文字そのものにマッチするのではなく、文字列内の「位置」にマッチする。
7.1 行頭・行末アンカー
^ 行頭(文字列の先頭、または改行直後の位置)
$ 行末(文字列の末尾、または改行直前の位置)
パターン: ^Hello
対象文字列: "Hello World"
マッチ結果: "[Hello] World"
パターン: ^Hello
対象文字列: "Say Hello"
マッチ結果: マッチなし
パターン: World$
対象文字列: "Hello World"
マッチ結果: "Hello [World]"
パターン: ^Hello World$
対象文字列: "Hello World"
マッチ結果: "[Hello World]"(完全一致)
7.2 文字列全体のアンカー
\A 文字列の先頭(マルチラインモードでも行頭ではなく文字列先頭)
\Z 文字列の末尾(末尾の改行の前)
\z 文字列の末尾(厳密に末尾)
対象文字列:
"Line 1\nLine 2\nLine 3"
パターン: ^Line (マルチラインモード)
マッチ: [Line] 1, [Line] 2, [Line] 3(各行頭にマッチ)
パターン: \ALine (マルチラインモード)
マッチ: [Line] 1(文字列先頭のみマッチ)
7.3 単語境界
\b 単語の境界(単語文字と非単語文字の間)
\B 単語の境界でない位置
パターン: \bcat\b
対象文字列: "the cat sat on concatenation"
マッチ結果: "the [cat] sat on concatenation"
(独立した単語 "cat" のみマッチ。"concatenation" 内の cat にはマッチしない)
パターン: \Bcat\B
対象文字列: "the cat sat on concatenation"
マッチ結果: "the cat sat on con[cat]enation"
(単語の途中にある "cat" のみマッチ)
7.4 実践例
# ファイル内のTODOコメントを検索
grep -n '^\s*//\s*TODO' src/*.java
# 行末の空白を検索
grep -n '\s+$' file.txt
# 特定の関数定義を検索(単語境界使用)
grep -n '\bfunction\b.*\bgetUser\b' *.js
# 空行を検索
grep -n '^$' file.txt
# 特定の拡張子で終わるファイル名
echo "$filename" | grep -E '\.(jpg|jpeg|png|gif)$'
8. グループ化とキャプチャ
8.1 キャプチャグループ((...))
丸括弧はパターンをグループ化し、マッチした内容をキャプチャ(記憶)する。
パターン: (\d{4})-(\d{2})-(\d{2})
対象文字列: "2024-01-15"
グループ0(全体): "2024-01-15"
グループ1: "2024"
グループ2: "01"
グループ3: "15"
プログラミング言語での使用例(Python):
import re
text = "Date: 2024-01-15"
match = re.search(r'(\d{4})-(\d{2})-(\d{2})', text)
if match:
print(f"年: {match.group(1)}") # 2024
print(f"月: {match.group(2)}") # 01
print(f"日: {match.group(3)}") # 15
8.2 名前付きキャプチャグループ
キャプチャグループに名前を付けることで、可読性が向上する。
構文(PCRE / Python / .NET):
(?P<name>pattern) Python形式
(?<name>pattern) .NET / PCRE2形式
(?'name'pattern) .NET代替形式
パターン: (?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})
import re
text = "Date: 2024-01-15"
match = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', text)
if match:
print(f"年: {match.group('year')}") # 2024
print(f"月: {match.group('month')}") # 01
print(f"日: {match.group('day')}") # 15
8.3 非キャプチャグループ((?:...))
グループ化だけ行い、キャプチャはしないグループ。パフォーマンスの向上やグループ番号の管理に有用。
パターン: (?:http|https)://(\w+\.\w+)
対象文字列: "https://example.com"
グループ0(全体): "https://example.com"
グループ1: "example.com"
(http|httpsの部分はキャプチャされない)
8.4 アトミックグループ((?>...))
アトミックグループ内のマッチが確定すると、バックトラックしない。
パターン: (?>a|ab)b
対象文字列: "ab"
動作: "a" にマッチ → アトミックグループ確定
→ "b" にマッチ → 成功
対象文字列: "abb"
動作: "a" にマッチ → アトミックグループ確定
→ "b" にマッチ → 成功
パターン: (?>a|ab)c
対象文字列: "abc"
動作: "a" にマッチ → アトミックグループ確定
→ "c" にマッチ試行 → "b" なので失敗
→ バックトラックしないので全体が失敗
(通常のグループなら "ab" を試してマッチ可能)
8.5 条件付きパターン
一部のエンジン(PCRE, .NET)では条件付きマッチが可能。
構文: (?(条件)trueパターン|falseパターン)
パターン: (\()?\d{3}(?(1)\)|-)\d{3}-\d{4}
マッチ: "(123)456-7890" または "123-456-7890"
非マッチ: "(123-456-7890" または "123)456-7890"
(開き括弧があれば閉じ括弧を期待、なければハイフンを期待)
9. 先読みと後読み(Lookahead / Lookbehind)
先読みと後読みは「ゼロ幅アサーション」と呼ばれ、文字を消費せずに条件を確認する。
9.1 先読み(Lookahead)
肯定先読み(Positive Lookahead): (?=...)
指定したパターンが後に続く位置にマッチする。
パターン: \w+(?=\s*=)
対象文字列: "name = 'John'"
マッチ結果: "[name] = 'John'"
("=" が後に続く単語にマッチ)
パターン: \d+(?=円)
対象文字列: "りんご100円、みかん200円"
マッチ結果: "りんご[100]円、みかん[200]円"
("円" が後に続く数字にマッチ。"円" 自体は含まない)
否定先読み(Negative Lookahead): (?!...)
指定したパターンが後に続かない位置にマッチする。
パターン: \d+(?!円)
対象文字列: "100円 200ドル 300円"
マッチ結果: "100円 [200]ドル 300円"
("円" が後に続かない数字にマッチ)
注意: 実際には "10", "30" にもマッチする場合がある(エンジンによる)
パターン: foo(?!bar)
対象文字列: "foobar foobaz fooqux"
マッチ結果: "foobar [foo]baz [foo]qux"
9.2 後読み(Lookbehind)
肯定後読み(Positive Lookbehind): (?<=...)
指定したパターンが前にある位置にマッチする。
パターン: (?<=\$)\d+
対象文字列: "Price: $100 and €200"
マッチ結果: "Price: $[100] and €200"
("$" の後の数字にマッチ。"$" 自体は含まない)
パターン: (?<=@)\w+
対象文字列: "user@example.com"
マッチ結果: "user@[example].com"
否定後読み(Negative Lookbehind): (?<!...)
指定したパターンが前にない位置にマッチする。
パターン: (?<!\$)\d+
対象文字列: "$100 and 200"
マッチ結果: "$1[00] and [200]"
("$" の直後でない数字にマッチ)
パターン: (?<!un)happy
対象文字列: "happy unhappy"
マッチ結果: "[happy] unhappy"
9.3 先読み・後読みの組み合わせ
# パスワード強度チェック(8文字以上、大文字・小文字・数字を含む)
パターン: ^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}$
解説:
^ 文字列の先頭
(?=.*[A-Z]) 大文字を少なくとも1つ含む(先読み)
(?=.*[a-z]) 小文字を少なくとも1つ含む(先読み)
(?=.*\d) 数字を少なくとも1つ含む(先読み)
.{8,} 8文字以上
$ 文字列の末尾
マッチ: "Passw0rd", "MyP4ssword123"
非マッチ: "password", "PASSWORD1", "Pass1"
# 3桁ごとにカンマを入れる位置を検出
パターン: (?<=\d)(?=(\d{3})+$)
対象文字列: "1234567890"
マッチ位置: 1,234,567,890
(先読みと後読みを組み合わせて、カンマを挿入すべき位置を特定)
9.4 後読みの制限
後読みには、エンジンによって制限がある。
制限事項:
- JavaScriptの古いバージョン: 後読み非対応(ES2018で追加)
- 多くのエンジン: 後読み内で可変長パターンを使用不可
NG: (?<=\w+) (可変長)
OK: (?<=\w{3}) (固定長)
- PCRE / Python: 可変長の後読みに制限あり
- .NET / Java: 可変長の後読みをサポート
- JavaScript ES2018+: 可変長の後読みをサポート
10. バックリファレンスと置換
10.1 バックリファレンス
キャプチャグループでマッチした内容を、パターン内で再参照できる。
パターン: (\w+)\s+\1
意味: 同じ単語が連続する箇所を検出
対象文字列: "the the quick brown fox fox"
マッチ結果: "[the the] quick brown [fox fox]"
パターン: (\w)\1
意味: 同じ文字の連続
対象文字列: "aabbbccd"
マッチ結果: "[aa][bb]b[cc]d"
名前付きバックリファレンス:
パターン: (?P<word>\w+)\s+(?P=word) Python形式
パターン: (?<word>\w+)\s+\k<word> .NET / PCRE2形式
10.2 置換での参照
正規表現の置換操作では、キャプチャグループを参照してダイナミックな置換が可能。
検索パターン: (\d{4})-(\d{2})-(\d{2})
置換パターン: $2/$3/$1
対象: "2024-01-15"
結果: "01/15/2024"
(YYYY-MM-DD → MM/DD/YYYY 変換)
import re
# 日付フォーマット変換
text = "Date: 2024-01-15"
result = re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\2/\3/\1', text)
print(result) # "Date: 01/15/2024"
# 名前付きグループを使用した置換
text = "John Smith"
result = re.sub(r'(?P<first>\w+)\s(?P<last>\w+)', r'\g<last>, \g<first>', text)
print(result) # "Smith, John"
# HTMLタグの変換
text = '<b>bold text</b>'
result = re.sub(r'<b>(.*?)</b>', r'<strong>\1</strong>', text)
print(result) # "<strong>bold text</strong>"
10.3 置換での特殊変数
言語やツールによって、置換パターンで使用できる特殊変数がある。
Perl / JavaScript:
$1, $2, ... キャプチャグループの参照
$& マッチ全体
$` マッチの前の文字列
$' マッチの後の文字列
$+ 最後にマッチしたグループ
Python:
\1, \2, ... キャプチャグループ(番号)
\g<1> キャプチャグループ(番号、明示的)
\g<name> 名前付きキャプチャグループ
\g<0> マッチ全体
sed:
\1, \2, ... キャプチャグループ
& マッチ全体
10.4 高度な置換例
# sedを使った実践的な置換
# CSV内の日付フォーマット変換(YYYY/MM/DD → YYYY-MM-DD)
sed -E 's/([0-9]{4})\/([0-9]{2})\/([0-9]{2})/\1-\2-\3/g' data.csv
# HTMLからタグを除去
sed -E 's/<[^>]+>//g' page.html
# CamelCase を snake_case に変換
echo "getUserName" | sed -E 's/([a-z])([A-Z])/\1_\2/g' | tr 'A-Z' 'a-z'
# → get_user_name
# 行末のスペースを除去
sed -E 's/[[:space:]]+$//' file.txt
// JavaScriptでの高度な置換
// 金額のフォーマット(3桁区切りカンマ)
"1234567890".replace(/\B(?=(\d{3})+(?!\d))/g, ",");
// → "1,234,567,890"
// テンプレートリテラル風の置換
const data = { name: "John", age: 30 };
"Hello {{name}}, you are {{age}} years old.".replace(
/\{\{(\w+)\}\}/g,
(match, key) => data[key] || match
);
// → "Hello John, you are 30 years old."
// URLのスラッグ化
"Hello World! This is a Test.".replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.toLowerCase();
// → "hello-world-this-is-a-test"
11. 正規表現エンジンの仕組み
正規表現エンジンの内部動作を理解することは、効率的なパターン作成とパフォーマンス問題の回避に不可欠である。
11.1 エンジンの種類
正規表現エンジンは大きく2つに分類される。
DFA(Deterministic Finite Automaton)エンジン
特徴:
- 決定性有限オートマトンベース
- テキストを1文字ずつ進め、各文字を最大1回だけ処理
- バックトラックしない
- 処理時間がテキスト長に対して線形(O(n))
- キャプチャグループ、バックリファレンス、先読み/後読みが使えない
使用環境:
- awk
- egrep(一部の実装)
- RE2(Google)
- GNU grep(ハイブリッド)
NFA(Nondeterministic Finite Automaton)エンジン
特徴:
- 非決定性有限オートマトンベース
- パターン主導(パターンの各要素を順番に試す)
- バックトラックを使用
- 最悪の場合、指数的な処理時間になりうる
- キャプチャグループ、バックリファレンス、先読み/後読みをサポート
使用環境:
- Perl, Python, Ruby, Java, JavaScript, .NET, PHP
- ほとんどのプログラミング言語の標準ライブラリ
11.2 NFAエンジンのバックトラッキング
NFAエンジンの核心はバックトラッキングである。
パターン: ab*bc
対象文字列: "abbbbc"
マッチングの流れ:
1. 'a' がテキストの 'a' にマッチ ✓
2. 'b*' が貪欲に 'bbbb' をすべて消費 ✓
3. パターンの 'b' をテキストの 'c' と比較 → 不一致 ✗
4. バックトラック: 'b*' が 'bbb' に縮小
5. パターンの 'b' がテキストの 'b' にマッチ ✓
6. パターンの 'c' がテキストの 'c' にマッチ ✓
→ 全体マッチ成功: "abbbbc"
11.3 バックトラッキングの可視化
パターン: /.*foo/
対象文字列: "xfooxxxxxxfoo"
ステップ1: .* が "xfooxxxxxxfoo" 全体を消費(貪欲)
ステップ2: foo とマッチ試行 → 文字列末尾で失敗
ステップ3: .* が1文字バックトラック → "xfooxxxxxxfo"
ステップ4: foo とマッチ試行 → "o" のみ → 失敗
ステップ5: .* が1文字バックトラック → "xfooxxxxxxf"
ステップ6: foo とマッチ試行 → "foo" → 失敗("fo" は2文字しかない)
...
ステップN: .* が "xfooxxxxxx" にバックトラック
ステップN+1: "foo" にマッチ → 成功!
結果: "xfooxxxxxxfoo" 全体がマッチ
11.4 状態遷移図
正規表現 a(b|c)*d の状態遷移図:
b
┌──┐
│ ▼
──→ (S0) ──a──→ (S1) ──d──→ ((S2))
│ ▲
└──┘
c
S0: 初期状態
S1: 'a' を読んだ後の状態
S2: 受理状態(マッチ成功)
S0 → S1: 'a' で遷移
S1 → S1: 'b' または 'c' でループ
S1 → S2: 'd' で遷移(マッチ成功)
11.5 コンパイルと最適化
多くのエンジンは正規表現を内部表現にコンパイルしてから実行する。
# Pythonでの正規表現コンパイル
import re
# コンパイルなし(毎回パースされる)
for line in lines:
if re.search(r'\d{4}-\d{2}-\d{2}', line):
process(line)
# コンパイルあり(パターンを再利用)
date_pattern = re.compile(r'\d{4}-\d{2}-\d{2}')
for line in lines:
if date_pattern.search(line):
process(line)
// Javaでの正規表現コンパイル
import java.util.regex.*;
// パターンのコンパイル(再利用可能)
Pattern pattern = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
for (String line : lines) {
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
processLine(line);
}
}
12. 正規表現のフラグとオプション
12.1 主要なフラグ
フラグ 名称 説明
i Case-Insensitive 大文字小文字を区別しない
g Global 全てのマッチを検索
m Multiline ^と$が各行の先頭/末尾にマッチ
s Single-line (DotAll) . が改行にもマッチ
x Extended (Verbose) 空白とコメントを許可
u Unicode Unicode対応
y Sticky (JavaScript) lastIndexの位置からマッチ
d HasIndices (JavaScript) マッチのインデックス情報を含む
12.2 大文字小文字の無視(i フラグ)
パターン: /hello/i
対象: "Hello HELLO hello HeLLo"
マッチ: "[Hello] [HELLO] [hello] [HeLLo]"
# インラインでの指定(一部のエンジン)
パターン: (?i)hello
パターン: (?i:hello)world ← "hello" 部分のみ大文字小文字無視
12.3 マルチラインモード(m フラグ)
対象文字列:
"First line
Second line
Third line"
パターン: /^Second/ (フラグなし)
マッチ: なし(^は文字列先頭のみ)
パターン: /^Second/m (マルチラインモード)
マッチ: "[Second] line"(各行の先頭にマッチ)
12.4 ドットオールモード(s フラグ)
対象文字列:
"line1\nline2\nline3"
パターン: /line1.*line3/ (フラグなし)
マッチ: なし(. が改行にマッチしない)
パターン: /line1.*line3/s (ドットオールモード)
マッチ: "[line1\nline2\nline3]"
12.5 拡張モード(x フラグ)
空白とコメントを使って、正規表現を読みやすく書ける。
import re
# 拡張モードなし(読みにくい)
pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
# 拡張モード(読みやすい)
pattern = re.compile(r'''
^
(?:
(?:
25[0-5] # 250-255
| 2[0-4][0-9] # 200-249
| [01]?[0-9][0-9]? # 0-199
)
\. # ドット区切り
){3} # 最初の3オクテット
(?:
25[0-5] # 250-255
| 2[0-4][0-9] # 200-249
| [01]?[0-9][0-9]? # 0-199
)
$
''', re.VERBOSE)
12.6 インラインフラグ
パターン内でフラグを指定する方法。
(?i) 大文字小文字無視を有効化
(?-i) 大文字小文字無視を無効化
(?i:pattern) パターン部分のみ大文字小文字無視
(?ims) 複数フラグを同時指定
例:
パターン: (?i)error|(?-i)WARNING
→ "error"は大文字小文字無視、"WARNING"は完全一致
13. 各プログラミング言語での正規表現
13.1 Python
import re
# ---- 基本操作 ----
# search: 最初のマッチを検索
text = "The price is $100.50 and $200.75"
match = re.search(r'\$(\d+\.\d{2})', text)
if match:
print(match.group()) # $100.50
print(match.group(1)) # 100.50
# findall: すべてのマッチをリストで取得
prices = re.findall(r'\$(\d+\.\d{2})', text)
print(prices) # ['100.50', '200.75']
# finditer: すべてのマッチをイテレータで取得
for match in re.finditer(r'\$(\d+\.\d{2})', text):
print(f"位置 {match.start()}-{match.end()}: {match.group()}")
# match: 文字列の先頭からマッチ
result = re.match(r'\d+', "123abc")
print(result.group()) # 123
result = re.match(r'\d+', "abc123")
print(result) # None(先頭がマッチしない)
# fullmatch: 文字列全体がマッチ
result = re.fullmatch(r'\d+', "123")
print(result.group()) # 123
result = re.fullmatch(r'\d+', "123abc")
print(result) # None
# ---- 置換 ----
# sub: パターンを置換
text = "Hello World Hello Python"
result = re.sub(r'Hello', 'Hi', text)
print(result) # "Hi World Hi Python"
# sub with count: 置換回数の制限
result = re.sub(r'Hello', 'Hi', text, count=1)
print(result) # "Hi World Hello Python"
# sub with function: 関数による動的置換
def double_number(match):
return str(int(match.group()) * 2)
text = "Score: 100, Level: 5"
result = re.sub(r'\d+', double_number, text)
print(result) # "Score: 200, Level: 10"
# ---- 分割 ----
# split: パターンで分割
text = "apple,banana;;cherry date"
result = re.split(r'[,;\s]+', text)
print(result) # ['apple', 'banana', 'cherry', 'date']
# ---- コンパイル ----
# パターンの事前コンパイル
email_pattern = re.compile(
r'''
[a-zA-Z0-9._%+-]+ # ローカル部分
@ # @マーク
[a-zA-Z0-9.-]+ # ドメイン
\.[a-zA-Z]{2,} # TLD
''',
re.VERBOSE
)
emails = email_pattern.findall("Contact: user@example.com or admin@test.org")
print(emails) # ['user@example.com', 'admin@test.org']
13.2 JavaScript
// ---- 基本操作 ----
// test: マッチの有無を確認
const pattern = /\d+/;
console.log(pattern.test("abc123")); // true
console.log(pattern.test("abcdef")); // false
// exec: 詳細なマッチ情報を取得
const regex = /(\d{4})-(\d{2})-(\d{2})/;
const result = regex.exec("Date: 2024-01-15");
if (result) {
console.log(result[0]); // "2024-01-15"
console.log(result[1]); // "2024"
console.log(result[2]); // "01"
console.log(result[3]); // "15"
console.log(result.index); // 6
}
// match: 文字列側のメソッド
const str = "cat bat sat";
console.log(str.match(/[a-z]at/g)); // ["cat", "bat", "sat"]
// matchAll: すべてのマッチを詳細に取得(ES2020)
const text = "Date: 2024-01-15, 2024-02-20";
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/g;
for (const match of text.matchAll(dateRegex)) {
console.log(`Full: ${match[0]}, Year: ${match[1]}`);
}
// ---- 置換 ----
// replace: パターン置換
const csv = "John,Smith,30";
const replaced = csv.replace(/(\w+),(\w+),(\d+)/, "$2, $1 (age: $3)");
console.log(replaced); // "Smith, John (age: 30)"
// replace with function
const prices = "Item A: $10, Item B: $20";
const updated = prices.replace(/\$(\d+)/g, (match, amount) => {
return `$${parseInt(amount) * 1.1}`;
});
console.log(updated); // "Item A: $11, Item B: $22"
// replaceAll(ES2021)
const result2 = "hello hello hello".replaceAll("hello", "hi");
console.log(result2); // "hi hi hi"
// ---- 分割 ----
// split: パターンで分割
const parts = "one, two, three;four".split(/[,;]\s*/);
console.log(parts); // ["one", "two", "three", "four"]
// ---- 名前付きグループ(ES2018) ----
const datePattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const dateMatch = datePattern.exec("2024-01-15");
console.log(dateMatch.groups.year); // "2024"
console.log(dateMatch.groups.month); // "01"
console.log(dateMatch.groups.day); // "15"
// ---- フラグ ----
const re1 = /pattern/gimsuy; // リテラル形式
const re2 = new RegExp("pattern", "gi"); // コンストラクタ形式
13.3 Java
import java.util.regex.*;
import java.util.List;
import java.util.ArrayList;
public class RegexExamples {
// ---- 基本操作 ----
public static void basicMatch() {
// パターンのコンパイルとマッチ
Pattern pattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher matcher = pattern.matcher("Date: 2024-01-15");
if (matcher.find()) {
System.out.println(matcher.group(0)); // "2024-01-15"
System.out.println(matcher.group(1)); // "2024"
System.out.println(matcher.group(2)); // "01"
System.out.println(matcher.group(3)); // "15"
}
// 名前付きグループ
Pattern namedPattern = Pattern.compile(
"(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})"
);
Matcher namedMatcher = namedPattern.matcher("2024-01-15");
if (namedMatcher.find()) {
System.out.println(namedMatcher.group("year")); // "2024"
}
}
// ---- すべてのマッチを取得 ----
public static List<String> findAll(String text, String regex) {
List<String> matches = new ArrayList<>();
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
matches.add(matcher.group());
}
return matches;
}
// ---- 置換 ----
public static void replacement() {
String text = "Hello World Hello Java";
String result = text.replaceAll("Hello", "Hi");
System.out.println(result); // "Hi World Hi Java"
// パターンを使った置換
Pattern pattern = Pattern.compile("(\\w+)@(\\w+)\\.(\\w+)");
Matcher matcher = pattern.matcher("user@example.com");
String replaced = matcher.replaceAll("[$1] at [$2] dot [$3]");
System.out.println(replaced); // "[user] at [example] dot [com]"
}
// ---- バリデーション ----
public static boolean isValidEmail(String email) {
return email.matches(
"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
);
}
// ---- 分割 ----
public static void splitting() {
String csv = "apple, banana, cherry;date";
String[] parts = csv.split("[,;]\\s*");
// ["apple", "banana", "cherry", "date"]
}
// ---- フラグ ----
public static void flags() {
// CASE_INSENSITIVE: 大文字小文字無視
Pattern p1 = Pattern.compile("hello", Pattern.CASE_INSENSITIVE);
// MULTILINE: マルチライン
Pattern p2 = Pattern.compile("^line", Pattern.MULTILINE);
// DOTALL: . が改行にもマッチ
Pattern p3 = Pattern.compile("start.*end", Pattern.DOTALL);
// COMMENTS: 拡張モード(空白・コメント)
Pattern p4 = Pattern.compile(
"\\d{4} # 年\n" +
"- # 区切り\n" +
"\\d{2} # 月\n" +
"- # 区切り\n" +
"\\d{2} # 日",
Pattern.COMMENTS
);
// 複数フラグの組み合わせ
Pattern p5 = Pattern.compile("pattern",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL
);
}
}
13.4 Go
package main
import (
"fmt"
"regexp"
"strings"
)
func main() {
// ---- 基本操作 ----
// コンパイル(RE2エンジン — 線形時間保証)
re := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
// マッチの確認
fmt.Println(re.MatchString("2024-01-15")) // true
// 最初のマッチを取得
match := re.FindString("Date: 2024-01-15 and 2024-02-20")
fmt.Println(match) // "2024-01-15"
// サブマッチ(キャプチャグループ)
submatch := re.FindStringSubmatch("Date: 2024-01-15")
fmt.Println(submatch[0]) // "2024-01-15"(全体)
fmt.Println(submatch[1]) // "2024"
fmt.Println(submatch[2]) // "01"
fmt.Println(submatch[3]) // "15"
// すべてのマッチ
allMatches := re.FindAllString("2024-01-15 and 2024-02-20", -1)
fmt.Println(allMatches) // ["2024-01-15", "2024-02-20"]
// ---- 置換 ----
result := re.ReplaceAllString("Date: 2024-01-15", "${2}/${3}/${1}")
fmt.Println(result) // "Date: 01/15/2024"
// 関数による置換
result2 := re.ReplaceAllStringFunc(
"Score: 100, Level: 5",
func(match string) string {
return strings.ToUpper(match)
},
)
fmt.Println(result2)
// ---- 分割 ----
splitRe := regexp.MustCompile(`[,;\s]+`)
parts := splitRe.Split("a,b; c d", -1)
fmt.Println(parts) // ["a", "b", "c", "d"]
// ---- 名前付きグループ ----
namedRe := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
match2 := namedRe.FindStringSubmatch("2024-01-15")
for i, name := range namedRe.SubexpNames() {
if name != "" {
fmt.Printf("%s: %s\n", name, match2[i])
}
}
// ---- GoのRE2エンジンの制約 ----
// 以下はGoでは使用不可:
// - バックリファレンス (\1, \2)
// - 先読み/後読み ((?=...), (?<=...))
// - 独占的量指定子 (*+, ++, ?+)
// - アトミックグループ ((?>...))
// これにより、すべてのパターンが線形時間で処理される保証がある
}
13.5 Ruby
# ---- 基本操作 ----
# マッチの確認
puts "hello123" =~ /\d+/ # 5(マッチ位置)
puts "hello" =~ /\d+/ # nil(マッチなし)
# match メソッド
match = "2024-01-15".match(/(\d{4})-(\d{2})-(\d{2})/)
puts match[0] # "2024-01-15"
puts match[1] # "2024"
puts match[2] # "01"
puts match[3] # "15"
# 名前付きキャプチャ
match = "2024-01-15".match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/)
puts match[:year] # "2024"
puts match[:month] # "01"
puts match[:day] # "15"
# scan: すべてのマッチを配列で取得
"cat bat sat".scan(/[a-z]at/) # ["cat", "bat", "sat"]
"2024-01-15, 2024-02-20".scan(/\d{4}-\d{2}-\d{2}/) # ["2024-01-15", "2024-02-20"]
# ---- 置換 ----
# sub: 最初のマッチを置換
"Hello World Hello".sub(/Hello/, "Hi") # "Hi World Hello"
# gsub: すべてのマッチを置換
"Hello World Hello".gsub(/Hello/, "Hi") # "Hi World Hi"
# ブロック付き置換
"Score: 100, Level: 5".gsub(/\d+/) { |m| m.to_i * 2 }
# "Score: 200, Level: 10"
# ---- 特殊変数 ----
if "Hello World" =~ /(\w+)\s(\w+)/
puts $~ # マッチ全体のMatchDataオブジェクト
puts $1 # "Hello"(最初のキャプチャ)
puts $2 # "World"(2番目のキャプチャ)
puts $& # "Hello World"(マッチ全体)
puts $` # ""(マッチの前)
puts $' # ""(マッチの後)
end
13.6 各言語の比較表
| 機能 | Python | JavaScript | Java | Go | Ruby |
|---|---|---|---|---|---|
| エンジン | NFA | NFA | NFA | RE2(DFA) | Onigmo(NFA) |
| 先読み | ✓ | ✓ | ✓ | ✗ | ✓ |
| 後読み | ✓(固定長) | ✓(ES2018) | ✓ | ✗ | ✓ |
| バックリファレンス | ✓ | ✓ | ✓ | ✗ | ✓ |
| 名前付きグループ | (?P<n>) | (?<n>) | (?<n>) | (?P<n>) | (?<n>) |
| Unicode | re.UNICODE | /u | デフォルト | デフォルト | デフォルト |
| 拡張モード | re.VERBOSE | ✗ | COMMENTS | ✗ | /x |
| アトミックグループ | ✗ | ✗ | ✓(Java9+) | ✗ | ✓ |
| 条件付き | ✗ | ✗ | ✗ | ✗ | ✓ |
14. コマンドラインツールでの正規表現
14.1 grep
grep(Global Regular Expression Print)は、ファイル内でパターンにマッチする行を検索する。
# 基本的な使い方
grep 'pattern' file.txt
# 主要なオプション
grep -i 'pattern' file.txt # 大文字小文字無視
grep -n 'pattern' file.txt # 行番号を表示
grep -c 'pattern' file.txt # マッチ行数を表示
grep -l 'pattern' *.txt # マッチするファイル名を表示
grep -r 'pattern' dir/ # ディレクトリを再帰検索
grep -v 'pattern' file.txt # マッチしない行を表示(反転)
grep -w 'word' file.txt # 単語全体マッチ
grep -o 'pattern' file.txt # マッチ部分のみ表示
grep -A 3 'pattern' file.txt # マッチ行の後3行も表示
grep -B 2 'pattern' file.txt # マッチ行の前2行も表示
grep -C 2 'pattern' file.txt # マッチ行の前後2行を表示
grep -P 'pattern' file.txt # PCRE(Perl互換正規表現)を使用
# ERE(拡張正規表現)の使用
grep -E 'pattern1|pattern2' file.txt
egrep 'pattern1|pattern2' file.txt # 同等
# ---- 実践的な例 ----
# ログファイルからエラーを抽出
grep -i 'error\|warning\|critical' /var/log/syslog
# 特定のIPアドレスを含む行を検索
grep -E '\b192\.168\.\d{1,3}\.\d{1,3}\b' access.log
# コメント行と空行を除外して設定を表示
grep -vE '^\s*(#|$)' /etc/nginx/nginx.conf
# 特定の関数呼び出しを再帰的に検索
grep -rn 'def\s\+process_data' --include='*.py' src/
# TODOコメントを含むファイルを検索
grep -rl 'TODO\|FIXME\|HACK' --include='*.{py,js,java}' .
# メールアドレスを抽出
grep -oE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' contacts.txt
# JSON内の特定のキーを含む行
grep -P '"name"\s*:\s*"[^"]*admin[^"]*"' config.json
14.2 sed
sed(Stream Editor)は、テキストの変換・置換を行うストリームエディタ。
# 基本的な置換
sed 's/old/new/' file.txt # 各行の最初のマッチを置換
sed 's/old/new/g' file.txt # すべてのマッチを置換
sed -i 's/old/new/g' file.txt # ファイルを直接編集
sed -i.bak 's/old/new/g' file.txt # バックアップ付きで直接編集
# 区切り文字の変更(パス操作で便利)
sed 's|/usr/local|/opt|g' config.txt
sed 's#http://#https://#g' urls.txt
# ---- 実践的な例 ----
# 行番号を指定して置換
sed '3s/old/new/' file.txt # 3行目のみ置換
sed '1,5s/old/new/g' file.txt # 1〜5行目で置換
# 特定のパターンを含む行で置換
sed '/error/s/status=.*/status=failed/g' log.txt
# 行の削除
sed '/^#/d' file.txt # コメント行を削除
sed '/^$/d' file.txt # 空行を削除
sed '/^#/d; /^$/d' config.txt # コメント行と空行を削除
# 行の追加
sed '/pattern/a\新しい行' file.txt # マッチ行の後に追加
sed '/pattern/i\新しい行' file.txt # マッチ行の前に追加
# グループ参照を使った置換
sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/g' dates.txt
# 2024-01-15 → 15/01/2024
# CSV内のフィールド入れ替え
sed -E 's/^([^,]+),([^,]+),(.+)/\2,\1,\3/' data.csv
# 行末の空白を除去
sed 's/[[:space:]]*$//' file.txt
# 複数の置換を連続実行
sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txt
# 特定のブロックを削除(開始パターンから終了パターンまで)
sed '/BEGIN_DELETE/,/END_DELETE/d' file.txt
# タブをスペース4つに変換
sed 's/\t/ /g' file.txt
14.3 awk
awkはパターンマッチングとテキスト処理のための言語。
# 基本構文
awk '/pattern/ { action }' file.txt
# ---- 実践的な例 ----
# 特定のパターンを含む行を表示
awk '/error/' log.txt
# フィールドの抽出
awk -F, '{print $1, $3}' data.csv # CSV の1列目と3列目
awk -F: '{print $1}' /etc/passwd # ユーザー名の一覧
# 正規表現でフィルタリング
awk '$3 ~ /^[0-9]+$/ {print $0}' data.txt # 3列目が数字の行
awk '$0 ~ /error|warning/ {print NR": "$0}' log.txt # エラーまたは警告
# 条件付き処理
awk '/^ERROR/ {errors++} END {print "Errors:", errors}' log.txt
# フィールドの置換
awk -F, 'BEGIN{OFS=","} {gsub(/old/, "new", $2); print}' data.csv
# 複数のパターン
awk '
/^START/ { capture = 1; next }
/^END/ { capture = 0; next }
capture { print }
' file.txt
# アクセスログの解析(IPアドレスごとのリクエスト数)
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -20
# 特定のパターンの前後N行を表示(grepの-C相当)
awk '/pattern/ {for(i=NR-2;i<=NR+2;i++) lines[i]=1}
{stored[NR]=$0}
END {for(i=1;i<=NR;i++) if(i in lines) print stored[i]}' file.txt
14.4 その他のツール
# ---- ripgrep (rg) ----
# 高速な grep 代替ツール
rg 'pattern' . # 再帰検索(デフォルト)
rg -i 'pattern' . # 大文字小文字無視
rg -t py 'import\s+re' . # Pythonファイルのみ
rg --pcre2 '(?<=@)\w+' emails.txt # PCRE2を使用
rg -U 'start\n.*\nend' file.txt # マルチライン
# ---- perl (one-liner) ----
# 先読み/後読みを使った高度な置換
perl -pe 's/(?<=\$)\d+/sprintf("%.2f", $&*1.1)/ge' prices.txt
# マルチライン処理
perl -0777 -pe 's/start.*?end/REPLACED/gs' file.txt
# 名前付きキャプチャの使用
perl -ne 'print "$+{ip}\n" if /(?<ip>\d+\.\d+\.\d+\.\d+)/' log.txt
# ---- find + xargs + grep の組み合わせ ----
# 特定のファイルタイプ内でパターンを検索
find . -name "*.py" -exec grep -l "import re" {} +
# 大量のファイルで効率的に検索
find . -name "*.log" -print0 | xargs -0 grep -l "ERROR"
# ---- jq での正規表現 ----
# JSONからパターンマッチで値を抽出
echo '{"email":"user@example.com"}' | jq 'select(.email | test("@example"))'
# 正規表現で値をフィルタ
cat data.json | jq '.[] | select(.name | test("^John"))'
15. 実践的なパターン集
15.1 バリデーション用パターン
# メールアドレス(RFC 5322 準拠の簡易版)
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
# より厳密なメールアドレス
^(?:[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*
|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]
|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")
@(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,})$
# URL
^https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&/=]*)$
# IPv4アドレス(厳密版)
^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$
# IPv6アドレス(簡易版)
^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$
# 日本の電話番号
^0\d{1,4}-\d{1,4}-\d{4}$
# 日本の携帯電話番号
^0[789]0-\d{4}-\d{4}$
# 日本の郵便番号
^\d{3}-\d{4}$
# クレジットカード番号(Luhnアルゴリズムは別途必要)
^(?:4[0-9]{12}(?:[0-9]{3})? # Visa
|5[1-5][0-9]{14} # MasterCard
|3[47][0-9]{13} # AmEx
|6(?:011|5[0-9]{2})[0-9]{12} # Discover
|(?:2131|1800|35\d{3})\d{11})$ # JCB
# 日本語文字列の検出
[\u3040-\u309F] # ひらがな
[\u30A0-\u30FF] # カタカナ
[\u4E00-\u9FFF] # 漢字(CJK統合漢字)
[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF]+ # 日本語文字列
15.2 データ抽出パターン
import re
# ---- ログ解析 ----
# Apacheアクセスログの解析
apache_log_pattern = re.compile(
r'(?P<ip>\d+\.\d+\.\d+\.\d+)' # IPアドレス
r'\s+-\s+' # -
r'(?P<user>\S+)' # ユーザー
r'\s+\[(?P<date>[^\]]+)\]' # 日時
r'\s+"(?P<method>\w+)' # HTTPメソッド
r'\s+(?P<path>\S+)' # パス
r'\s+(?P<protocol>[^"]+)"' # プロトコル
r'\s+(?P<status>\d+)' # ステータスコード
r'\s+(?P<size>\d+)' # レスポンスサイズ
)
log_line = '192.168.1.1 - admin [15/Jan/2024:10:30:00 +0900] "GET /api/users HTTP/1.1" 200 1234'
match = apache_log_pattern.match(log_line)
if match:
print(f"IP: {match.group('ip')}")
print(f"Status: {match.group('status')}")
# ---- 構造化データの抽出 ----
# HTML内のリンク抽出
html = '<a href="https://example.com">Link 1</a> <a href="https://test.org">Link 2</a>'
links = re.findall(r'href="([^"]+)"', html)
# ['https://example.com', 'https://test.org']
# HTMLタグの除去
clean_text = re.sub(r'<[^>]+>', '', html)
# 'Link 1 Link 2'
# JSONからのキー・値抽出(簡易版。本来はjsonモジュールを使用すべき)
json_text = '{"name": "John", "age": 30, "city": "Tokyo"}'
pairs = re.findall(r'"(\w+)"\s*:\s*"?([^",}]+)"?', json_text)
# [('name', 'John'), ('age', '30'), ('city', 'Tokyo')]
# ---- テキスト処理 ----
# マークダウンの見出し抽出
markdown = """
# Title
## Section 1
### Subsection 1.1
## Section 2
"""
headings = re.findall(r'^(#{1,6})\s+(.+)$', markdown, re.MULTILINE)
# [('#', 'Title'), ('##', 'Section 1'), ('###', 'Subsection 1.1'), ('##', 'Section 2')]
# ソースコードからの関数定義抽出
code = """
def process_data(input_data, threshold=0.5):
pass
def calculate_score(x, y):
pass
class DataProcessor:
def transform(self, data):
pass
"""
functions = re.findall(r'def\s+(\w+)\s*\(([^)]*)\)', code)
# [('process_data', 'input_data, threshold=0.5'),
# ('calculate_score', 'x, y'),
# ('transform', 'self, data')]
15.3 テキスト変換パターン
import re
# CamelCase → snake_case
def camel_to_snake(name):
s1 = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
print(camel_to_snake("getUserName")) # "get_user_name"
print(camel_to_snake("HTTPResponse")) # "http_response"
print(camel_to_snake("getHTTPSURL")) # "get_https_url"
# snake_case → CamelCase
def snake_to_camel(name):
return re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name.capitalize())
print(snake_to_camel("get_user_name")) # "GetUserName"
# 数値のカンマ区切り
def add_commas(number_str):
return re.sub(r'(\d)(?=(\d{3})+(?!\d))', r'\1,', number_str)
print(add_commas("1234567890")) # "1,234,567,890"
# 全角英数字 → 半角英数字
def fullwidth_to_halfwidth(text):
return re.sub(r'[A-Za-z0-9]',
lambda m: chr(ord(m.group()) - 0xFEE0), text)
print(fullwidth_to_halfwidth("Hello123")) # "Hello123"
# 連続するスペースを1つに正規化
def normalize_spaces(text):
return re.sub(r'\s+', ' ', text).strip()
print(normalize_spaces(" hello world \n foo ")) # "hello world foo"
16. パフォーマンスと最適化
16.1 パフォーマンスに影響する要因
正規表現のパフォーマンスは、パターンの構造と対象文字列の特性に大きく依存する。
主な影響要因:
1. バックトラッキングの量
2. パターンの複雑さ
3. 量指定子の使い方(貪欲 vs 怠惰 vs 独占的)
4. 先読み/後読みの使用
5. 文字クラスの範囲
6. パターンのコンパイル有無
7. 対象文字列の長さ
16.2 避けるべきパターン
# ---- 壊滅的バックトラッキング ----
# 危険なパターン(指数的なバックトラッキング)
(a+)+b # "aaaaaaaaaaac" で極端に遅くなる
(a|aa)+b # 同様の問題
(.*a){10} # 繰り返し内の .* が危険
# 具体例:
パターン: (a+)+$
対象: "aaaaaaaaaaaaaaaaaaaaac"
→ バックトラッキングが指数的に増加し、処理が完了しない
理由:
a+ が "aaaaaa..." 全体を消費
→ $ にマッチしない(末尾が 'c')
→ バックトラック
→ 外側の + により異なる分割パターンをすべて試行
→ n文字の場合、2^n通りの組み合わせを探索
16.3 最適化テクニック
# ---- 1. アンカーの活用 ----
# 悪い例(文字列全体を走査)
パターン: \d{4}-\d{2}-\d{2}
# 良い例(先頭からマッチ)
パターン: ^\d{4}-\d{2}-\d{2}
# ---- 2. 具体的な文字クラスの使用 ----
# 悪い例(.* は何にでもマッチし、大量のバックトラック)
パターン: ".*"
# 良い例(否定文字クラスでバックトラックを回避)
パターン: "[^"]*"
# ---- 3. 独占的量指定子の活用 ----
# バックトラックが不要な場合
パターン: \w++\s # 独占的(バックトラックなし)
vs
パターン: \w+\s # 貪欲(バックトラックあり)
# ---- 4. アトミックグループの活用 ----
パターン: (?>abc|abd) # "abc" を試してダメなら即失敗
vs
パターン: (abc|abd) # "abc" を試してダメなら "abd" も試す
# ---- 5. 非キャプチャグループの使用 ----
# キャプチャが不要な場合
パターン: (?:pattern) # メモリ節約
vs
パターン: (pattern) # キャプチャのオーバーヘッド
# ---- 6. 交替(|)の順序 ----
# よくマッチする選択肢を先に書く
パターン: (common|rare|very_rare) # 頻度順
# 共通接頭辞をまとめる
パターン: (?:there|the|then) # 非効率
パターン: the(?:re|n)? # 効率的
16.4 パフォーマンス測定
import re
import time
# パフォーマンス計測
def benchmark_regex(pattern, text, iterations=100000):
compiled = re.compile(pattern)
start = time.perf_counter()
for _ in range(iterations):
compiled.search(text)
elapsed = time.perf_counter() - start
print(f"Pattern: {pattern}")
print(f"Time: {elapsed:.4f}s ({iterations} iterations)")
print(f"Per iteration: {elapsed/iterations*1000000:.2f}μs")
print()
# 比較テスト
text = "The quick brown fox jumps over the lazy dog"
# 貪欲 vs 怠惰
benchmark_regex(r'".*"', '"hello world" said "foo"')
benchmark_regex(r'".*?"', '"hello world" said "foo"')
# . vs 否定文字クラス
benchmark_regex(r'".*?"', '"hello world"')
benchmark_regex(r'"[^"]*"', '"hello world"') # こちらが高速
# コマンドラインでのパフォーマンス比較
time grep -cP 'pattern1' large_file.txt
time grep -cP 'pattern2' large_file.txt
17. セキュリティとReDoS攻撃
17.1 ReDoS(Regular Expression Denial of Service)
ReDoSは、悪意のある入力によって正規表現エンジンを指数的なバックトラッキングに陥らせ、サービス拒否を引き起こす攻撃である。
攻撃の原理:
1. 脆弱な正規表現パターンを特定
2. バックトラッキングを最大化する入力を構築
3. CPU使用率が100%に張り付き、サービスが応答不能に
脆弱なパターンの特徴:
- ネストした量指定子: (a+)+, (a*)*
- 重複する選択肢: (a|a)+
- 重複する量指定子とリテラル: (a+)+b
17.2 実際のReDoS事例
# 2016年 Stack Overflow の障害
パターン: ^[\s\u200c]+|[\s\u200c]+$
攻撃入力: 大量のスペース(20,000文字以上)
結果: 34分間のダウンタイム
# 2019年 Cloudflare の障害
原因: WAFルール内の脆弱な正規表現
パターン: (?:(?:\"|'|\]|\}|\\|\d|(?:nan|infinity|true|false|null|
undefined|symbol|math)|\`|\-|\+)+[)]*;?((?:\s|-|~|!|
\{\}|\|\||\+)*.*(?:.*=.*)))
結果: 27分間のグローバルダウン
17.3 脆弱性の検出と対策
# ---- 脆弱なパターンの例 ----
# 脆弱: ネストした量指定子
vulnerable_patterns = [
r'(a+)+',
r'(a|aa)+',
r'(.*a){x}', # x > 10
r'([a-zA-Z]+)*',
r'(a+|b+|c+)*',
]
# ---- 安全なパターンへの書き換え ----
# 脆弱: (a+)+$
# 安全: a+$
# 脆弱: ([a-zA-Z]+)*
# 安全: [a-zA-Z]* または [a-zA-Z]++(独占的)
# 脆弱: (.*\n)*
# 安全: [\s\S]* または [^\n]*(\n[^\n]*)*
17.4 防御策
import re
import signal
# ---- 1. タイムアウトの設定 ----
class RegexTimeout(Exception):
pass
def timeout_handler(signum, frame):
raise RegexTimeout("Regex execution timed out")
def safe_match(pattern, text, timeout=1):
"""タイムアウト付きの正規表現マッチ"""
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(timeout)
try:
result = re.search(pattern, text)
signal.alarm(0)
return result
except RegexTimeout:
return None
# ---- 2. 入力長の制限 ----
def validate_with_limit(pattern, text, max_length=1000):
"""入力長を制限した正規表現バリデーション"""
if len(text) > max_length:
raise ValueError(f"Input too long: {len(text)} > {max_length}")
return re.match(pattern, text)
# ---- 3. RE2エンジンの使用 ----
# google-re2 パッケージ(線形時間保証)
# pip install google-re2
import re2
# RE2は後方参照をサポートしないが、ReDoSの心配がない
pattern = re2.compile(r'[a-z]+@[a-z]+\.[a-z]+')
result = pattern.search("user@example.com")
// Java: タイムアウト付きマッチ
import java.util.concurrent.*;
import java.util.regex.*;
public class SafeRegex {
public static boolean safeMatch(String pattern, String input, int timeoutMs) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Boolean> future = executor.submit(() -> {
return Pattern.matches(pattern, input);
});
try {
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
future.cancel(true);
return false;
} catch (Exception e) {
return false;
} finally {
executor.shutdown();
}
}
}
17.5 ReDoS検出ツール
ツール名 言語/環境 特徴
safe-regex Node.js パターンの安全性チェック
rxxr2 OCaml 学術的なReDoS検出
regex-static-analysis Python 静的解析
redos-checker Web オンラインチェッカー
18. 正規表現のデバッグとテスト
18.1 オンラインツール
regex101.com - PCRE, JavaScript, Python, Go対応。リアルタイムマッチング
regexr.com - JavaScriptベース。コミュニティパターン
debuggex.com - 正規表現の可視化(状態遷移図)
regexper.com - 鉄道図(Railroad diagram)生成
18.2 デバッグのアプローチ
1. パターンを分解する
大きなパターンを小さなパーツに分割し、各パーツを個別にテスト
2. テストケースを用意する
- マッチすべき入力(正常系)
- マッチすべきでない入力(異常系)
- エッジケース(空文字、極端に長い文字列、特殊文字)
3. 段階的に構築する
シンプルなパターンから始めて、徐々に複雑にしていく
4. フラグの影響を確認する
i, m, s, x などのフラグがマッチに与える影響を確認
18.3 ユニットテスト
import re
import unittest
class TestEmailRegex(unittest.TestCase):
"""メールアドレス正規表現のテスト"""
EMAIL_PATTERN = re.compile(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
)
def test_valid_emails(self):
valid = [
"user@example.com",
"user.name@example.com",
"user+tag@example.co.jp",
"user123@test.org",
"a@b.cc",
]
for email in valid:
with self.subTest(email=email):
self.assertRegex(email, self.EMAIL_PATTERN)
def test_invalid_emails(self):
invalid = [
"",
"user",
"@example.com",
"user@",
"user@.com",
"user@com",
"user @example.com",
"user@example",
]
for email in invalid:
with self.subTest(email=email):
self.assertNotRegex(email, self.EMAIL_PATTERN)
def test_edge_cases(self):
# 長いメールアドレス
long_local = "a" * 64 + "@example.com"
self.assertRegex(long_local, self.EMAIL_PATTERN)
# 特殊文字
self.assertRegex("user.name+tag@example.com", self.EMAIL_PATTERN)
if __name__ == '__main__':
unittest.main()
// Jest を使ったテスト
describe('Date Regex', () => {
const dateRegex = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
test.each([
'2024-01-15',
'2024-12-31',
'2024-02-29',
])('should match valid date: %s', (date) => {
expect(dateRegex.test(date)).toBe(true);
});
test.each([
'2024-13-01', // 月が13
'2024-00-15', // 月が00
'2024-01-32', // 日が32
'24-01-15', // 年が2桁
'2024/01/15', // 区切り文字が違う
])('should not match invalid date: %s', (date) => {
expect(dateRegex.test(date)).toBe(false);
});
});
19. Unicode と国際化対応
19.1 Unicodeプロパティ
Unicode対応の正規表現では、文字のプロパティでマッチングできる。
構文: \p{PropertyName} または \p{PropertyName=Value}
# 文字カテゴリ
\p{L} 任意のアルファベット文字(Letter)
\p{Lu} 大文字(Uppercase Letter)
\p{Ll} 小文字(Lowercase Letter)
\p{N} 任意の数字(Number)
\p{Nd} 十進数字(Decimal Number)
\p{P} 句読点(Punctuation)
\p{S} 記号(Symbol)
\p{Z} 区切り(Separator)
\p{M} 結合文字(Mark)
# スクリプト(文字体系)
\p{Han} 漢字
\p{Hiragana} ひらがな
\p{Katakana} カタカナ
\p{Latin} ラテン文字
\p{Cyrillic} キリル文字
\p{Arabic} アラビア文字
\p{Hangul} ハングル
# ブロック
\p{InCJKUnifiedIdeographs} CJK統合漢字
\p{InBasicLatin} 基本ラテン文字
19.2 各言語でのUnicode対応
# Python
import re
# Unicode文字のマッチ
text = "日本語のテスト hello 世界"
# Unicode文字クラス(Python 3はデフォルトでUnicode)
hiragana = re.findall(r'[\u3040-\u309F]+', text)
katakana = re.findall(r'[\u30A0-\u30FF]+', text)
kanji = re.findall(r'[\u4E00-\u9FFF]+', text)
# \w はUnicodeの単語文字にもマッチ(Python 3)
words = re.findall(r'\w+', text)
# ['日本語のテスト', 'hello', '世界']
# regexモジュール(サードパーティ)でUnicodeプロパティ使用
# pip install regex
import regex
japanese = regex.findall(r'\p{Han}+', text) # ['日本語', '世界']
hiragana = regex.findall(r'\p{Hiragana}+', text) # ['の']
katakana = regex.findall(r'\p{Katakana}+', text) # ['テスト']
// JavaScript(ES2018+)
const text = "日本語のテスト hello 世界";
// Unicode プロパティエスケープ(/u フラグ必須)
const kanji = text.match(/\p{Script=Han}+/gu);
// ['日本語', '世界']
const hiragana = text.match(/\p{Script=Hiragana}+/gu);
// ['の']
const katakana = text.match(/\p{Script=Katakana}+/gu);
// ['テスト']
// カテゴリによるマッチ
const letters = text.match(/\p{Letter}+/gu);
// ['日本語のテスト', 'hello', '世界']
const numbers = "123 456 ⅦⅧⅨ".match(/\p{Number}+/gu);
// ['123', '456', 'ⅦⅧⅨ']
19.3 日本語テキスト処理の実践例
import re
# ---- 日本語テキストの処理 ----
# 全角スペースの正規化
text = "全角 スペース の 処理"
normalized = re.sub(r'[\u3000\s]+', ' ', text) # "全角 スペース の 処理"
# カタカナの抽出
text = "アップル社のiPhoneはスマートフォンです"
katakana_words = re.findall(r'[\u30A0-\u30FF]+', text)
# ['アップル', 'スマートフォン']
# 数値表現の統一(全角→半角)
text = "価格は1,234円です"
result = re.sub(r'[0-9]', lambda m: chr(ord(m.group()) - 0xFEE0), text)
result = result.replace(',', ',')
# "価格は1,234円です"
# 日本語の文区切り
text = "最初の文。次の文!最後の文?"
sentences = re.split(r'(?<=[。!?])', text)
# ['最初の文。', '次の文!', '最後の文?']
# ルビの除去(HTMLふりがな)
html = '<ruby>漢字<rp>(</rp><rt>かんじ</rt><rp>)</rp></ruby>'
clean = re.sub(r'<ruby>(.*?)<rp>.*?</rp><rt>.*?</rt><rp>.*?</rp></ruby>', r'\1', html)
# '漢字'
20. 正規表現のベストプラクティス
20.1 可読性の確保
# ---- 悪い例: 1行の暗号的パターン ----
pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
# ---- 良い例: 拡張モードで分かりやすく ----
pattern = re.compile(r'''
^
(?:
(?:
25[0-5] # 250-255
| 2[0-4][0-9] # 200-249
| [01]?[0-9][0-9]? # 0-199
)
\. # ドット区切り
){3} # 最初の3オクテット
(?:
25[0-5]
| 2[0-4][0-9]
| [01]?[0-9][0-9]?
) # 最後のオクテット
$
''', re.VERBOSE)
# ---- さらに良い例: パーツに分割 ----
OCTET = r'(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)'
IPV4_PATTERN = re.compile(
rf'^(?:{OCTET}\.){{3}}{OCTET}$'
)
20.2 よくある間違いと対策
# ---- 1. 貪欲マッチの意図しない動作 ----
# 間違い: HTMLタグのマッチ
パターン: <.*>
対象: "<b>text</b>"
結果: "<b>text</b>" 全体にマッチ(意図しない)
# 修正: 怠惰マッチまたは否定文字クラス
パターン: <.*?> または <[^>]*>
# ---- 2. ^$ の意味の誤解 ----
# 問題: マルチラインでの動作
パターン: ^pattern$
→ デフォルトでは文字列全体の先頭/末尾
→ mフラグで各行の先頭/末尾
# ---- 3. 特殊文字のエスケープ忘れ ----
# 間違い
パターン: file.txt # file + 任意の1文字 + txt にマッチ
パターン: price: $100 # $ がアンカーとして解釈される
# 修正
パターン: file\.txt
パターン: price: \$100
# ---- 4. 文字クラス内のメタ文字 ----
# 文字クラス内ではほとんどのメタ文字がリテラルになる
[.+*?] # これらはリテラルの ., +, *, ? にマッチ
[-abc] # ハイフンは先頭か末尾に置く
[abc-] # または末尾に
[a\-z] # またはエスケープ
[^abc] # ^ は先頭でのみ否定の意味
[a^bc] # 先頭以外の ^ はリテラル
# ---- 5. 空文字列へのマッチ ----
# 問題: * は0回マッチを許容
パターン: \d*
対象: "abc"
結果: 空文字列にマッチ(各位置で0回マッチ)
# 修正: + を使用して1回以上を要求
パターン: \d+
20.3 正規表現を使うべき場面と使うべきでない場面
# ---- 正規表現を使うべき場面 ----
✓ テキストのパターン検索
✓ 入力バリデーション(メール、電話番号、日付など)
✓ ログファイルの解析
✓ テキストの一括置換
✓ 簡易的なテキストパーサー
✓ コマンドラインでのテキスト処理
# ---- 正規表現を使うべきでない場面 ----
✗ HTMLやXMLの本格的なパース → DOMパーサーを使用
✗ JSONの解析 → JSONパーサーを使用
✗ 複雑な文法の解析 → パーサーコンビネータや構文解析器を使用
✗ 数値の範囲チェック → プログラムロジックで実装
✗ 完全なメールアドレスのバリデーション → 専用ライブラリを使用
✗ URLの完全なパース → URLパーサーを使用
20.4 メンテナンス性を高めるTips
# 1. 定数として定義する
EMAIL_PATTERN = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
PHONE_PATTERN = re.compile(r'^0\d{1,4}-\d{1,4}-\d{4}$')
DATE_PATTERN = re.compile(r'^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$')
# 2. パーツに分割して組み立てる
YEAR = r'(?P<year>\d{4})'
MONTH = r'(?P<month>0[1-9]|1[0-2])'
DAY = r'(?P<day>0[1-9]|[12]\d|3[01])'
DATE_PATTERN = re.compile(f'^{YEAR}-{MONTH}-{DAY}$')
# 3. コメントを活用する
APACHE_LOG = re.compile(r'''
^
(?P<ip>\S+) # クライアントIP
\s+\S+\s+ # identとauthuser
(?P<user>\S+) # ユーザー名
\s+\[(?P<time>[^\]]+)\] # タイムスタンプ
\s+"(?P<request>[^"]*)" # リクエスト行
\s+(?P<status>\d+) # ステータスコード
\s+(?P<size>\S+) # レスポンスサイズ
$
''', re.VERBOSE)
# 4. テストを書く(前述のユニットテスト参照)
# 5. 代替手段も検討する
# 単純な文字列操作で済む場合は正規表現を使わない
# 悪い例
re.sub(r'^https://', '', url)
# 良い例
url.removeprefix('https://') # Python 3.9+
# 悪い例
re.split(r',', csv_line)
# 良い例
csv_line.split(',')
21. まとめ
21.1 正規表現の学習ロードマップ
レベル1(初級):
- リテラル文字のマッチ
- 基本メタ文字: . ^ $ * + ?
- 文字クラス: [...], [^...], \d, \w, \s
- 量指定子: {n}, {n,}, {n,m}
- 基本的なgrepとsedの使用
レベル2(中級):
- グループ化とキャプチャ: (...), (?:...)
- バックリファレンス: \1, \2
- 選択: |
- 貪欲 vs 怠惰マッチ
- アンカー: \b, \A, \Z
- フラグ: i, g, m, s, x
レベル3(上級):
- 先読み/後読み: (?=), (?!), (?<=), (?<!)
- 名前付きキャプチャ
- 独占的量指定子
- アトミックグループ
- 条件付きパターン
- Unicodeプロパティ
レベル4(エキスパート):
- エンジンの内部動作(NFA/DFA)
- パフォーマンス最適化
- ReDoS脆弱性の理解と対策
- 各言語エンジンの違いの把握
- 複雑なパターンの設計とデバッグ
21.2 クイックリファレンス
基本メタ文字:
. 任意の1文字 \d 数字 [0-9]
^ 行頭 \D 数字以外
$ 行末 \w 単語文字 [a-zA-Z0-9_]
| OR \W 単語文字以外
\ エスケープ \s 空白文字
() グループ化 \S 空白文字以外
[] 文字クラス \b 単語境界
量指定子:
* 0回以上(貪欲) *? 0回以上(怠惰)
+ 1回以上(貪欲) +? 1回以上(怠惰)
? 0 or 1回 ?? 0 or 1回(怠惰)
{n} ちょうどn回 {n}? n回(怠惰)
{n,} n回以上 {n,}? n回以上(怠惰)
{n,m} n〜m回 {n,m}? n〜m回(怠惰)
先読み/後読み:
(?=X) 肯定先読み (?<=X) 肯定後読み
(?!X) 否定先読み (?<!X) 否定後読み
グループ:
(X) キャプチャグループ
(?:X) 非キャプチャグループ
(?P<n>X) 名前付きグループ(Python)
(?<n>X) 名前付きグループ(JS/.NET)
(?>X) アトミックグループ
フラグ:
i 大文字小文字無視 m マルチライン
s ドットオール x 拡張(コメント付き)
g グローバル u Unicode
21.3 推奨リソース
書籍:
- 『詳説 正規表現 第3版』(Jeffrey E.F. Friedl著)
→ 正規表現のバイブル的存在
オンラインリソース:
- regex101.com — インタラクティブなテスト・学習環境
- regular-expressions.info — 包括的な正規表現リファレンス
- MDN Web Docs — JavaScript正規表現リファレンス
- Python re モジュールドキュメント
ツール:
- regex101.com — パターンテスト・デバッグ
- regexr.com — 視覚的なパターンテスト
- grex — 例からパターンを自動生成(CLIツール)
正規表現は、一度習得すれば、テキスト処理のあらゆる場面で強力な武器となる。基本を確実に押さえ、実践を通じて応用力を磨いていくことで、効率的で安全な正規表現を書けるようになるだろう。重要なのは、正規表現が最適な解決策かどうかを常に判断し、適材適所で活用することである。