Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

正規表現の先読み・後読み(look ahead、look behind)を活用しよう

こんにちは、hachi8833です。
今回は、Rubyを中心に複数の言語やライブラリにわたる正規表現の先読み・後読みについての記事です。

主な対象言語・ライブラリ

  • PCRE、またはPCREに近い正規表現ライブラリ
    • 例: Ruby(Onigmo)、Perl、PHPなど

この他の正規表現ライブラリでは、先読み・後読み機能のすべてor一部を利用できないことがあります。

1. 先読み・後読みの応用例

プログラム中で、単に特定の文字列やパターンにマッチするかどうか(truefalseか)を取りたいだけであれば、先読み・後読みの出番はあまりありません。

先読み・後読みは、以下のような場合に有用です。

マッチする文字列に含めたくない条件がある場合

マッチする文字列には含まれないが、その前後の文字列によって除外したい条件がある場合、先読みや後読みが使えます。

私の正規表現コレクションの中からいくつか引用いたします。

a =~ /(?<!弱)小会社/

これは「小会社」という誤用にマッチし、「弱小会社」という単語にはマッチしないパターンです。

マッチするかどうかだけが知りたいのであれば、[^弱]小会社という基本的な正規表現だけで十分です。しかし私の場合は誤用の部分だけをハイライトしたいので、「小会社」という部分だけにマッチするよう、こうした先読み・後読みを多用します。

a =~ /就業(?=住宅)/

これは「集合住宅」のつもりが誤って「就業住宅」になっていることをチェックするパターンです。もちろん、用途によっては単に就業住宅とすれば足りることもありますが、私は「就業」だけをハイライトするために肯定先読みを使っています。

参考

Rubyでは、最後に実行した正規表現チェックのマッチ文字列を取り出すのに以下を利用できます。

参考: [Ruby] Kernelの特殊変数をできるだけ$記号なしで書いてみる

否定表現で正規表現を簡潔にする

先読み・後読みには否定表現があります。

呼び方 正規表現 用例
否定先読み 対象(?!先読み条件) look(?!ahead): lookの先にaheadがない場合に、lookにマッチ
否定後読み (?<!後読み条件)対象 (?<!look)behind: lookがbehindの後にない場合に、behindにマッチ

これをうまく活用すると、正規表現の苦手な「否定マッチ」のかなりの部分をカバーできます。条件が「部分否定」であれば十分対応可能です。

先の(?<!弱)小会社も、否定後読みによる部分否定マッチです。

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

正規表現は全否定が苦手

よく知られていることですが、「○☓という文字列を含まない行にマッチ」のような投げやりな否定表現は、まともに正規表現で表そうとすると大変なことになってしまいます。

以下はStackoverflowで見つけた事例です。

# "hede"を含まない行にマッチ
a =~ /^([^h]*(h([^e]|$)|he([^d]|$)|hed([^e]|$))?)*$/

正規表現で「〜という文字列を含まない」を表そうとするなら、そもそもそれが本当に必要なのかどうかを考え直す方がよいかもしれません。

grep系のコマンドで単に特定の文字を含む行を除外したいのであれば、-vオプション(マッチしない行を出力)で対応するのがベストだと思います。

ネットで検索すると以下のような方法でできると書かれている記事を見かけますが、ほとんどが間に合わせの近似解でしかなく、完全ではありません。

a =~ /^((?!hoge).)*$/
a =~ /^(?:(?!hoge).)*$/

つい先ごろリリースされたRuby 2.4.1では、上述のような「〜を含まないパターン」をシンプルに書けるようになりました。詳しくは以下の記事をご覧ください。早く他のライブラリにも広がって欲しいと願っています。

参考

なお、英名は当初absent operatorとされていましたが、その後指摘を受け、absence operatorと改名するとのことです(#87 Improve document about the absence operator)。

3. 先読み・後読みの中でquantifierを使うことについて

quantifierは量化子や量指定子などと訳されることもあります。要するに以下のような「繰り返し回数」を表すメタ文字のことです。

  • *: 直前の文字の0回以上の繰り返し
  • +: 直前の文字の1回以上の繰り返し
  • ?: 直前の文字の0回または1回の繰り返し -- 私は「ありやなしやの?」と自分だけで呼んでいます
  • {n, m}:(直前の文字のn回以上のm回以下の繰り返し)

主に私にとって困ったことなのですが、このquantifierやORのメタ文字|の実装状況は、先読み・後読みのライブラリごとに異なっています。

  • 論外: そもそも先読み・後読みを実装していない(Go言語の標準ライブラリ
  • 先読みはあるが後読みが実装されていない(ごく最近までのJavaScriptとか
  • 先読み・後読みでquantifierをまったく利用できない実装
  • 先読みではquantifierを使えるが、後読みでは使えない実装
  • 先読み・後読みの挙動が微妙に他のライブラリと異なる実装
  • 先読みでも後読みでもquantifierを使える実装(Onigmo、.NET Frameworkなど)
    • ただしパターンによってはエラーになることもある(|と併用した場合など)

特に後読みでのquantifierの利用はともすると効率が落ちやすいことがよく指摘されています。普通に考えても、処理と逆方向への探索でさらにquantifierが使われると処理が重くなるというのは理解できます。

私はそれらをある程度承知のうえで先読み・後読みの中でquantifierを使っています。私の利用目的ではメリットの方が圧倒的に大きいからです。

ベンチマークを取っていなくて恐縮ですが、私の経験した範囲では先読み・後読みの扱いやパフォーマンスはライブラリの実装によって異なると感じています。

補足 .NET Frameworkの正規表現は(・∀・)イイ!!

この部分は私の観測範囲に限られていて、私の好みも多分に含まれています。

私がこれまで扱った中で最もよかった正規表現ライブラリは、.NET Frameworkのものです。PCREでできることはひととおり網羅したうえで、さらに多くの機能が搭載され、先読み・後読みもフルで利用できます。

少なくとも機能面では知る限りでは最もリッチなライブラリです。時代も環境も違うのでパフォーマンスを比較しようがないのですが、当時少なくとも先読み・後読みでquantifierを使ったぐらいで目に見えて性能が落ちることはありませんでした。

実のところ、私が最初に知ったのが.NET Frameworkの正規表現でした。このライブラリでできることが当たり前だと思っていたのですが、他の多くの言語に付属する正規表現ライブラリや秀丸の正規表現DLLでは使えない機能があることを知り、実装ごとの違いに関心をもつようになりました。

ドキュメントも充実

見逃せないのは、正規表現ライブラリの中ではドキュメントが圧倒的に充実していることです。C#やVBに寄った解説が多いのは当然としても、BNF記法に毛が生えたようなそっけないドキュメントぐらいしかないことの多い他のライブラリと比べると、私の中ではかなり光り輝いています。.NET Frameworkのバージョンごとにどう機能が違うのかといった点までは調べていませんが、ほとんど違いはないようです。

ネット上の記事では後読みとバックトラックが同一視(あるいは混同)されることが多いのですが、このドキュメントでは(というより.NET Frameworkではなのかもしれませんが)、バックトラックと先読み・後読みは明確に区別されています。実際、.NET Frameworkでは先読みアサーションを(?< )の代わりに(?> )と書くことで不要なバックトラックを抑制できます(quantifierを書いてしまったら元も子もないかもしれませんが)。

正直、あらゆる言語で.NET Frameworkの正規表現を使いたいと強く思います。なお.NET Frameworkはいつの間にかオープンソースになっています(最新バージョンではないようですが)。もちろん正規表現もです。

関連記事

はじめての正規表現とベストプラクティス#4 先読みと後読みを極める


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。