正規表現の貪欲マッチと非貪欲マッチ

正規表現の量指定子(*+?{n,m})はデフォルトで「貪欲(greedy)」に動作する。できるだけ長くマッチしようとするのだ。一方、量指定子の後ろに ? を付けると「非貪欲(non-greedy / lazy)」になり、できるだけ短くマッチするようになる。

貪欲マッチの挙動

HTML タグを抽出したい場面を考えてみよう。次のコードは意図どおりに動かない。

import re

text = "<p>Hello</p><p>World</p>"
pattern = r"<.*>"

result = re.findall(pattern, text)
print(result)
期待した結果

['<p>', '</p>', '<p>', '</p>'] のように個別のタグを取得したい

実際の結果

['<p>Hello</p><p>World</p>'] と文字列全体がマッチしてしまう

.* は「任意の文字を 0 回以上」という意味だが、貪欲マッチなので最初の < から最後の > まで可能な限り長くマッチする。途中にある > で止まってくれない。

非貪欲マッチで解決する

量指定子の後ろに ? を付けると非貪欲マッチになる。

import re

text = "<p>Hello</p><p>World</p>"
pattern = r"<.*?>"

result = re.findall(pattern, text)
print(result)
# ['<p>', '</p>', '<p>', '</p>']

.*? は「任意の文字を 0 回以上、ただし最短で」という意味になる。最初の < にマッチしたら、次に > が見つかった時点ですぐにマッチを終える。これで個別のタグを抽出できた。

量指定子ごとの非貪欲版

すべての量指定子に非貪欲版がある。

貪欲非貪欲意味
**?0 回以上(最短)
++?1 回以上(最短)
???0 回または 1 回(最短)
{n,m}{n,m}?n〜m 回(最短)

実践例:引用符で囲まれた文字列を抽出

CSV やログファイルでよくあるパターンとして、引用符で囲まれた文字列の抽出がある。

import re

text = 'name="Alice" age="30" city="Tokyo"'

# 貪欲マッチ(失敗例)
greedy = re.findall(r'".*"', text)
print(greedy)
# ['"Alice" age="30" city="Tokyo"']

# 非貪欲マッチ(成功例)
lazy = re.findall(r'".*?"', text)
print(lazy)
# ['"Alice"', '"30"', '"Tokyo"']

貪欲マッチだと最初の " から最後の " まで全部マッチしてしまうが、非貪欲マッチなら各引用符ペアを個別に抽出できる。

非貪欲マッチを使わない別解

非貪欲マッチの代わりに、否定文字クラスを使う方法もある。

import re

text = "<p>Hello</p><p>World</p>"

# 非貪欲マッチ
pattern1 = r"<.*?>"

# 否定文字クラス
pattern2 = r"<[^>]*>"

print(re.findall(pattern1, text))
# ['<p>', '</p>', '<p>', '</p>']

print(re.findall(pattern2, text))
# ['<p>', '</p>', '<p>', '</p>']

[^>]* は「> 以外の文字を 0 回以上」という意味だ。> が現れた時点で必然的にマッチが止まるので、非貪欲マッチと同じ結果になる。パフォーマンス面ではこちらのほうが有利な場合もあるため、状況に応じて使い分けるとよい。