正規表現の先読み・後読みを5つのエンジンで試してわかったこと
(?=)、(?!)、(?<=)、(?<!) を Python・Perl・PHP・JavaScript(Dart)・.NET で同じパターンで実測。後読みの長さ制限の違い、貪欲マッチの罠、2021年の例の誤り。
2026年11月改訂: この記事は2021年に韓国語で書いた講座を、日本語で書き直したものです。すべての例を、Python 3.9 の
re、Perl 5.34、PHP 8.5(PCRE2 10.47)、Dart 3.11(RegExpはJavaScriptと同じ文法)、.NET 10 の5つのエンジンで動かし直しました。元の記事の最後の例はどのエンジンでも説明どおりに動かず、後読みで許される範囲もエンジンごとに違いました。以下の結果はすべて実測です。
先読み・後読みとは
先読み・後読み(lookaround)は、今の位置の前(または後ろ)にあるパターンが存在するかを確かめるだけで、その部分はマッチ結果に含めない書き方です。幅ゼロの条件だと考えてください。
| 記号 | 名前 | 意味 |
|---|---|---|
(?=…) |
肯定先読み | 後ろに…がある |
(?!…) |
否定先読み | 後ろに…がない |
(?<=…) |
肯定後読み | 前に…がある |
(?<!…) |
否定後読み | 前に…がない |
先読み (?=)
.+(?=:) は「コロンの手前までの文字列」を探し、コロン自体は結果から外します。コロンをそのまま書く(.+:)と、結果は http: のようにコロンを含みます。
| テキスト | .+(?=:) の結果(5エンジン共通) |
|---|---|
http://www.abc.com |
http |
http://www.abc.com:8080 |
http://www.abc.com ← 罠 |
2行目が、2021年の記事で抜けていた罠です。.+ は貪欲(greedy)なので、できるだけ長く、つまり最後のコロンの手前まで取ります。最初のコロンまでにしたいなら次のどれかです。
| パターン | http://www.abc.com:8080 での結果 |
|---|---|
.+?(?=:)(最短一致) |
http、://www.abc.com(2件) |
[^:]+(?=:)(コロン以外) |
http、//www.abc.com(2件) |
^[^:]+(?=:)(先頭に固定) |
http(1件) |
3つとも5つのエンジンで同じ結果でした。「先頭のスキームだけ」が欲しいなら、先頭に固定した最後のパターンがいちばん意図がはっきりします。
後読み (?<=)
後読みは逆に前を見ます。(?<=\$)[0-9.]+ は $ の直後の数字だけを取り出します。$ はメタ文字なので \$ とエスケープします。
| テキスト | 結果(5エンジン共通) |
|---|---|
1: $600.4 2: $10.25 |
600.4、10.25 |
(?<=,)[a-z\s!]+ を Hello, world! に使うと、結果は world! ではなく先頭に空白の付いた world! です。文字クラスに \s が入っているので、カンマの後ろの空白も一緒に取れます。空白を外したいなら、(?<=,\s)[a-z!]+ のように後読みの中に空白を入れます。
否定後読み (?<!) と、2021年の例の訂正
2021年の記事では、\b(?<!\$)\d+ を $105$677$788 に使うと「$ のない数字だけを探す」と説明していました。実際に動かすと、5つのエンジンすべてで結果は0件です。このテキストの数字はすべて $ の直後にあるので、探す対象そのものがありません(記事を移行したときに空白が消えた可能性が高いです)。空白のあるテキストで測り直しました。
| パターン | $105 677 $788 での結果 |
|---|---|
\b(?<!\$)\d+ |
677 |
(?<![\d$])\d+ |
677 |
\b が必要なのは、それがないと $105 の 05 のように数字の途中からマッチが始まりうるからです。2つ目のパターンのように「前に数字も $ もない」を条件にすれば、\b なしで同じ結果になります。
エンジンで違うこと:後読みの長さ
この記事でいちばん大事な部分です。後読みの中のパターンの長さをどこまで許すかは、エンジンごとに違います。
- パターン①:
(?<=\$|USD )\d+。後読みの中の選択肢の長さが違う(1文字と4文字) - パターン②:
(?<=\$\s*)\d+。後読みの中に長さの上限がない
| パターン | テキスト | Python 3.9 re |
Perl 5.34 | PHP 8.5(PCRE2 10.47) | JavaScript(Dart 3.11) | .NET 10 |
|---|---|---|---|---|---|---|
| ① | $5 USD 7 |
エラー | 5、7(実験的機能の警告) |
5、7 |
5、7 |
5、7 |
| ② | $ 42 |
エラー | エラー | エラー | 42 |
42 |
実際のエラーメッセージです。
Python : look-behind requires fixed-width pattern
PHP : Compilation failed: length of lookbehind assertion is not limited at offset 0
Perl : Lookbehind longer than 255 not implemented
Perl : Variable length lookbehind is experimental in regex (警告。動作はする)
まとめると次のとおりです。
- どこでも通るパターンが必要なら、後読みの中は長さが固定のパターンだけにする(
(?<=\$)、(?<=USD )) - 選択肢の長さが違うなら、
(?:(?<=\$)|(?<=USD ))\d+のように後読みを2つに分ける。Pythonを含む5つのエンジンすべてで5、7が返った - 長さに上限のない後読みがどうしても必要なら、キャプチャグループに置き換えるのが移植しやすい。
\$\s*(\d+)は5つのエンジンすべてで$ 42にマッチし、グループ1は42になる - Pythonでどうしても使うなら、サードパーティの
regexモジュールは可変長の後読みに対応している(標準のreは非対応)
実務での使い道:パスワードの条件チェック
先読みをパターンの先頭に複数並べると、「すべての条件を同時に満たす」を表せます。
^(?=.*\d)(?=.*[A-Z]).{8,}$
| 入力 | 結果(5エンジン共通) |
|---|---|
Passw0rdX |
マッチ(数字あり、大文字あり、8文字以上) |
password1 |
マッチしない(大文字なし) |
それぞれの (?=…) は位置を動かさずに条件だけを確かめるので、条件はいくつでもつなげられます。ただ、規則が複雑になったら、1つの正規表現より条件ごとに分けたコードのほうが、エラーメッセージも出しやすく保守もしやすくなります。
実験の方法
同じパターンとテキストを1つのTSVファイルに書き、エンジンごとに10行ほどの実行プログラムを作って、すべてのマッチを出力しました。
| エンジン | 使った呼び出し |
|---|---|
| Python | re.findall |
| Perl | while (/re/g) |
| PHP | preg_match_all(/u フラグ) |
| Dart | RegExp(..., unicode: true).allMatches |
| .NET | Regex.Matches(.NET 10 の単一ファイル実行 dotnet run t.cs) |
正規表現の「標準」は1つではありません。自分のコードが使うエンジンで直接確かめる習慣が、いちばん確実です。
よくある質問
後読みの中で * や + を使える?
エンジンによります。(?<=\$\s*) のような長さに上限のない後読みが動いたのは .NET と JavaScript(Dartで確認)だけで、Pythonのre、PHP(PCRE2 10.47)、Perl 5.34 ではコンパイルエラーになりました。
.+(?=:) がURLでおかしな結果になるのはなぜ?
.+ が貪欲(greedy)で、最後のコロンの手前まで取るからです。http://www.abc.com:8080 では http ではなく http://www.abc.com にマッチします。スキームだけ欲しいなら ^[^:]+(?=:) を使います。
先読みでパスワードの条件をチェックできる?
できます。^(?=.*\d)(?=.*[A-Z]).{8,}$ は、数字と大文字をそれぞれ1つ以上含む8文字以上の文字列にだけマッチします。5つのエンジンすべてで同じ結果でした。