5分で始める正規表現入門
Regex(正規表現)とは、テキスト内のパターンを記述するための方法です。基本的な知識があるだけで、普通なら20行かかるような文字列の検索・検証・変換が一気にできるようになります。
以前、乱雑なログファイルからメールアドレスをすべて抽出しなければならないことがありました。文字列操作のロジックを2時間かけて書いていたところ、先輩エンジニアが1行の正規表現で解決してくれたのです。その瞬間、テキスト処理に対する考え方が根本から変わりました。正規表現は、週末の練習がキャリア全体に渡って効いてくる、稀有なスキルのひとつです。
まずは最もシンプルな例から始めましょう。Pythonを開いて試してみてください:
import re
text = "Contact us at [email protected] or [email protected]"
matches = re.findall(r'\w+@\w+\.\w+', text)
print(matches)
# ['[email protected]', '[email protected]']
\w+@\w+\.\w+ が最初の正規表現パターンです。ループを一切使わずに、2つのメールアドレスを見つけ出しました。これが正規表現の醍醐味です。
徹底解説:正規表現の構成要素
正規表現は、少数のコアシンボルで構成されています。この約15個の構文をマスターすれば、実務で遭遇するほぼあらゆるパターンを書けるようになります。
文字クラス
\w— 単語文字(英字・数字・アンダースコア)\d— 任意の数字(0〜9)\s— 任意の空白文字(スペース・タブ・改行)\W、\D、\S— それぞれの否定(大文字 = 否定)[abc]— a、b、c のいずれか1文字[^abc]— a、b、c 以外の任意の文字[a-z]— 任意の小文字英字
数量詞
+— 1回以上*— 0回以上?— 0回または1回(省略可能){3}— ちょうど3回{2,5}— 2回以上5回以下
アンカーと境界
^— 文字列の先頭$— 文字列の末尾\b— 単語の境界
Pythonでの組み合わせ例:
import re
# YYYY-MM-DD形式の日付にマッチ
pattern = r'\d{4}-\d{2}-\d{2}'
text = "Deployment date: 2025-03-15, rollback date: 2025-03-16"
dates = re.findall(pattern, text)
print(dates)
# ['2025-03-15', '2025-03-16']
# 単独の単語「error」にのみマッチ(「errors」や「error_code」は除外)
pattern = r'\berror\b'
log = "found 3 errors but only one error was critical"
print(re.findall(pattern, log))
# ['error']
グループとキャプチャ
括弧 () はキャプチャグループを作ります。マッチ全体ではなく、特定の部分だけを取り出すことができます。
import re
log_line = "2025-03-15 14:23:01 ERROR Database connection failed"
pattern = r'(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) (\w+) (.+)'
match = re.match(pattern, log_line)
if match:
date, time, level, message = match.groups()
print(f"日付: {date}, レベル: {level}")
print(f"メッセージ: {message}")
# 日付: 2025-03-15, レベル: ERROR
# メッセージ: Database connection failed
応用テクニック
非キャプチャグループと選択
結果を汚染せずにグループ化だけしたい場合があります。そのための構文が (?:...) です。キャプチャせずにグループ化できます。
import re
# 「color」または「colour」にマッチ(米国式・英国式のスペル両対応)
pattern = r'colo(?:u)?r'
text = "The color and colour are both valid spellings"
print(re.findall(pattern, text))
# ['color', 'colour']
# | による選択
pattern = r'\b(cat|dog|bird)\b'
text = "I have a cat and a dog, but no bird"
print(re.findall(pattern, text))
# ['cat', 'dog', 'bird']
先読みと後読み
ゼロ幅アサーションは、正規表現の中でも特に強力なテクニックです。マッチで文字を消費することなく、特定の位置の前後を確認できます。
import re
# 先読み:「 USD」の直前にある金額にのみマッチ
pattern = r'\$\d+(?:\.\d{2})?(?= USD)'
text = "Total: $49.99 USD, Tax: $3.50 USD"
print(re.findall(pattern, text))
# ['$49.99', '$3.50']
# 後読み:「version 」の直後にあるバージョン番号にのみマッチ
pattern = r'(?<=version )\d+\.\d+'
text = "Running version 3.11, upgrading to version 3.12"
print(re.findall(pattern, text))
# ['3.11', '3.12']
Bashでの正規表現
正規表現はPython専用ではありません。シェルスクリプトでは、grep がデフォルトで基本的な正規表現を使用します。-E オプション(または egrep)を渡すと拡張正規表現が使え、+、?、| が利用可能になります。
# ログファイルからIPアドレスを含む行を抽出
grep -E '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b' /var/log/nginx/access.log
# HTTPステータスコード(200、404、500など)を集計
grep -oE ' [0-9]{3} ' access.log | sort | uniq -c
# 「GET」を含まない行を抽出
grep -v 'GET' access.log
# 大文字小文字を無視してエラーレベルを検索
grep -iE 'error|warning|critical' app.log
文字列置換への正規表現の活用
検索・置換はおそらく最も一般的な実用ユースケースです。3種類の異なるフォーマットで入力される電話番号を正規化する実践例を見てみましょう。
import re
# すべての電話番号をXXX-XXX-XXXX形式に統一
def normalize_phone(text):
return re.sub(
r'\(?([0-9]{3})\)?[.\s-]?([0-9]{3})[.\s-]?([0-9]{4})',
r'\1-\2-\3',
text
)
print(normalize_phone("Call (123) 456-7890 or 123.456.7890 or 123-456-7890"))
# Call 123-456-7890 or 123-456-7890 or 123-456-7890
より良い正規表現を書くための実践的なヒント
ヒント1:Pythonでは生文字列を使う
パターンには必ず r プレフィックスをつけましょう。つけないと、正規表現が評価される前にPythonがバックスラッシュを解釈してしまい、\n が改行に、\b がバックスペースになってしまいます。生文字列を使えばそれを防げます。
# 間違い — \b がバックスペース(ASCII 8)として解釈される
pattern = "\bword\b"
# 正解 — 生文字列でバックスラッシュをそのまま保持
pattern = r'\bword\b'
ヒント2:欲張りにならず、具体的に指定する
デフォルトでは .* は欲張りマッチ(グリーディ)で、できるだけ多くのテキストにマッチしようとします。? を追加すると最短一致(レイジー)モードに切り替わり、最も早いマッチ位置で止まります。
import re
html = "<b>bold</b> and <b>more bold</b>"
# グリーディ — 最初の<b>から最後の</b>まで全部マッチ
print(re.findall(r'<b>.*</b>', html))
# ['<b>bold</b> and <b>more bold</b>']
# レイジー — 最初の閉じタグで停止
print(re.findall(r'<b>.*?</b>', html))
# ['<b>bold</b>', '<b>more bold</b>']
ヒント3:繰り返し使うパターンはコンパイルする
同じパターンをタイトなループで繰り返し使う場合は、事前にコンパイルしておきましょう。10万回のイテレーションのベンチマークでは、コンパイル済みパターンは毎回 re.findall() に生文字列を渡す場合と比べて約2〜3倍高速です。
import re
# 一度だけコンパイル
email_pattern = re.compile(r'[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}')
# 複数回再利用
for line in open('emails.txt'):
matches = email_pattern.findall(line)
if matches:
print(matches)
ヒント4:パターンは対話的にテストする
コードに組み込む前に、regex101.com にパターンを貼り付けて確認しましょう。各グループがマッチした文字を正確にハイライト表示し、すべてのトークンをわかりやすい言葉で説明し、よくあるミスも指摘してくれます。PythonのREPLで20分かかるようなデバッグが、そこでは30秒で終わることも珍しくありません。
ヒント5:正規表現を使うべきでない場面を知る
深く構造化されたフォーマットには正規表現は向いていません。HTMLの解析にはBeautifulSoupやlxmlを、JSONの検証には jsonschema のようなスキーマバリデーターを使いましょう。目安として、正規表現が2レベル以上のネストや状態管理を必要とする場合は、適切なパーサーを使うべきサインです。
実践的なミニプロジェクト
nginxのアクセスログを解析してHTTPステータスコードごとにリクエスト数を集計するスクリプトです。実務でも実際に書くような内容です:
import re
from collections import Counter
log_pattern = re.compile(
r'(?P<ip>[\d.]+) .+ \[(?P<time>[^\]]+)\] '
r'"(?P<method>\w+) (?P<path>[^ ]+) HTTP/[\d.]+" '
r'(?P<status>\d{3}) (?P<size>\d+)'
)
status_counts = Counter()
with open('/var/log/nginx/access.log') as f:
for line in f:
match = log_pattern.match(line)
if match:
status_counts[match.group('status')] += 1
for status, count in sorted(status_counts.items()):
print(f"HTTP {status}: {count} リクエスト")
名前付きグループ((?P<name>...))を使うとコードが自己文書化されます。match.group('status') と書けば、何番目のグループかを数えなくても、何が入っているか一目でわかります。
最初の数時間は正規表現が暗号のように見えるかもしれません。でも、あるとき突然コツがつかめます。気づけば、これまで手動でループ処理していたデータの中にパターンが見え始め、コードが半分に縮んでいきます。

