正規表現の先読みと後読み — lookahead / lookbehind

正規表現には「マッチはしないが、条件として参照する」という特殊な構文がある。先読み(lookahead)と後読み(lookbehind)だ。これらを総称してルックアラウンド(lookaround)と呼ぶ。文字列を消費せずに前後の文脈を確認できるため、複雑な条件付きマッチングに欠かせないテクニックとなっている。

ルックアラウンドの全体像

ルックアラウンドには 4 種類ある。肯定・否定の 2 軸と、前方・後方の 2 軸の組み合わせだ。

種類構文意味
肯定先読み(?=...)直後に ... がある
否定先読み(?!...)直後に ... がない
肯定後読み(?<=...)直前に ... がある
否定後読み(?直前に ... がない

「先読み」は現在位置の右側(後ろ)を見る。「後読み」は左側(前)を見る。名前と方向が逆に感じるかもしれないが、「これから読む方向」が先だと考えるとわかりやすい。

肯定先読み (?=…)

「直後に特定のパターンが続く」場合だけマッチさせたいときに使う。先読み部分自体はマッチ結果に含まれない。

import re

text = "100円 200ドル 300円 400ユーロ"
result = re.findall(r"\d+(?=円)", text)
print(result)  # ['100', '300']

\d+(?=円) は「直後に『円』がある数字列」にマッチする。マッチ結果には数字部分だけが含まれ、「円」は含まれない。これがルックアラウンドの核心的な性質であり、文字列を「消費しない」と表現される理由だ。

否定先読み (?!..)

逆に「直後に特定のパターンが続かない」場合にマッチさせる。

import re

text = "100円 200ドル 300円 400ユーロ"
result = re.findall(r"\d+(?!円)", text)
print(result)  # ['10', '200', '30', '400']

結果が直感に反するかもしれない。100 の場合、10 の直後は 0 であり「円」ではないため、10 がマッチしてしまう。否定先読みを使うときは、このような部分マッチに注意が必要だ。より正確にフィルタリングしたい場合は、パターン全体の設計を見直す必要がある。

import re

text = "100円 200ドル 300円 400ユーロ"
result = re.findall(r"\d+(?!円)\d*\S+", text)
print(result)  # ['200ドル', '400ユーロ']

このように通貨記号ごと取得するパターンに変えれば、意図した結果が得られる。

肯定後読み (?<=…)

「直前に特定のパターンがある」場合にマッチさせる。先読みとは逆方向を確認する構文だ。

import re

text = "price:500 tax:80 total:580"
result = re.findall(r"(?<=price:)\d+", text)
print(result)  # ['500']

(?<=price:) は「直前に price: がある位置」を示す。price: 自体はマッチ結果に含まれず、数値だけが返される。ログやデータから特定のラベルに紐づく値を抜き出すときに重宝する。

否定後読み (?<!..)

「直前に特定のパターンがない」場合にマッチさせる。

import re

text = "Mr.Smith Mrs.Jones Dr.Brown Smith"
result = re.findall(r"(?<!\w\.)\b[A-Z][a-z]+\b", text)
print(result)  # ['Smith']

敬称が付いていない単独の名前だけを抽出している。(?<!\w\.) は「直前に英数字 + ピリオドの組み合わせがない」ことを条件にしている。

Python の後読みの制約

Python の re モジュールでは、後読みのパターンは固定長でなければならないという制約がある。+* のような可変長の量指定子は使えない。

固定長(OK)

(?<=abc)、(?<=\d{3}) のようにマッチする文字数が確定しているパターンは問題なく動作する。

可変長(エラー)

(?<=\d+)、(?<=a*b) のようにマッチ長が不定なパターンは re モジュールでは使えず、エラーになる。

可変長の後読みが必要な場合は、サードパーティの regex モジュール(pip install regex)を使うか、パターンの設計を変えて先読みで代用する方法がある。

実用例:カンマ区切りの数値フォーマット

ルックアラウンドの実践的な応用例として、数値に 3 桁ごとのカンマを挿入する処理を見てみよう。

import re

def add_commas(n):
    s = str(n)
    return re.sub(r"(?<=\d)(?=(\d{3})+$)", ",", s)

print(add_commas(1234567))    # 1,234,567
print(add_commas(100))        # 100
print(add_commas(1000000000)) # 1,000,000,000

(?<=\d) で「直前に数字がある位置」、(?=(\d{3})+$) で「直後に 3 の倍数個の数字が末尾まで続く位置」を指定している。この位置にカンマを挿入するわけだ。ルックアラウンドは文字を消費しないため、re.sub で「位置」に文字を挿入するという、やや特殊な使い方が可能になる。

ルックアラウンドは最初はとっつきにくいが、「マッチに含めたくないけど条件としては必要」という場面を意識すると、自然に使いどころが見えてくるはずだ。