Pythonで文字列のパターンマッチングや抽出を行う際、標準ライブラリであるreモジュールが非常に強力なツールとなります。本稿では、reモジュールの基本的な使い方から、フラグを活用した高度なマッチングまでを解説します。
基本的なテキスト抽出
以下のようなログデータから、特定のフォーマットを持つエラーコード(例: ERR-XXXXX)を抽出したい場合を考えます。
2023-10-01 10:00:00 INFO System started
2023-10-01 10:05:23 ERROR ERR-48291 Database connection failed
2023-10-01 10:10:45 WARN Disk space low
2023-10-01 10:15:12 ERROR ERR-93012 Timeout occurred
標準的な文字列操作でも実現可能ですが、reモジュールを使えばより簡潔に記述できます。
import re
log_data = """2023-10-01 10:00:00 INFO System started
2023-10-01 10:05:23 ERROR ERR-48291 Database connection failed
2023-10-01 10:10:45 WARN Disk space low
2023-10-01 10:15:12 ERROR ERR-93012 Timeout occurred"""
# 従来の文字列操作による抽出
error_codes_manual = []
for line in log_data.splitlines():
parts = line.split()
if len(parts) >= 4 and parts[2] == "ERROR":
error_codes_manual.append(parts[3])
print(error_codes_manual) # ['ERR-48291', 'ERR-93012']
# reモジュールを使用した抽出
error_codes_regex = re.findall(r"ERR-\d{5}", log_data)
print(error_codes_regex) # ['ERR-48291', 'ERR-93012']
マッチングの基本とグループ化
正規表現の強力な機能の一つが「グループ化」です。丸括弧 () を使用してパターンをグループ化し、特定の部分を抽出できます。
import re
# 任意の1文字にマッチ
match_any = re.search(r".", "hello")
print(match_any.group()) # 'h'
# グループ化と交互マッチング
text = "version_2.1_release"
pattern = r"version_(\d)\.(\d)_(alpha|beta|release)"
match_ver = re.search(pattern, text)
if match_ver:
print(match_ver.group()) # 'version_2.1_release'
print(match_ver.group(1)) # '2'
print(match_ver.group(3)) # 'release'
# マッチしない場合の処理
no_match = re.search(r"version_3", text)
print(no_match) # None
名前付きグループによるデータ抽出
複雑な文字列から複数の要素を抽出する際、名前付きグループ (?P<name>pattern) を使用すると、コードの可読性が大幅に向上します。ここではISO 8601形式のタイムスタンプをパースする例を見てみましょう。
import re
timestamp = "2023-11-15T08:30:45Z"
# 名前付きグループを使用したパターン
ts_pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})T(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})Z"
match_ts = re.search(ts_pattern, timestamp)
if match_ts:
# 辞書形式でグループを取得
time_dict = match_ts.groupdict()
print(time_dict)
# {'year': '2023', 'month': '11', 'day': '15', 'hour': '08', 'minute': '30', 'second': '45'}
# 特定のグループへのアクセス
print(f"Year: {match_ts.group('year')}, Month: {match_ts.group('month')}")
文字列の分割、置換、および完全一致
reモジュールは、検索だけでなく文字列の操作にも有用な関数を提供しています。
import re
# \d を使用した数字のマッチング
print(re.search(r"\d+", "Item42").group()) # '42'
# re.split() による分割
csv_line = "apple,banana,,cherry"
# 連続するカンマや単一のカンマで分割
print(re.split(r",+", csv_line)) # ['apple', 'banana', 'cherry']
# re.sub() による置換
# 連続する空白を単一のスペースに置換
messy_text = "Python is awesome"
clean_text = re.sub(r"\s+", " ", messy_text)
print(clean_text) # 'Python is awesome'
# re.fullmatch() による完全一致(文字列全体がパターンに一致する場合のみ)
print(re.fullmatch(r"[a-z]+", "hello")) # Match object
print(re.fullmatch(r"[a-z]+", "hello123")) # None
# re.match() は文字列の先頭からのマッチのみを検証
print(re.match(r"\d+", "123abc")) # Match object ('123')
print(re.match(r"\d+", "abc123")) # None
パターンのコンパイルと再利用
同じ正規表現パターンを繰り返し使用する場合、re.compile() でパターンをコンパイルしておくことで、パフォーマンスの向上とコードの整理が図れます。
import re
# パターンのコンパイル
email_pattern = re.compile(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+")
# コンパイルしたパターンオブジェクトのメソッドを直接呼び出し
text = "Contact us at support@example.com or info@test.org"
emails = email_pattern.findall(text)
print(emails) # ['support@example.com', 'info@test.org']
# search や match も利用可能
first_email = email_pattern.search(text)
print(first_email.group()) # 'support@example.com'
フラグ(Flags)による動作の拡張
フラグを指定することで、正規表現のデフォルトの動作を変更できます。フラグは各関数の引数として渡すか、コンパイル時に指定します。
import re
# re.IGNORECASE (re.I): 大文字・小文字を区別しない
print(re.search(r"python", "Learn PYTHON", re.I)) # Match object
# re.MULTILINE (re.M): 複数行モード。^ と $ が各行の先頭と末尾にマッチ
multiline_text = """first line
second line
third line"""
# 通常の ^ は文字列の先頭にのみマッチ
print(re.findall(r"^second", multiline_text)) # []
# re.M を指定すると各行の先頭にマッチ
print(re.findall(r"^second", multiline_text, re.M)) # ['second']
# re.DOTALL (re.S): ドット (.) が改行文字 (\n) にもマッチするようになる
html_snippet = "<div>\n Content\n</div>"
print(re.search(r"<div>.*</div>", html_snippet)) # None
print(re.search(r"<div>.*</div>", html_snippet, re.S)) # Match object
# re.VERBOSE (re.X): 正規表現内にコメントや空白を挿入し、可読性を高める
# r"" (raw string) を使用してエスケープ文字の解釈を抑制
ip_pattern = re.compile(r"""
\b # ワード境界
(?:\d{1,3}\.) # 最初の3オクテットとドット (非キャプチャグループ)
{3} # 上記の繰り返し
\d{1,3} # 最後のオクテット
\b # ワード境界
""", re.X)
test_ip = "Server IP is 192.168.1.100."
print(ip_pattern.search(test_ip).group()) # '192.168.1.100'