Pythonの正規表現(regex)を使った業務自動化パターン集
\n\n
Pythonで業務自動化を行う際、正規表現(regex)は避けて通れないスキルです。ログファイルの解析、CSVデータのクリーニング、ユーザー入力のバリデーションなど、実務で毎日のように登場します。本記事では、教科書的な例ではなく、実際のプロジェクトで使えるパターンを中心に解説します。
\n\n
正規表現とは〜業務での重要性
\n\n
正規表現は、文字列パターンマッチングの強力なツールです。Pythonのreモジュールを使うことで、複雑な文字列処理を数行で実装できます。
\n\n
業務で正規表現が活躍する場面:
\n
- \n
- ログファイルからエラー情報を抽出
- メールアドレスやURLのバリデーション
- 構造化されていないテキストからデータを抽出
- ファイル名やパスの一括処理
- HTMLやXMLのスクレイピング
\n
\n
\n
\n
\n
\n\n
基本的な使い方を30秒で理解する
\n\n
Pythonで正規表現を使うにはreモジュールをインポートします。主な関数は以下の4つです。
\n\n
import re\n\n# パターンをコンパイル(繰り返し使う場合は推奨)\npattern = re.compile(r'\\d{4}-\\d{2}-\\d{2}')\n\n# マッチしたかどうかを確認\nif pattern.search('2024-01-15'):\n print('マッチしました')\n\n# マッチした全文字列を取得\nmatches = pattern.findall('日付: 2024-01-15, 2024-01-16')\nprint(matches) # ['2024-01-15', '2024-01-16']\n\n# 置換\nresult = pattern.sub('****-**-**', '誕生日は2024-01-15です')\nprint(result) # 誕生日は****-**-**です\n
\n\n
実務ユースケース1:ログファイルからエラーを自動抽出
\n\n
大規模なシステムではログファイルが数GB単位で生成されることもあります。その中からエラー情報だけを抽出して、レポート生成する場面は頻繁です。
\n\n
シナリオ:アプリケーションログから、エラーレベルのログと発生時刻、エラーメッセージを抽出したい
\n\n
import re\nfrom datetime import datetime\n\ndef extract_errors_from_log(log_file_path):\n \"\"\"\n ログファイルからエラー情報を抽出\n ログフォーマット例: [2024-01-15 14:30:45] ERROR [module.py:123] Connection timeout\n \"\"\"\n \n # パターンの定義:タイムスタンプ、ログレベル、ファイル情報、メッセージ\n error_pattern = re.compile(\n r'\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\] (ERROR|CRITICAL) \\[([^\\]]+)\\] (.+)'\n )\n \n errors = []\n \n try:\n with open(log_file_path, 'r', encoding='utf-8') as f:\n for line_num, line in enumerate(f, 1):\n match = error_pattern.search(line)\n if match:\n timestamp, level, location, message = match.groups()\n errors.append({\n 'line': line_num,\n 'timestamp': timestamp,\n 'level': level,\n 'location': location,\n 'message': message.strip()\n })\n except FileNotFoundError:\n print(f'ファイルが見つかりません: {log_file_path}')\n return []\n \n return errors\n\n# 使用例\nif __name__ == '__main__':\n errors = extract_errors_from_log('application.log')\n \n # エラー数をサマリー\n print(f'\\n発見されたエラー数: {len(errors)}')\n print('-' * 80)\n \n for error in errors[:10]: # 最初の10件表示\n print(f\"[{error['timestamp']}] {error['level']} at {error['location']}\")\n print(f\" メッセージ: {error['message']}\")\n print()\n
\n\n
実務ユースケース2:CSV形式のデータをクリーニング
\n\n
外部システムから受け取ったCSVデータは、フォーマットが統一されていないことが多いです。例えば、電話番号の記号が混在していたり、余分なスペースが入っていたりします。正規表現を使ってデータをクリーニングします。
\n\n
import re\nimport csv\nfrom typing import List, Dict\n\ndef clean_phone_number(phone: str) -> str:\n \"\"\"\n 電話番号をクリーニング\n 入力例: \"090-1234-5678\", \"(090)1234-5678\", \"09012345678\"\n 出力例: \"09012345678\"\n \"\"\"\n # 数字以外を全て削除\n digits_only = re.sub(r'\\D', '', phone)\n \n # 日本の電話番号か確認(10〜11桁)\n if re.match(r'^0\\d{9,10}$', digits_only):\n return digits_only\n return '' # 無効な番号\n\ndef validate_email(email: str) -> bool:\n \"\"\"\n メールアドレスの基本的なバリデーション\n 完全な検証はより複雑ですが、実務では十分なケースが多いです\n \"\"\"\n pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n return bool(re.match(pattern, email))\n\ndef extract_domain_from_email(email: str) -> str:\n \"\"\"\n メールアドレスからドメインを抽出\n \"\"\"\n match = re.search(r'@([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})', email)\n return match.group(1) if match else ''\n\ndef clean_csv_data(input_file: str, output_file: str):\n \"\"\"\n CSVデータをクリーニング\n \"\"\"\n cleaned_rows = []\n \n with open(input_file, 'r', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n \n for row in reader:\n cleaned_row = {}\n \n # 名前:前後のスペースを削除\n cleaned_row['name'] = re.sub(r'^\\s+|\\s+$', '', row.get('name', ''))\n \n # 電話番号:クリーニング\n cleaned_row['phone'] = clean_phone_number(row.get('phone', ''))\n \n # メールアドレス:バリデーション\n email = row.get('email', '').lower().strip()\n if validate_email(email):\n cleaned_row['email'] = email\n cleaned_row['domain'] = extract_domain_from_email(email)\n else:\n cleaned_row['email'] = ''\n cleaned_row['domain'] = ''\n \n # 郵便番号:ハイフンなしの7桁に統一\n postal = re.sub(r'\\D', '', row.get('postal_code', ''))\n if re.match(r'^\\d{7}$', postal):\n cleaned_row['postal_code'] = postal\n else:\n cleaned_row['postal_code'] = ''\n \n cleaned_rows.append(cleaned_row)\n \n # クリーニング済みデータを出力\n if cleaned_rows:\n with open(output_file, 'w', encoding='utf-8', newline='') as f:\n fieldnames = cleaned_rows[0].keys()\n writer = csv.DictWriter(f, fieldnames=fieldnames)\n writer.writeheader()\n writer.writerows(cleaned_rows)\n \n print(f'{len(cleaned_rows)}件のデータをクリーニングして {output_file} に保存しました')\n\n# 使用例\nif __name__ == '__main__':\n # テスト\n print('電話番号クリーニング:')\n print(f' 090-1234-5678 → {clean_phone_number(\"090-1234-5678\")}')\n print(f' (090)1234-5678 → {clean_phone_number(\"(090)1234-5678\")}')\n print()\n \n print('メールバリデーション:')\n print(f' user@example.com: {validate_email(\"user@example.com\")}')\n print(f' invalid.email: {validate_email(\"invalid.email\")}')\n
\n\n
実務ユースケース3:複雑なテキストからの情報抽出
\n\n
例えば、営業がメールで送ってきた受注情報が非構造化のテキスト形式である場合、それを自動でパースする必要があります。このような場面での正規表現活用は日常的です。
\n\n
import re\nfrom typing import Dict, Optional\n\ndef parse_order_email(email_text: str) -> Optional[Dict]:\n \"\"\"\n 営業メールから受注情報をパース\n \n 入力例:\n 顧客名: 株式会社ABC\n 注文番号: ORD-2024-001234\n 商品: A製品 × 100個, B製品 × 50個\n 金額: ¥1,234,560\n 納期: 2024-02-15\n \"\"\"\n \n order = {}\n \n # 顧客名を抽出\n customer_match = re.search(r'顧客名:\\s*(.+?)(?=\\n|$)', email_text)\n if customer_match:\n order['customer'] = customer_match.group(1).strip()\n \n # 注文番号を抽出(ORD-YYYY-XXXXXX形式)\n order_match = re.search(r'(ORD-\\d{4}-\\d{6})', email_text)\n if order_match:\n order['order_id'] = order_match.group(1)\n \n # 商品と数量を抽出(複数の場合)\n items = []\n item_pattern = re.compile(r'([ぁ-ん製品a-zA-Z0-9]+)\\s*×\\s*(\\d+)(?:個|個)?')\n for match in item_pattern.finditer(email_text):\n items.append({\n 'product': match.group(1),\n 'quantity': int(match.group(2))\n })\n order['items'] = items\n \n # 金額を抽出(¥記号または\"円\"の形式)\n amount_match = re.search(r'[¥¥]([\\d,]+)|([\\d,]+)\\s*円', email_text)\n if amount_match:\n amount_str = amount_match.group(1) or amount_match.group(2)\n order['amount'] = int(amount_str.replace(',', ''))\n \n # 納期を抽出(YYYY-MM-DD形式)\n delivery_match = re.search(r'納期:\\s*(\\d{4}-\\d{2}-\\d{2})', email_text)\n if delivery_match:\n order['delivery_date'] = delivery_match.group(1)\n \n return order if order else None\n\n# 使用例\nif __name__ == '__main__':\n email_sample = \"\"\"\n顧客名: 株式会社ABC\n注文番号: ORD-2024-001234\n商品: A製品 × 100個, B製品 × 50個\n金額: ¥1,234,560\n納期: 2024-02-15\nお疲れ様です。以下の通り発注します。\n\"\"\"\n \n result = parse_order_email(email_sample)\n if result:\n print('抽出された受注情報:')\n print(f' 顧客: {result.get(\"customer\")}')\n print(f' 注文番号: {result.get(\"order_id\")}')\n print(f' 商品:')\n for item in result.get('items', []):\n print(f' - {item[\"product\"]}: {item[\"quantity\"]}個')\n print(f' 金額: ¥{result.get(\"amount\"):,}')\n print(f' 納期: {result.get(\"delivery_date\")}')\n
\n\n
実務ユースケース4:ファイルパスの一括処理
\n\n
ファイルシステムから大量のファイルを処理する際、ファイル名のパターンマッチングで処理を分岐させることがよくあります。
\n\n
import re\nimport os\nfrom pathlib import Path\nfrom typing import List, Dict\n\ndef classify_backup_files(directory: str) -> Dict[str, List[str]]:\n \"\"\"\n バックアップファイルを日付やタイプで分類\n ファイル名例: backup_2024_01_15_full.tar.gz, backup_2024_01_15_incremental.tar.gz\n \"\"\"\n \n # ファイル名パターン: backup_YYYY_MM_DD_TYPE.tar.gz\n backup_pattern = re.compile(\n r'backup_(\\d{4})_(\\d{2})_(\\d{2})_(full|incremental|differential)\\.tar\\.gz'\n )\n \n classified = {\n 'full': [],\n 'incremental': [],\n 'differential': [],\n 'invalid': []\n }\n \n try:\n for filename in os.listdir(directory):\n filepath = os.path.join(directory, filename)\n \n # ディレクトリはスキップ\n if os.path.isdir(filepath):\n continue\n \n match = backup_pattern.match(filename)\n if match:\n year, month, day, backup_type = match.groups()\n classified[backup_type].append({\n 'filename': filename,\n 'date': f'{year}-{month}-{day}',\n 'path': filepath,\n 'size': os.path.getsize(filepath)\n })\n else:\n classified['invalid'].append(filename)\n \n except OSError as e:\n print(f'ディレクトリアクセスエラー: {e}')\n \n return classified\n\ndef rename_files_by_pattern(directory: str, pattern: str, replacement: str) -> int:\n \"\"\"\n ファイル名を正規表現で一括置換\n 例: 'report_2024_01_15.xlsx' → 'report_20240115.xlsx'\n \"\"\"\n \n regex = re.compile(pattern)\n renamed_count = 0\n \n for filename in os.listdir(directory):\n filepath = os.path.join(directory, filename)\n \n if os.path.isfile(filepath):\n new_filename = regex.sub(replacement, filename)\n \n if new_filename != filename:\n new_filepath = os.path.join(directory, new_filename)\n try:\n os.rename(filepath, new_filepath)\n print(f'リネーム: {filename} → {new_filename}')\n renamed_count += 1\n except OSError as e:\n print(f'リネーム失敗 {filename}: {e}')\n \n return renamed_count\n\n# 使用例\nif __name__ == '__main__':\n # ファイル分類の例(実際のディレクトリに対して実行)\n # classified = classify_backup_files('/path/to/backups')\n # print(f\"全バックアップ数: {len(classified['full']) + len(classified['incremental'])}\")\n \n # ファイル名置換の例\n # count = rename_files_by_pattern(\n # '/path/to/files',\n # r'report_(\\d{4})_(\\d{2})_(\\d{2})',\n # r'report_\\1\\2\\3'\n # )\n # print(f'リネーム完了: {count}件')\n pass\n
\n\n
よくある応用パターン
\n\n
パターン1:複数行テキストの処理(MULTILINE・DOTALL)
\n\n
ログやHTMLなど、複数行にまたがるテキストを処理する際は、フラグを使い分けることが重要です。
\n\n
import re\n\ntext = \"\"\"\nERROR: Connection failed\n at module.py line 123\n details: timeout after 30s\n\nWARNING: Retrying...\n\"\"\"\n\n# MULTILINE:^と$が行単位でマッチ\nerror_blocks = re.findall(\n r'^([A-Z]+): (.+?)$\\n\\s+(.+?)(?=\\n^[A-Z]|\\Z)',\n text,\n re.MULTILINE | re.DOTALL\n)\n\nfor level, message, details in error_blocks:\n print(f'{level}: {message}')\n print(f' {details.strip()}')\n
\n\n
パターン2:後方参照による重複チェック
\n\n
HTMLのタグマッチングやペアの検証など、後方参照が活躍する場面です。
\n\n
import re\n\ndef validate_matching_tags(html: str) -> bool:\n \"\"\"\n 対応するHTMLタグをチェック(シンプルな場合のみ)\n 実務ではBeautifulSoupなどの専門ライブラリを使用することを推奨\n \"\"\"\n # 開きタグと閉じタグが対応しているか確認\n pattern = r'<(\\w+)[^>]*>(.*?)\\1>'\n \n # すべてのペアマッチを検証\n matches = re.findall(pattern, html)\n return len(matches) > 0\n\n# テスト\nprint(validate_matching_tags('Hello
')) # True\nprint(validate_matching_tags('text')) # True\n
\n\n
パターン3:前後の文脈を含める(肯定先読み・肯定後読み)
\n\n
import re\n\ntext = 'price: $100, cost: $50, discount: $10'\n\n# 肯定先読み:$の後に続く数字を抽出($は含めない)\nprices = re.findall(r'(?<=\\$)\\d+', text)\nprint(prices) # ['100', '50', '10']\n\n# 肯定後読み:特定の単語の後の数字を抽出\nprices_with_context = re.findall(r'price: \\$(\\d+)', text)\nprint(prices_with_context) # ['100']\n\n# 否定先読み:\"test\"の後に続かない\"ing\"を抽出\ntext2 = 'running, testing, jumping, walking'\nmatches = re.findall(r'\\w+ing(?!)', text2) # これはすべてマッチ\nprint(matches)\n
\n\n
性能最適化のコツ
\n\n
大規模ファイル処理時の正規表現性能は重要です。
\n\n
import re\nimport time\n\n# NG例:毎回パターンをコンパイル(遅い)\ndef slow_process(lines):\n for line in lines:\n if re.search(r'\\d{4}-\\d{2}-\\d{2}', line): # 毎回コンパイル\n pass\n\n# OK例:パターンを事前にコンパイル(速い)\ndate_pattern = re.compile(r'\\d{4}-\\d{2}-\\d{2}')\n\ndef fast_process(lines):\n for line in lines:\n if date_pattern.search(line): # コンパイル済みを再利用\n pass\n\n# ベンチマーク\nlines = ['2024-01-15 error occurred'] * 10000\n\nstart = time.time()\nslow_process(lines)\nprint(f'遅い方法: {time.time() - start:.4f}秒')\n\nstart = time.time()\nfast_process(lines)\nprint(f'速い方法: {time.time() - start:.4f}秒')\n
\n\n
重要な注意点
\n\n
1. 正規表現は万能ではない
\n\n
複雑なパターン(HTMLのネストなど)には、正規表現より専門ライブラリが向いています。
\n\n
import re\nfrom bs4 import BeautifulSoup\n\n# NG:正規表現でHTMLパース(脆弱)\nhtml = 'Text
'\ntext_regex = re.search(r'(.+?)
', html)\n\n# OK:専門ライブラリを使用\nsoup = BeautifulSoup(html, 'html.parser')\ntext_bs = soup.find('p').text\n
\n\n
2. 特殊文字のエスケープを忘れずに
\n\n
ドット、アスタリスク、括弧などは、特殊文字として解釈されます。
\n\n
import re\n\n# NG:ドットは任意の1文字にマッチ\npattern_bad = r'192.168.1.1' # 192x168x1x1 にもマッチ\n\n# OK:ドットをエスケープ\npattern_good = r'192\\.168\\.1\\.1'\n\nip = '192.168.1.1'\nprint(re.match(pattern_bad, ip)) # マッチ\nprint(re.match(pattern_good, ip)) # マッチ\n\nip_bad = '192x168x1x1'\nprint(re.match(pattern_bad, ip_bad)) # マッチしてしまう!\nprint(re.match(pattern_good, ip_bad)) # None(正しい)\n
\n\n
3. グローバルマッチと最初のマッチの使い分け
\n\n
import re\n\ntext = 'Error at line 10, Error at line 20'\n\n# 最初のエラーだけ処理\nfirst_error = re.search(r'Error at line (\\d+)', text)\nif first_error:\n print(f'最初のエラー: {first_error.group(1)}') # 10\n\n# すべてのエラーを処理\nall_errors = re.findall(r'Error at line (\\d+)', text)\nprint(f'全エラー: {all_errors}') # ['10', '20']\n
\n\n
4. 長時間の実行(ReDoS攻撃への対策)
\n\n
不適切な正規表現はバックトラッキングで長時間かかることがあります。ユーザー入力を正規表現で処理する場合は特に注意。
\n\n
import re\nimport signal\n\ndef timeout_handler(signum, frame):\n raise TimeoutError('正規表現処理がタイムアウト')\n\n# NG:バックトラッキング爆発の可能性\n# pattern = r'(a+)+b' # \"aaaa...\" にマッチしない場合、極端に遅い\n\n# OK:より安全なパターン\npattern = r'a+b'\n\n# 長時間処理への対策\nsignal.signal(signal.SIGALRM, timeout_handler)\nsignal.alarm(5) # 5秒でタイムアウト\n\ntry:\n result = re.search(pattern, 'test string')\nexcept TimeoutError:\n print('タイムアウト')\nfinally:\n signal.alarm(0) # アラームをキャンセル\n
\n\n
実務で使えるテンプレート集
\n\n
よく使うパターンをまとめたテンプレートです。
\n\n
import re\nfrom typing import List, Optional\n\nclass RegexTemplates:\n \"\"\"業務で頻繁に使う正規表現パターン\"\"\

