正規表現の丸かっこ(...)はグループと呼ばれ、パターンの一部をひとまとめにしたり、マッチした部分を取り出したりするのに使います。本記事では、グループの中身を取り出す方法、取り出さずにまとめるだけの非キャプチャグループ、名前で扱う名前付きグループ、そして同じ内容の再出現を捉える後方参照を、Pythonのreモジュールで例とともに一つずつ整理します。
使うのは標準ライブラリのreモジュールだけで、追加インストールは不要です。\dや*、|といった基本的な記号は当サイトの別の記事「正規表現の基本構文とパターン検索(Python)」で扱っています。
本記事は、正規表現のグループに絞って解説します。
目次
- グループの一覧
- グループで囲んだ部分を取り出す
- 取り出さずにまとめる(非キャプチャグループ(?:...))
- 名前で扱う(名前付きグループ(?P<name>...))
- 同じ内容の再出現を捉える(後方参照)
- まとめ
グループの一覧
この記事で扱うグループ関連の構文を、先に一覧で示します。細かい意味や使いどころは、表のあとの各節で例とともに記述しています。
| 構文 | 意味 |
|---|---|
(…) | 囲んだ部分の文字列を取り出せるようにする(キャプチャグループ) |
(?:…) | 選択や繰り返しのためにまとめるだけで、取り出さない(非キャプチャグループ) |
(?P<name>…) | 名前を付けて、名前で取り出せる(名前付きキャプチャグループ) |
\1 / (?P=name) | 前のグループと同じ文字列に、もう一度マッチさせる(後方参照) |
なお、グループ化の構文には(?>...)(アトミックグループ、Python3.11以降)もあります。これはバックトラックを抑えて、特定のパターンで処理が極端に遅くなる「破壊的バックトラック」を防ぐためのものですが、本記事では対象外とします。
グループで囲んだ部分を取り出す
グループ(…)で囲むと、マッチした文字列のうち囲んだ部分を取り出せます。
実際に取り出すには、reの関数を使います。ここでは取り出し方を2つ示します。すべてのマッチからまとめて取り出すre.findallと、1か所だけを詳しく調べるre.searchです。
すべてのマッチからまとめて取り出す(re.findall)
グループを使うと、re.findallは囲んだ部分だけを返します。戻り値はグループの数で変わり、グループが0個ならマッチ全体、1個ならその中身、2個以上なら各グループを並べたタプルのリストになります。
print(re.findall(r"\d+円", "120円と80円")) # ['120円', '80円']
print(re.findall(r"(\d+)円", "120円と80円")) # ['120', '80']
print(re.findall(r"(\d+)年(\d+)月", "2020年4月と2021年12月")) # [('2020', '4'), ('2021', '12')]
print(re.findall(r"(\d+)年(\d+)月", "2020年4月と2021年")) # [('2020', '4')]このように、re.findallは見つかったすべてのマッチから、囲んだ部分をまとめて取り出せます。1か所だけを取り出して詳しく調べたいときは、次のre.searchを使います。
1か所を取り出して詳しく扱う(re.searchとMatchオブジェクト)
re.search(パターン, 文字列)は、最初に見つかった1か所をMatchオブジェクトとして返します(見つからなければNone)。re.findallは文字列やタプルを返しましたが、re.searchはMatchオブジェクトを返すので、そのメソッドでマッチした内容や位置を取り出せます。
.group(0)はマッチ全体、.group(1)・.group(2)はかっこで囲んだ部分を左から順に、.groups()は全グループをまとめたタプルを返します。
m = re.search(r"(\d+)年(\d+)月", "2024年12月")
print(m.group(0)) # '2024年12月' マッチ全体
print(m.group(1)) # '2024' 1つ目のグループ
print(m.group(2)) # '12' 2つ目のグループ
print(m.groups()) # ('2024', '12') 全グループのタプル見つからないとき、re.searchはNoneを返します。Noneに対して.group()を呼ぶとエラーになるため、結果を使う前に確認します。
m = re.search(r"(\d+)月", "今月")
print(m) # None 数字がないのでマッチしない実際のコードでは、if m:のように、マッチしたかどうかを確かめてから.group()を使います。
マッチした位置を知りたいときは.span()を使います。引数なしでマッチ全体、番号を渡すとそのグループの位置を「(開始, 終了)」のタプルで返します。
m = re.search(r"(\d+)年(\d+)月", "2024年12月")
print(m.span()) # (0, 8) マッチ全体の位置
print(m.span(1)) # (0, 4) グループ1の位置
print(m.span(2)) # (5, 7) グループ2の位置で返されるのは第2引数で与えた文字列に対する位置です。位置は0から数え、終了の位置は含みません(Pythonのスライスや.span()rangeと同じです)。たとえば(5, 7)は5・6文字目を指します。
取り出さずにまとめる(非キャプチャグループ(?:...))
グループには「取り出す」だけでなく「まとめる」役割もあります。たとえば、選択(|)や繰り返し(*や+)を一部だけに効かせたいとき、丸かっこでまとめます。ただし、まとめたいだけで取り出す必要がないこともあります。その場合は(?:...)を使うと、グループとしてまとめつつ、キャプチャ(取り出し)はしません。
キャプチャグループと非キャプチャグループでのre.findallの比較を行います。
print(re.findall(r"(赤|青)色", "赤色と青色")) # ['赤', '青']
print(re.findall(r"(?:赤|青)色", "赤色と青色")) # ['赤色', '青色'] (...)は囲んだ部分「だけ」を取り出しますが、その「囲んだ部分だけを取り出す」機能をなくして「まとめる」機能だけを残したのが、非キャプチャグループ(?:...)です。このため、(?:赤|青)色は非キャプチャなので、「赤または青」だけを取り出す機能はなく、「赤または青」を「まとめる」機能だけを持ちます。よって、まとめた部分が後ろの色と結合して、「赤色」「青色」を返します。
非キャプチャグループは、グループ番号にも影響しません。キャプチャグループを増やすと後ろのグループの番号がずれますが、(?:...)なら番号は変わりません。
m1 = re.search(r"(Mr\.|Ms\.) (\w+)", "Mr. Smith")
print(m1.group(2)) # 'Smith' 敬称がグループ1、名前がグループ2
print(m1.span(2)) # (4, 9) 名前の位置
m2 = re.search(r"(?:Mr\.|Ms\.) (\w+)", "Mr. Smith")
print(m2.group(1)) # 'Smith' 敬称は非キャプチャなので、名前がグループ1
print(m2.span(1)) # (4, 9) 名前の位置敬称をキャプチャグループにすると、名前は.group(2)になります。敬称を非キャプチャにすれば、名前は.group(1)のままです。必要な部分だけをキャプチャしておくと、番号の管理が楽になります。.span()についても同様です。
補足として、キャプチャグループを繰り返し(+、*、{n}など)と組み合わせると、グループが保持するのは最後の1回分だけになります。
print(re.findall(r"(ab)+", "ababab")) # ['ab'] キャプチャ
print(re.findall(r"(?:ab)+", "ababab")) # ['ababab'] 非キャプチャ (ab)+は"ababab"全体にマッチしますが、グループ(ab)が覚えているのは最後の"ab"だけになります。取り出す必要がなければ(?:ab)+にすれば、キャプチャしないのでマッチ全体['ababab']が得られます。
名前で扱う(名前付きグループ(?P<name>...))
キャプチャグループには名前を付けられます。(?P<name>...)と書き、.group("name")で名前を指定して取り出します。.groupdict()は、名前と中身をまとめた辞書を返します。
m = re.search(r"(?P<year>\d+)年(?P<month>\d+)月", "2024年12月")
print(m.group("year")) # '2024'
print(m.span("year")) # (0, 4) year の位置
print(m.group("month")) # '12'
print(m.span("month")) # (5, 7) month の位置
print(m.groupdict()) # {'year': '2024', 'month': '12'}番号ではなく名前で扱えるので、グループが増えてもどれが何かが分かりやすくなります。なお、名前付きグループは番号でもアクセスできます(名前と番号の両方が使えます)。
print(m.group(1), m.group(2)) # 2024 12re.findallは名前を返しません。名前付きでも、結果はこれまでと同じくタプル(または文字列)のリストです。名前で取り出したい場合は、re.searchの.groupdict()を使います。
print(re.findall(r"(?P<year>\d+)年(?P<month>\d+)月", "2020年4月")) # [('2020', '4')]同じ内容の再出現を捉える(後方参照)
パターンの中で\1と書くと、「グループ1がマッチしたのと同じ内容」を表します。これを後方参照と言います。同じ単語の連続や、対になる記号などを捉えるのに使います。
print(re.findall(r"\b(\w+) \1\b", "the the cat dog dog")) # ['the', 'dog'](\w+)が単語を捕まえ、\1が「その単語と同じ文字列」を表すので、このパターンは「同じ単語が2回続く箇所」にマッチします。"the the"と"dog dog"はマッチし、1回だけの"cat"はマッチしません。
名前付きグループには(?P=name)で後方参照します。
print(re.findall(r"\b(?P<word>\w+) (?P=word)\b", "the the dog dog")) # ['the', 'dog']後方参照が捉えるのは「同じ内容(マッチした文字列)」であって、「同じパターン」ではありません。たとえば(\d+)のあとの\1は、「先に捕まえた数字とまったく同じ数字」を表し、「任意の数字」ではありません。
まとめ
正規表現のグループについて、reモジュールで例を示しながら確認しました。
(...):キャプチャグループ。re.searchの.group(番号)・.groups()やre.findallで中身を取り出せる。位置は.span()で得られる。(?:...):非キャプチャグループ。まとめるだけで取り出さず、グループ番号にも影響しない。(?P<name>...):名前付きグループ。.group("name")・.groupdict()で名前で扱える。\1・(?P=name):後方参照。同じ「内容」の再出現を捉える。
グループは、マッチした内容を構造的に取り出すための基本になります。
なお、次の記事「正規表現の先読み・後読み (Python) 」で正規表現のパターンマッチングに条件をつける先読み・後読みを取り扱います。