正規表現の量指定子(*、+、?、{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 回以上」という意味だ。> が現れた時点で必然的にマッチが止まるので、非貪欲マッチと同じ結果になる。パフォーマンス面ではこちらのほうが有利な場合もあるため、状況に応じて使い分けるとよい。