正規表現において、*(0回以上)や +(1回以上)などの量指定子は、デフォルトで**貪欲(Greedy)**な挙動をとります。これは、条件を満たす限り「可能な限り長い文字列」にマッチしようとする性質です。
一方、特定の区切り文字までの「最小限の範囲」を取得したい場合は、非貪欲(Lazy / Reluctant)、あるいは最短マッチと呼ばれる挙動に切り替える必要があります。この記事では、意図しない長大なマッチを防ぎ、必要な部分だけを正確に抽出する方法を解説します。
解決したい課題
設定ファイルやコードから、引用符 " で囲まれた値を抽出したい場面を考えます。 単純に ".*" と記述すると、行の最初の引用符から最後の引用符までをひとまとめにしてマッチしてしまい、間の区切り文字ごと飲み込んでしまう問題が発生します。
実装例:設定値の抽出
以下は、複数のパラメータが記述された文字列から、値を個別に抽出するコードです。貪欲なパターンと非貪欲なパターンそれぞれの挙動を比較します。
ソースコード
import re
# 解析対象のテキスト(設定値を模した文字列)
# ターゲット:ダブルクォートで囲まれた値 ("10s", "3", "True") を個別に取得したい
config_text = 'timeout="10s"; retries="3"; verbose="True";'
# 1. 貪欲マッチ (Greedy) のパターン
# .* : 任意の文字に可能な限り長くマッチする
# 結果 : 最初の " から 最後の " までが1つのマッチになってしまう
greedy_pattern = r'".*"'
# 2. 最短マッチ (Lazy) のパターン
# .*? : 任意の文字にマッチするが、可能な限り短く済ませる
# 結果 : " から次の " までを個別にマッチする
lazy_pattern = r'".*?"'
# 実行と結果表示
print("--- 貪欲マッチ (Greedy) ---")
greedy_matches = re.findall(greedy_pattern, config_text)
for m in greedy_matches:
print(f"Match: {m}")
print("\n--- 最短マッチ (Lazy) ---")
lazy_matches = re.findall(lazy_pattern, config_text)
for m in lazy_matches:
print(f"Match: {m}")
実行結果
--- 貪欲マッチ (Greedy) ---
Match: "10s"; retries="3"; verbose="True"
--- 最短マッチ (Lazy) ---
Match: "10s"
Match: "3"
Match: "True"
解説
貪欲マッチ(Greedy)の挙動
パターン r'".*"' を使用した場合、正規表現エンジンは最初の " を見つけた後、行末に向かって可能な限り遠くにある " を探します。その結果、間の "; retries=" といった区切り文字も含めた全体が1つの文字列として抽出されてしまいます。
最短マッチ(Lazy)への切り替え
量指定子(*, +, ?, {m,n})の直後に ? を付加することで、挙動を「最短マッチ」に変更できます。
*(貪欲) →*?(最短)+(貪欲) →+?(最短)
上記のコードでは r'".*?"' とすることで、「最初の " から始まり、最初に見つかる次の " まで」という条件になり、個々の値を正しく分割して抽出できています。HTMLタグの抽出(<div>...</div>)など、閉じ文字が明確な構造を解析する際には、この最短マッチが不可欠です。
