今回あたりから正規表現が本領発揮しつつ、魔界入りし始めます。今回の記事は自分でもかなり苦しみました。まだ見落としがあるかもしれませんので、今後も更新すると思います。

正規表現の先読み・後読みは言葉で説明するとわかりづらいので、具体例から先に学ぶのがよいと思います。Rubularの実行例を用意しましたので、ぜひ自分で動かして遊んでみましょう。

正規表現はじめの十二歩: 「先読み」と「否定先読み」

(?=正規表現)
先読み(look-ahead): 次の場合に使う
・その位置の直後正規表現がある場合にのみマッチさせたい
・だが直後のその正規表現マッチに含めたくない
(?!正規表現)
否定先読み(negative look-ahead): 次の場合に使う
・その位置の直後正規表現がある場合にのみマッチから除外したい
・だが直後のその正規表現マッチに含めたくない

先読みの例

上の例は、「通過スワップ」というタイポの「通過」、または「通化スワップ」というタイポの「通化」にマッチします。それ以外にはマッチしません。

これはあえて先読みだけを記述したものです。「検討」と「を祈る」の間、つまり文字と文字の「間」にマッチしていることにご注目ください。これが「位置」です。

否定先読みの例

(?!なら)(?!だから)と2つ置いていることにご注目ください。上述したように、位置指定子はそれ自体は文字ではないので、このように複数置いて条件を追加できます。言葉で表せば「その文字の直後にならだからもない」という意味です。

正規表現はじめの十三歩: 後読みと否定後読み

(?<=正規表現)
後読み(look-behind): 次の場合に使う
・その位置の直前正規表現がある場合にのみマッチさせたい
・だが直前のその正規表現マッチに含めたくない
(?<!正規表現)
否定後読み(negative look-behind): 次の場合に使う
・文字列の直前正規表現がある場合にのみマッチから除外したい
・だが直前のその正規表現マッチに含めたくない

後読みの例

「再コンパル」というタイポを検出します。「再」をハイライトせず「コンパル」だけをハイライトできます。

否定後読みの例

「クノロジー」というタイポを検出します。否定後読みを用いて、タイポでない文字列を除外しています。

否定後読み(?<!テ)と否定先読み(?!パーク)を同時に使っているのがポイントです。

なお、クノロジーパークという施設は実在します。

先読み/後読みとは

改めて説明します。

(?=正規表現)
先読み(look-ahead)
(?!正規表現)
否定先読み(negative look-ahead)
(?<=正規表現)
後読み(look-behind)
(?<!正規表現)
否定後読み(negative look-behind)

先読みと後読みは、条件付け、限定、フィルタに利用できます。正規表現のパワーを飛躍的に高める有用な表現トップクラスなのでぜひ使いこなしましょう。特に、マッチした部分をハイライトするときや置換するときにぜひとも欲しくなる記法です。割と覚えにくいので、私はカンペを作りました。

なお、先読みと後読みの両方をまとめて「lookaround」と呼ぶこともあります。

先読みや後読みは難しく言うと「位置指定子」ですが、「position specifier」みたいな英語があるわけではないようです。

先読みや後読みはあくまで位置を指定する記法なので、それ自体は文字ではありません。つまり文字カウントとしてはゼロです。上述の例で先読みを単独で使うと文字と文字の「間」にマッチしましたが、その理由がこれです。

なお、先読みと後読みは残念ながらライブラリによって機能にかなり差があります(後述)。

参考: 「先」と「後」はマリオになったつもりで考えよう

日本語訳の「先読み」「後読み」は誤解を招きやすいという問題があります。というのも、日本語の「先」や「後」がそもそも曖昧さを含んでいるからです(「前」「後」も同じく曖昧です)。

その意味で、英語の「look-ahead」「look-behind」で覚える方が間違えにくいかもしれません。

「先」「後」は、自分がマリオになったつもりで文字を読み進めるときの進行方向で考えるとよいでしょう。

これならたとえアラビア語のような「右から左に書く言語」(BiDi)であっても統一的に扱えます(というよりそう考えるしかありません)。

そういえば最近のマリオは3Dと2Dを行き来できますね。

参考: (?なんちゃら)って?

高機能な正規表現ライブラリの多くは、先読みや後読みといった拡張機能を(?なんちゃら)の形で表します。

POSIX系にありがちな[: :][[: :]]形式の拡張は普遍性(特にUnicode対応)に不安があるため、私は使わないようにしています。

先読み/後読みのポイント

1. 原理的にはいくつでも追加できる

先読みや後読みは、前回学んだ\A\zと異なり、原理的には正規表現の途中にいくつでも置けます(もちろん文字セット[]の中は除きます)。

