Pythonの正規表現で日々のテキスト処理を効率化する実践テクニック

本記事では、Pythonのreモジュールを使った正規表現の実践的な活用方法を紹介します。メールアドレスの抽出、ログファイルの解析、データクレンジングなど、実務でよく使うパターンをすぐに仕事で活用できるコード例で解説します。

Pythonの正規表現とは?基礎知識から始める

Pythonの正規表現は、reモジュールを使ってテキストパターンマッチングと置換を行う強力な機能です。データ分析、ログ解析、入力値の検証など、開発現場で毎日のように活用されます。

正規表現は強力な反面、複雑なパターンになると可読性が落ちやすいため、実務ではシンプルなパターンから段階的に複雑化させるアプローチが重要です。

よく使う基本パターン

パターン 説明 マッチ例
\d 数字1文字 0-9
\w 単語文字(英数字とアンダースコア) a-z, A-Z, 0-9, _
\s 空白文字 スペース、タブ、改行
+ 1回以上の繰り返し a+は「a」「aa」「aaa」に対応
* 0回以上の繰り返し a*は「」「a」「aa」に対応

実践例1:メールアドレスの抽出と検証

テキストファイルやログから有効なメールアドレスを抽出する場面は頻繁にあります。以下のコード例では、re.findall()を使ってメールアドレスを一括抽出し、さらにre.match()で個別の妥当性をチェックします。

import re

# テキストからメールアドレスをすべて抽出
text = """
連絡先:user1@example.com、user2@test.co.jp
管理者:admin@company.org
"""

# 基本的なメールパターン(簡易版)
email_pattern = r'[\w\.-]+@[\w\.-]+\.\w+'
emails = re.findall(email_pattern, text)

print("抽出されたメール:")
for email in emails:
    print(f"  {email}")

# より厳密な検証(RFC 5322準拠ではなく実用的なレベル)
strict_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

for email in emails:
    if re.match(strict_pattern, email):
        print(f"✓ 有効: {email}")
    else:
        print(f"✗ 無効: {email}")

メール抽出でよくあるハマりポイント

問題1: 日本語ドメイン対応 — 国際化ドメイン名(IDN)を含むメールアドレスをマッチさせたい場合、単純な\wでは対応できません。その場合は[\w\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\u4e00-\u9fff]のようにUnicodeレンジを追加します。

問題2: 長すぎるメールの除外 — メールアドレスの長さに上限がある場合、^.{1,254}$のような長さチェックを組み合わせます。

# 日本語対応とメール長チェックの例
import re

email_text = "contact@日本企業.jp invalid_very_long_email_address_that_exceeds_limit@test.co.jp"

# 国際化ドメイン対応
idna_pattern = r'[\w\.\-\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\u4e00-\u9fff]+@[\w\.\-\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\u4e00-\u9fff]+'
emails = re.findall(idna_pattern, email_text)

# メール長チェック(1〜254文字)
for email in emails:
    if 1 <= len(email) <= 254:
        print(f"OK: {email}")
    else:
        print(f"NG(長さ超過): {email}")

実践例2:ログファイル解析とデータ抽出

アクセスログやエラーログから特定の情報を抽出する場面では、複数のパターンを組み合わせた正規表現が活躍します。以下の例では、Apacheアクセスログから日時、ステータスコード、レスポンスサイズを抽出します。

import re
from datetime import datetime

# Apacheアクセスログのサンプル
log_lines = [
    '192.168.1.1 - - [01/Jan/2025:10:15:30 +0900] "GET /api/users HTTP/1.1" 200 1234',
    '192.168.1.2 - - [01/Jan/2025:10:16:45 +0900] "POST /api/data HTTP/1.1" 201 567',
    '192.168.1.3 - - [01/Jan/2025:10:17:22 +0900] "GET /404 HTTP/1.1" 404 0',
]

# ログパターン:IP、日時、メソッド、パス、ステータス、サイズ
log_pattern = r'(\d+\.\d+\.\d+\.\d+).*?\[(.+?)\].*?"(\w+)\s(.+?)\s.*?"\s(\d+)\s(\d+)'

for line in log_lines:
    match = re.search(log_pattern, line)
    if match:
        ip, timestamp, method, path, status, size = match.groups()
        print(f"IP: {ip}, メソッド: {method}, パス: {path}, ステータス: {status}, サイズ: {size}B")

