正規表現の先読み(lookahead)と後読み(lookbehind)は、指定パターンに一致する部分を取得する前に、その前(あるいは後ろ)が条件を満たしているかを確認し、満たしている場合のみ指定パターンに一致した部分を取得する方法です。条件に使った前後の文字列は、取得結果には含まれません。
たとえば「120円」という文字列から、「円が後ろに付いた数字」を取り出す場合を考えます。先読みを使った\d+(?=円)では、\d+が指定パターン(取り出したい数字)、(?=円)の円が条件です。円が後ろにあることを確認したうえで、取り出すのは数字の120だけです(条件の円は結果に含まれません)。一方、ふつうのパターン\d+円は、円まで含めて120円を取り出します。
この「確かめるだけで結果に取り込まない」ことを、正規表現では「消費しない」と言います。先読み・後読みのように、消費せずに位置の条件だけを確かめる記法を、まとめてアサーションと呼びます。
先読み・後読みには、条件が満たされることを求める肯定と、満たされないことを求める否定があり、先読みと後読みとの組み合わせにより全4パターンあります(肯定先読み/否定先読み/肯定後読み/否定後読み)。
本記事ではこれらを、Pythonのreモジュールで一つずつ整理します。reモジュールは標準モジュールなのでインストールは不要です。
なお、本記事で用いる正規表現の基本構文およびグループのre.searchやMatchオブジェクトの.span()は過去の記事で扱っています。
目次
- 先読み・後読みの一覧
- 先読み:指定パターン直後の文字に条件を課す
- 後読み:指定パターン直前の文字に条件を課す
- 後読みは固定長だけ(Pythonの制約)
- 複数の条件をまとめて確かめる(先読みの活用)
- まとめ
先読み・後読みの一覧
この記事で扱う構文を、先に一覧で示します。表の「条件」とは、(?=...)などの...に置く、前後をチェックするためのパターンのことです。細かい意味や使いどころは、表のあとの各節で例とともにまとめています。
| 構文 | 呼び方 | 意味 |
|---|---|---|
(?=…) | 肯定先読み | 指定パターンの直後が、条件に一致することを求める |
(?!…) | 否定先読み | 指定パターンの直後が、条件に一致しないことを求める |
(?<=…) | 肯定後読み | 指定パターンの直前が、条件に一致することを求める。Pythonでは条件は固定長のみ |
(?<!…) | 否定後読み | 指定パターンの直前が、条件に一致しないことを求める。Pythonでは条件は固定長のみ |
条件として確かめた部分はマッチ(取得結果)に含まれません。
なお、先読み・後読みはグループではなく位置の条件なので、それ自体はグループ番号を持たず、.group()で取り出す対象にもなりません。後読みには「条件は固定長でなければならない」というPython特有の制約があります。
先読み:指定パターン直後の文字に条件を課す
先読みは、指定パターンの直後が条件に一致するかを確かめます。確かめるだけで、その後ろの部分(条件)は取り出しません。肯定(?=...)と否定(?!...)があります。
肯定先読み(?=...)
(?=...)は「指定パターンの直後が条件に一致するなら、指定パターンにマッチした部分を取得する」という意味です。条件に使った部分はマッチに含めません。
たとえば「円が続く数字だけ」を取り出してみます。
print(re.findall(r"\d+(?=円)", "120円と80ドルと300円")) # ['120', '300']\d+(?=円)では、\d+が指定パターン、(?=円)の円が条件です。80ドルの80は直後が円ではない(条件に一致しない)ので取り出されず、条件に一致する120と300だけが残ります。取り出されるのは指定パターンの数字だけで、条件の円は含まれません。
マッチに円が含まれないことを、位置で確認します。
m = re.search(r"\d+(?=円)", "120円")
print(m.group()) # '120'
print(m.span()) # (0, 3) "円"(位置3)は含まれないマッチは指定パターン\d+に当たる120(位置0〜2)だけで、条件の円(位置3)は含まれません。これは、先読みが条件の円を消費しない(確かめるだけで、マッチに取り込まず、照合位置もそこへ進めない)ためで、.span()が(0, 3)になり円の位置を含まないことに表れています。
否定先読み(?!...)
(?!...)は逆に「指定パターンの直後が条件に一致しないなら、指定パターンにマッチした部分を取得する」という意味です。
たとえば「httpのうち、httpsではないもの」を取り出します。
print(re.findall(r"http(?!s)", "http://a https://b http://c")) # ['http', 'http']http(?!s)では、httpが指定パターン、(?!s)のsが条件です。「直後がsでないhttp」を表すので、httpsのhttpは直後がs(条件に一致)なので除外され、残り2つのhttpがマッチします。
補足として、\d+のような繰り返しの直後に否定先読みを置くと、直感に反する結果になることがあります。
print(re.findall(r"\d+(?!円)", "120円")) # ['12']「円が続かない数字」を期待すると、120の後ろは円なので、何もマッチしないように思えます。しかし\d+は貪欲なので、まず120にマッチし、その直後が円で条件に当たると、エンジンは1文字戻して12を試します。12の直後は0(円ではない)なので条件に一致せず、否定先読みが成立して12がマッチします。
このような後戻り(バックトラック)を避けるには、否定先読みに「直後が数字でないこと」も加え、\d+が数字の途中で切れないようにします。
print(re.findall(r"\d+(?![\d円])", "120円と80点")) # ['80'](?![\d円])は「直後が数字でも円でもない」という条件です。120円の120は直後が円、そこで1文字戻した12も直後が0(数字)なので、どちらも条件に合わず取り出されません。80は直後が点なので取り出されます。さきほどの\d+(?!円)が12を拾ったのに対し、条件に\dを加えたことで数字の途中で切れなくなりました。
後読み:指定パターン直前の文字に条件を課す
後読みは、指定パターンの直前が条件に一致するかを確かめます。先読みと同じく、その前の部分(条件)は取り出しません。肯定(?<=...)と否定(?<!...)があります。ただしPythonでは、後読みの条件は固定長である必要があります。
肯定後読み(?<=...)
(?<=...)は「指定パターンの直前が条件に一致するなら、指定パターンにマッチした部分を取得する」という意味です。
たとえば「$に続く数字(金額)」を取り出します。
print(re.findall(r"(?<=\$)\d+", "$100と$250")) # ['100', '250'](?<=\$)\d+では、\d+が指定パターン、(?<=\$)の$が条件です。「直前が$である数字」を表します。$は行末を表すメタ文字なので、文字として扱うには\$とエスケープします。取り出されるのは指定パターンの数字だけで、条件の$は含まれません。
これも位置で確認します。
m = re.search(r"(?<=\$)\d+", "$100")
print(m.group()) # '100'
print(m.span()) # (1, 4) "$"(位置0)は含まれない$は位置0、数字100は位置1〜3です。マッチは100だけなので.span()は(1, 4)で、条件の$(位置0)は含まれません。
否定後読み(?<!...)
(?<!...)は「指定パターンの直前が条件に一致しないなら、指定パターンにマッチした部分を取得する」という意味です。
たとえば「マイナス符号が付いていない数字(負でない数)」を取り出します。
print(re.findall(r"(?<!-)\b\d+", "5 -3 80 -12")) # ['5', '80'](?<!-)\b\d+では、\d+が指定パターン、(?<!-)の-が条件です。「直前が-でない数字」を表すので、-3や-12の数字は直前が-(条件に一致)なので除外され、5と80が残ります。ここで\b(単語境界)を添えているのは、数字の先頭で判定するためです。これがないと、複数桁の数字で一部だけが拾われることがあります(-12の1は直前が-なので除外されても、2は直前が1で-ではないため、拾われてしまいます)。
後読みは固定長だけ(Pythonの制約)
Pythonの後読みは、条件((?<=...)の...の部分)が固定長(常に同じ文字数にマッチする)でなければなりません。長さが決まらない条件を後読みに入れると、コンパイル時(re.compileやre.searchなどを呼んだ時点)にエラーになります。
# 固定長ならOK
re.compile(r"(?<=abc)x") # "abc" は常に3文字
re.compile(r"(?<=\d{3})x") # ちょうど3文字
# 長さが決まらないとエラー
try:
re.compile(r"(?<=\d+)x") # \d+ は1文字以上で長さが不定
except re.error as e:
print(e) # look-behind requires fixed-width pattern
try:
re.compile(r"(?<=\d{2,4})x") # 2〜4文字で幅がある
except re.error as e:
print(e) # look-behind requires fixed-width pattern選択(|)を使う場合は、どの選択肢も同じ長さなら後読みの条件に使えますが、長さが違うとエラーになります。
re.compile(r"(?<=cat|dog)x") # "cat" も "dog" も3文字
try:
re.compile(r"(?<=cat|fish)x") # 3文字と4文字で長さが違う
except re.error as e:
print(e) # look-behind requires fixed-width pattern先読みにはこの制約はありません。先読みの条件は、長さが決まらなくても構いません。
re.compile(r"(?=\d+)x") # 先読みは長さが不定でもよいこの違いは、照合の向きから来ています。後読みは「現在位置から手前へ何文字か戻って照合する」という動きのため、戻る文字数(=条件の長さ)が決まっている必要があります。長さが不定だと、何文字戻ればよいか決められません。先読みは現在位置から前向きに照合するだけなので、この制約がありません。
複数の条件をまとめて確かめる(先読みの活用)
先読みは消費しないので、同じ位置から複数の条件を重ねて確認できます。これは「文字列が複数の条件を同時に満たすか」を調べるのに便利です。
ここではre.fullmatchを使います。re.fullmatchは、文字列全体がパターンに一致するときだけMatchを返し、一致しなければNoneを返します。
「6文字以上で、数字を1つ以上含む」かどうかを判定してみます。
pattern = r"(?=.*\d).{6,}"
print(bool(re.fullmatch(pattern, "abc123"))) # True 6文字・数字あり
print(bool(re.fullmatch(pattern, "abcdefg"))) # False 7文字だが数字なし
print(bool(re.fullmatch(pattern, "ab12"))) # False 数字はあるが6文字未満ここでは.{6,}(6文字以上)が指定パターン、(?=.*\d)が条件です。(?=.*\d)は「どこかに数字が1つ以上ある」という条件で、.*で任意の文字を読み飛ばし、その先に\dがあるかを見ています。先読みは消費しないので、文字列の先頭でまず条件「数字を含むか」を確認し、続けて指定パターン.{6,}で全体の長さを照合できます。両方を満たすときだけfullmatchが成立します。
条件を増やすときは、(?=...)を必要なだけ並べます。たとえば「8文字以上で、数字と大文字をそれぞれ1つ以上含む」なら次のようになります。
pattern = r"(?=.*\d)(?=.*[A-Z]).{8,}"
print(bool(re.fullmatch(pattern, "Passw0rd"))) # True
print(bool(re.fullmatch(pattern, "password"))) # False 数字も大文字もないパスワードの条件チェックのように、いくつもの条件をAND結合したい場面で使われる書き方です。
まとめ
正規表現の先読み・後読みについて、reモジュールで例を示しながら確認しました。指定パターンが取り出したい本体、条件が...に置いて前後をチェックするパターンです。
(?=...):肯定先読み。指定パターンの直後が条件に一致するときだけ成立する(条件は取り出さない)。(?!...):否定先読み。指定パターンの直後が条件に一致しないときだけ成立する。(?<=...):肯定後読み。指定パターンの直前が条件に一致するときだけ成立する。Pythonでは条件は固定長のみ。(?<!...):否定後読み。指定パターンの直前が条件に一致しないときだけ成立する。Pythonでは条件は固定長のみ。
ここで、「成立する」は「指定パターンにマッチした部分を取得する」ことを意味します。先読み・後読みは、前後を条件にしつつ、その部分はマッチに含めない(消費しない)のが共通の性質です。マッチした位置に文字を差し込む置換(re.sub)と組み合わせると、桁区切りの挿入のような処理にも使えます。
なお、次の記事「正規表現のフラグ(Python)」で正規表現のグループを取り扱います。