実装に依存する可能性がありますが、その気になれば/(?<!(?<=トテ))/のように入れ子にすることすら可能です(意味のある例をちょっと思いつきませんが)。

src="img/20181114_122202_Jn9I0a-1024x376.png" sizes="(max-width: 1024px) 100vw, 1024px"

言葉で説明すると、以下をすべて満たすもののみがマッチします。

参考: 正規表現にも「AND」が隠れているでも説明しましたが、正規表現の文字やメタ文字(|は除く)は「AND」の関係を表すので、(?<=東京)(?<!大阪)と続けて書くことで、その位置に関する条件を追加できます。指定できるのはあくまで位置であり、文字ではないことに注意しましょう。

ただし上の例は実用上は非常に冗長ですので真似しないでください。上では条件を4つも指定していますが、条件は先読みに1つ、後読みに1つあれば十分です。

2. 先読みや後読みはいくつ連続しても「1つの位置」に集約される

今回の最大の目玉です。ちょっとわかりにくいので、具体例を出します。

(?<!東京)のような否定後読みを5つ連続で置いていることにご注目ください。連続している限り、先読みや後読みをいくつ置いても、その位置はただ1箇所を指します。

先読みや後読み以外の正規表現をすべて削除してみるとこのことがよくわかります。

上は、最初の1つだけ後読み、後は否定後読みにしたものですが、5つある後読みが1箇所だけを指しているのがおわかりいただけるかと思います。

この連続する後読み同士を試しにRubularで入れ替えてみてください。結果はまったく変わりません。

言い換えると「連続する先読み後読み同士の位置関係や順序は消滅する」ということです。これは、以下の性質から導かれます。

なお、連続する先読みや後読みは、たとえ丸かっこ()で仕切っても位置を分断できません。

上は連続する(?<=東京)(?<!大阪)をそれぞれ()に入れていますが、位置はやはり1箇所に収束しています。

??注意: 連続で無意味な組み合わせを作らないこと

これで安心して先読みや後読みを連続させられると思いたいところですが、連続しているもの同士はANDの関係になっていることに注意しましょう。

たとえば、肯定先読みの2つ以上の連続や、肯定後読みの2つ以上の連続は、たいてい無意味です。

/(?<=東京)(?<=大阪)/は「その位置の直前にあるのは東京であり、かつ大阪である」ということになるので、正規表現としてはvalidでも、マッチすることは永久にありません。メタ文字が入ればまた違うとは思いますが。

また、肯定先読みと肯定後読みの連続は冗長です。これもメタ文字が入ればまた違うとは思いますが。

この場合、/(?<=東京)(?=特許)/と書くぐらいなら/(?<=東京特許)/などと1つにまとめて書く方が素直です。

また、否定先読みと否定後読みの連続も無駄の多いパターンです。これもメタ文字が入ればまた違うとは思いますが。

この場合、/(?<!東)(?!京)/などと書くぐらいなら/(?<!東京)/などとまとめて書く方が素直です(それでもパターンとして有意義とは言えませんが)。

3. パターンの途中にも置ける

先読み・後読み・否定先読み・否定後読みは位置指定子なので、一応パターンの途中にも置けます。

なお、位置指定子をパターンの途中に置く積極的な意味はそれほどないと思われます。私が気づいていないだけかもしれませんが。

4. 先読みや後読み「そのもの」には量指定子を付けられない

先読みや後読みが位置のみを表すので、先読みや後読みそのものに?+といった量指定子を付ける意味はありません。

regex101で試すと上のようにエラーになります。

なぜかRubularではエラーになりませんが、これはエラー扱いにする方がよいように思えます。

??注意: 先読み/後読みでも「否定」にはご用心

否定表現は、すなわち文字セットの補集合を表します。

[^文字]のような否定文字セットは改行文字にもマッチすることを前回説明しましたが、似たようなことが否定先読みや否定後読みでも起きます。

上の例では、「黄巻紙」以外に単なる「巻紙」にまでマッチしてしまいました。改行文字や非文字(冒頭位置や末尾位置など)も/(?<![青赤])巻紙/に該当するからです。

文字セットの補集合は途方もなくデカイことを思い知らされます。そんなものに+*のような凶悪な量指定子を付けたらどれだけパフォーマンスが落ちるかと思うと 。もっとも先読みや後読みそのものには量指定子を付けられませんが。

単なる「巻紙」を除外したい場合は、次のように\n\r\tといった改行文字や非表示文字を文字セットの除外に追加するか、/(?<![青赤])(?<=[黄紫緑])巻紙/などのように明示的な条件を追加するなどの対策が必要です。

