正規表現には「マッチはしないが、条件として参照する」という特殊な構文がある。先読み(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 モジュールでは、後読みのパターンは固定長でなければならないという制約がある。+ や * のような可変長の量指定子は使えない。
(?<=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 で「位置」に文字を挿入するという、やや特殊な使い方が可能になる。
ルックアラウンドは最初はとっつきにくいが、「マッチに含めたくないけど条件としては必要」という場面を意識すると、自然に使いどころが見えてくるはずだ。