正規表現のグループ化には (...) を使うが、番号で参照するだけだとパターンが複雑になるにつれて意味がわかりにくくなる。Python では (?P<name>...) という構文で名前付きグループを定義でき、キャプチャした内容に名前でアクセスできる。コードの可読性を大幅に向上させる仕組みだ。
基本的な名前付きグループ
(?P<name>pattern) でグループに名前を付ける。マッチオブジェクトからは group('name') で値を取得できる。
import re
text = "2025-02-08"
m = re.match(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", text)
if m:
print(m.group("year")) # 2025
print(m.group("month")) # 02
print(m.group("day")) # 08
\1 や \2 のように番号で参照するより、year、month、day という名前のほうが意図は明快だろう。特に 3 つ以上のグループがあるパターンでは、番号の対応を追うだけで疲弊する。名前付きグループはその問題を根本的に解決してくれる。
groupdict で辞書として取得する
名前付きグループの便利な点として、groupdict() メソッドでキャプチャ結果を辞書として取得できることが挙げられる。
import re
text = "Alice: age=30, city=Tokyo"
pattern = r"(?P<name>\w+): age=(?P<age>\d+), city=(?P<city>\w+)"
m = re.match(pattern, text)
if m:
d = m.groupdict()
print(d)
# {'name': 'Alice', 'age': '30', 'city': 'Tokyo'}
返される辞書はそのまま関数の引数やデータベースへの投入に使えるため、テキストパーシングの効率が格段に上がる。番号グループの場合は groups() でタプルが返るだけなので、後続処理との連携で差が出る場面は多い。
re.sub での名前付き参照
re.sub の置換文字列内では \g<name> の構文で名前付きグループを参照できる。
import re
text = "Tanaka Taro"
result = re.sub(
r"(?P<family>\w+) (?P<given>\w+)",
r"\g<given> \g<family>",
text
)
print(result) # Taro Tanaka
\1、\2 を使うより、何を入れ替えているのかが一目瞭然になる。チームで共有するコードであればなおさら、名前付き参照を使ったほうがレビュー時の認知負荷が下がるだろう。
バックリファレンスとは
バックリファレンスは、パターン内で先にキャプチャした内容を後から参照する機能だ。同じ文字列の繰り返しを検出するときに使う。番号グループなら \1、名前付きグループなら (?P=name) で参照する。
import re
text = "aabbcc the the end"
m = re.search(r"\b(?P<word>\w+)\s+(?P=word)\b", text)
if m:
print(m.group()) # the the
(?P<word>\w+) で最初にキャプチャした単語と、(?P=word) で同じ文字列が続く箇所を検出している。タイポや重複単語のチェックに有効なパターンだ。
番号バックリファレンスとの比較
名前を使わずに番号で同じことをする場合と比較してみよう。
(\w+)\s+\1 のようにグループ番号で参照する。短いパターンでは十分だが、グループが増えると番号の対応を追いにくい。
(?P<word>\w+)\s+(?P=word) のように名前で参照する。パターンが長くなっても意味が明確で、保守性が高い。
短い使い捨てのパターンなら番号でも問題ないが、再利用や共有を前提とするなら名前付きを選んだほうが無難だ。
finditer と名前付きグループの組み合わせ
re.finditer と組み合わせると、テキストから構造化されたデータを繰り返し抽出できる。
import re
log = """
[2025-02-08 10:00] INFO: server started
[2025-02-08 10:05] ERROR: connection refused
[2025-02-08 10:10] INFO: retry successful
"""
pattern = r"\[(?P<date>[\d-]+) (?P<time>[\d:]+)\] (?P<level>\w+): (?P<msg>.+)"
for m in re.finditer(pattern, log):
d = m.groupdict()
if d["level"] == "ERROR":
print(f"{d['date']} {d['time']} - {d['msg']}")
# 2025-02-08 10:05 - connection refused
各マッチを辞書化し、level で絞り込んでから出力している。ログ解析やデータ抽出のスクリプトでは、このパターンが定番の書き方になる。
名前の衝突に注意する
同一パターン内で同じ名前のグループを複数定義するとエラーになる。これは番号グループにはない名前付き固有の制約だ。
import re
# これはエラーになる
try:
re.compile(r"(?P<x>\d+)-(?P<x>\w+)")
except re.error as e:
print(e) # redefinition of group name 'x'
別の情報をキャプチャするなら、それぞれ異なる名前を付ける必要がある。命名の手間は増えるが、それ自体がパターンの設計を見直すきっかけにもなるので、むしろ良い制約と捉えてよいだろう。
名前付きグループとバックリファレンスは、正規表現を「書く」だけでなく「読む」コストも下げてくれる。特にチーム開発や長期運用のプロジェクトでは積極的に採用したいテクニックだ。