ことほど左様に、否定表現はコワいと改めて思います。正規表現は最初に極力肯定的な表現を追求し、否定表現は最後の手段ぐらいに考える方がよいと思います。私も知らずにまだまだ否定表現のワナを踏んでいるかもしれません 。

興味のある方は「ド・モルガンの法則」を調べてみるとよいでしょう。

先読み/後読みはライブラリごとの差が大きい

先ほども書きましたが、正規表現の先読み/後読みは残念ながらライブラリによって差が大きいのが現状です。

たとえばJavaScriptやGo言語に組み込みの正規表現には、後読み機能自体がそもそもありません

ただしChromeのV8は先読み/後読みをフルで使えます(参考: JavaScript: Chrome V8なら正規表現で後読み(look behind)がフル機能で使える)
また、Go言語向けのdlclark/regexp2というパッケージは.NET Frameworkの正規表現ライブラリを移植したもので、パフォーマンスはともかく機能は.NET Framework並です。

先読みが使えない正規表現ライブラリは、さすがにメジャーなものにはそれほどないようです。

問題は多くのライブラリで後読みに制約がかかっていることです。Ruby、PHP、Python(PCRE)など、ほとんどの正規表現ライブラリでは、後読み/否定後読みの中のパターンの長さを不定にできないようになっています。

なお、先読みなら上述のようなメジャーなライブラリでパターンの長さを不定にできます。

これは(私にとっては)かなり厳しい制約で、その場合?+*のような長さ不定の量指定子は使えません

たとえ量指定子を使わなくても、長さが不定になる表現は後読みの中で使えません

ここは私の推測ですが、多くの正規表現ライブラリではこの書き方を自粛しているのだと思います。理由としては特に後読みや否定後読みはただでさえ検索の効率が落ちやすく、その中で長さ不定の量指定子などを許すとさらに効率が落ちてしまう可能性があるためです。

ただし.NET Frameworkや最新のChrome V8のJavaScriptなら後読みや否定後読みで長さ不定の量指定子も使えます。

「後読みで長さ不定の表現が使えない」問題の回避方法

「長さが不定の文字列をどうしても後読みの中で使いたい!」という方に、いくつかの回避方法をご紹介します。

1. 全体を|で分割して回避(否定でない後読みなら)

この|による全体分割は強力な味方です。応用範囲が広く、可読性もよいのでぜひ活用しましょう。

それぞれの後読みの中で文字列の長さを揃えるのがコツです。

ただし、これも否定後読みでは注意が必要です。

そもそも「AまたはB」でない「Aでない」または「Bでない」という否定がらみのロジックの可読性が低い(非常に間違えやすい)という問題があります。

また、Rubyの否定後読みでは、長さが同じであっても/(?<!(東京|大阪))特許事務所/という書き方自体が許されません。否定後読みの中で()を使うだけでもエラーになります(ちょっと厳しすぎる気もします)。

PHP/Pythonでは長さが同じ否定表現/(?<!(東京|大阪))特許事務所/は許されます(regex101.com)。もちろんChrome V8 Javascriptもです。

2. 複数の肯定後読みを()で囲んで|でつなぐ

文字列の長さごとに肯定後読みを書き、それらを()で囲んで|でつなぐ方法もあります。()の入れ子が増えるのが難点ですが、可読性はさほど下がりませんし、この方がコンパクトに書ける場合もあるので、これもおすすめです。

前述のように、Rubyの否定後読みの中ではそもそも()を書けないので、肯定後読みが対象です。

あくまで想像ですが、否定後読みの中で()を許すと((?<!(東京|神奈川))のような人間が間違えやすい「否定とORの併用」ロジックの乱用を誘発するので、戒めのために禁止しているのかもしれません。
しかし否定先読みでは((?!(東京|大阪))という書き方は許されています。
このことについては私の中で戒めだと思うことにします。

3. {N}で長さ指定して回避

{N}量指定子による一意の長さ指定は例外的に使えます。{N,M}などの範囲量指定子は使えません。

これを1.や2.のように|でつなげても構いませんし、要注意ながら否定後読みでも使えます(否定表現を|でつなぐのは避けたい)。

もっとも、普段から+*のような凶悪な量指定子ではなく、{N}{N,M}のように大人しい量指定子を積極的に使いたいものです。

まとめ

正規表現は常に冒頭から末尾に向けて探索を進めるので、マッチの頻度が高いものを左に寄せると速くなります。たとえば( | | )は途中でマッチすればそこで処理を終えるので、頻度の高いものを左に置くようにしましょう。