# エラーのみをフィルタリング
print("\n--- エラーログのみ ---")
for line in log_lines:
    match = re.search(log_pattern, line)
    if match:
        status = match.group(5)
        if status.startswith('4') or status.startswith('5'):
            print(f"エラー検出: {line}")

ログ解析で効率を上げるコツ

re.compile()の活用 — 同じパターンで複数行をマッチさせる場合、re.compile()で事前にパターンをコンパイルすると処理速度が向上します。大規模ログファイル(数100万行)では10-20%の高速化が期待できます。

import re
import time

# パターンを事前コンパイル
log_pattern = re.compile(r'(\d+\.\d+\.\d+\.\d+).*?\[(.+?)\].*?"(\w+)\s(.+?)\s.*?"\s(\d+)\s(\d+)')

log_lines = ['192.168.1.1 - - [01/Jan/2025:10:15:30 +0900] "GET /api/users HTTP/1.1" 200 1234'] * 100000

# コンパイル版(高速)
start = time.time()
for line in log_lines:
    match = log_pattern.search(line)
elapsed_compiled = time.time() - start

print(f"コンパイル版: {elapsed_compiled:.3f}秒")

実践例3:データクレンジングと形式統一

CSVデータやフォーム入力には不要な空白や特殊文字が含まれることが多いです。re.sub()を使った置換で、データを統一フォーマットに整形します。

import re

# 複数の電話番号形式をすべて統一フォーマットに変換
phone_numbers = [
    "090-1234-5678",
    "09012345678",
    "090 1234 5678",
    "+81-90-1234-5678",
    "(090)1234-5678",
]

# ステップ1:非数字文字をすべて除去
# ステップ2:先頭の+81を0に置換
# ステップ3:0から始まる10桁に統一
def normalize_phone(phone):
    # 非数字と括弧を除去
    digits_only = re.sub(r'[^\d+]', '', phone)
    # 国番号を日本の0に変換
    digits_only = re.sub(r'^\+81', '0', digits_only)
    # 10桁か11桁かチェック
    if re.match(r'^0\d{9}$', digits_only):
        return digits_only
    elif re.match(r'^0\d{10}$', digits_only):
        return digits_only
    else:
        return None

print("電話番号正規化結果:")
for phone in phone_numbers:
    normalized = normalize_phone(phone)
    print(f"{phone:20} → {normalized if normalized else '無効'}")

re.sub()で複数パターンを同時処理

複数の置換ルールがある場合、re.sub()を連鎖させるより、関数を使った動的な置換が効率的です。

import re

text = "価格: ¥1,234,567 / 割引: ¥12,345"

# パターン: 金額(数字+カンマ)をマッチ
def remove_currency_and_comma(match):
    value = match.group(1)  # マッチした値(例:¥1,234,567)
    digits = re.sub(r'[^\d]', '', value)  # 数字のみ抽出
    return digits

# 複雑な変換ルール
result = re.sub(r'(¥[\d,]+)', remove_currency_and_comma, text)
print(f"変換前: {text}")
print(f"変換後: {result}")

# さらに別の規則を適用(複数ステップ)
text2 = "Contact: user@example.com, phone: 090-1234-5678"
# メールと電話番号をマスク
text2 = re.sub(r'[\w\.-]+@[\w\.-]+', 'xxx@xxx.xxx', text2)
text2 = re.sub(r'\d{3}-\d{4}-\d{4}', 'xxx-xxxx-xxxx', text2)
print(f"マスク後: {text2}")

正規表現の使うべき場面と使うべきでない場面

✅ 正規表現を使うべき場面

  • テキストファイルやログから特定パターンのデータを抽出する
  • ユーザー入力の形式検証(メール、電話番号、郵便番号など)
  • 複数の異なる形式を統一フォーマットに変換する
  • テキストの置換・削除で複雑な条件が必要な場合
  • HTMLやJSONをシンプルなパターンマッチで処理する場合

❌ 正規表現を使うべきでない場面

  • ネストされたHTML/XMLの解析 → BeautifulSoupxml.etreeを使用
  • 複雑なJSON処理 → jsonモジュールを使用
  • データベースクエリの構築 → ORM(SQLAlchemy)を使用
  • HTMLからのテキスト抽出 → HTMLパーサーライブラリを使用

デバッグのコツ:正規表現を可視化する

複雑な正規表現のパターンマッチングをデバッグする際は、re.finditer()で各マ

K
AWS・Python・生成AIを専門とするソフトウェアエンジニア。AI・クラウド・開発ワークフローの実践ガイドを執筆しています。詳しく見る →