👀

正規表現の先読み・後読み

USAMI Kosuke2022/08/21に公開2件のコメント

正規表現の「先読み(lookahead)」「後読み(lookbehind)」について紹介します。

正規表現の「位置へのマッチ」

正規表現は、文字列のパターンマッチに使われます。例えば [0-9]{4} は数字4つが並ぶ文字列にマッチする表現です。

多くの正規表現は「文字列」にマッチしますが、「文字列」ではなく「位置」にマッチする表現があります。これは、アンカーと呼ばれます。また、長さ0の文字列にマッチすると考えて、ゼロ幅アサーションとも呼ばれます。

アンカーの例として、^(先頭)$(末尾)\b(単語の境界)などがあります。

\bcat\b

この例では、cat にはマッチします。一方、categoryconcatcat を含みますが前後が単語の境界になっていないためマッチしません。

先読み

先読み(lookahead)は、位置にマッチする記法の一種です。位置の指定に正規表現を使います。(?=) で囲む記法となっています。

a(?=..d)

これは、a の次に 任意の2文字+d が来る場合に限り、a にマッチする、という正規表現です。abracadabra という文字列の中には a がいくつかあります。このうち、先読みの「任意の2文字+d が来る」という条件に当てはまるのはひとつだけです。

後読み

後読み(lookbehind)は先読みと似ています。

  • (?=regex) : 先読み。次に regex がくる位置にマッチ。
  • (?<=regex) : 後読み。前に regex がくる位置にマッチ。
(?<=<em>)cat(?=</em>)

これは、後読みと先読みを両方使って、cat<em></em> で囲まれている場合にのみマッチします。

否定先読み・否定後読み

先読みと後読みのバリエーションとして以下のものがあります。

  • (?!regex) : 否定先読み。次に regex がこない位置にマッチ。
  • (?<!regex) : 否定後読み。前に regex がこない位置にマッチ。
(?!2022)\d{4}

\d{4} は数字4つの並びにマッチしますが、否定先読み (?!2022)2022 の前の位置にはマッチしません。そのため、全体として (?!2022)\d{4}2022 にはマッチしません。

先読みが便利な場合(1)

先読みが有益な場合はどのような場合でしょうか。実のところ、先読みを使わないと書けないパターンというものはありません。

しかし、先読みを使うと楽に書けるパターンがあります。そのうちのひとつが、以下のパターンです。

(?=.*hoge.*)(?=.*fuga.*)(?=.*piyo.*).*

これは、.* という正規表現の前に3つの先読みがあります。この3つの先読みは、ある特定の位置以降の文字列が .*hoge.* / .*fuga.* / .*piyo.* の3つの正規表現すべてにマッチすることを要求しています。そのため、全体として hoge / fuga / piyo の3つを含む文字列にマッチする正規表現となります。

別の言い方をすると、先読みを使うと複数の正規表現のAND演算が書ける、ということになります。

なお、hoge / fuga / piyo の3つを含む文字列にマッチする正規表現として、以下のものはどうでしょうか。

.*hoge.*fuga.*piyo.*

これは、3つの文字列がこの順番どおりに出てきたときのみマッチします。どの順番で出てきても良いようにするには、以下のようになります(見やすくするため途中で改行を入れています)。

.*hoge.*fuga.*piyo.*|.*hoge.*piyo.*fuga.*|
.*fuga.*piyo.*hoge.*|.*fuga.*hoge.*piyo.*|
.*piyo.*hoge.*fuga.*|.*piyo.*fuga.*hoge.*

長い正規表現は読みづらく感じることが多いです。同じ意味の正規表現が、先読みを知っていれば (?=.*hoge.*)(?=.*fuga.*)(?=.*piyo.*).* と短く書けます。

先読みが便利な場合(2)

もうひとつ、先読みが便利なパターンを挙げます。

数字が何個か並んでいる文字列に対して、3桁ごとにカンマを挿入するコードを、正規表現で書いてみます。ここではプログラミング言語にPerlを使います。

#!/usr/bin/perl

$text = '12345678';
$text =~ s/(?<=\d)(?=(\d\d\d)+$)/,/g;
print $text;

実行すると、確かに 1234567812,345,678 に置換されています。しかし、なぜこのコードでうまくいくのでしょうか。詳しく見てみましょう。

(?<=\d)(?=(\d\d\d)+$) は位置にマッチする正規表現です。(?<=\d) なので直前が数字であり、(?=(\d\d\d)+$) なので直後から数字3つの繰り返し(つまり、数字6つなど数字が3の倍数個)で文字列が終わる、という位置です。12345678 に対しては、23 の間、56 の間、の2箇所にマッチします。

位置にマッチする正規表現は、「長さ0の文字列」にマッチする正規表現です。マッチした箇所を , という文字列に置換するという処理は、該当の位置に , を挿入する処理と同じです。したがって、23 の間、56 の間に , が挿入されます。

先読みを知っていると、このようにスマートなコードが書けます。

注意点

先読みや後読みは、正規表現の歴史の中では比較的新しい機能です。そのため、言語や環境によってはサポートされていない場合があります。ご注意ください。

参考文献

便利ツール

  • Expressions : 正規表現を簡単に試せるmacOSアプリ。本記事の正規表現のスクリーンショットはこのアプリのもの。
  • CodeRunner : 各種プログラミング言語を簡単に動かせるmacOSアプリ。本記事のPerlコードのスクリーンショットはこのアプリのもの。

補足

この記事は、KyotoLT Online 第27回で発表した内容(正規表現の少し進んだ機能)を記事の形に書き直したものです。

GitHubで編集を提案
株式会社ゆめみ

みんな知ってるあのサービスも、ゆめみが一緒に作ってます。スマホアプリ/Webサービスの企画・UX/UI設計、開発運用の内製化支援。Swift,Kotlin,Rust,Go,Flutter,ML,React,AWS等エンジニア・クリエイターの会社です。Twitterで情報配信中

Discussion

解説ありがとうございます。ツールまで紹介していただけるのは試行しやすくて助かります。

余談ですが、個人的にはどちらが「先」でどちらが「後」か、少し混乱します。

同感です。個人的にはlookbehindには「戻り読み」という訳語が好みなのですが、採用例があまり多くありません。ぱっと探して見つかるのは正規表現ライブラリ「鬼車」のドキュメントPHPのマニュアルぐらいです。

(?=.*hoge.*)(?=.*fuga.*)(?=.*piyo.*).*

この正規表現パターンですが、hogeの直前に最長一致.*を使うと、いったん文字列(または行)の末尾まで読み込み、そこから戻ってhogeを探すことになります。最短一致.*?を使う(hogeが現れたらそこで探索を打ち切る)ほうが、効率的な正規表現パターンになります。

use strict;
use warnings;
use Benchmark qw(cmpthese);

my $text = join 'a' x 1000, 'hoge', 'fuga', 'piyo';

cmpthese(-1, {
    '.*hoge.*' => sub {
        my $v = $text =~ m/(?=.*hoge.*)(?=.*fuga.*)(?=.*piyo.*).*/;
    },
    '.*?hoge' => sub {
        my $v = $text =~ m/(?=.*?hoge)(?=.*?fuga)(?=.*?piyo).*/;
    },
});
# Perl 5.36.0での実行結果:
#               Rate .*hoge.*  .*?hoge
# .*hoge.*  133980/s       --     -94%
# .*?hoge  2096352/s    1465%       --

コメントありがとうございます。

個人的にはlookbehindには「戻り読み」という訳語が好み

なるほど、「戻り読み」いいですね。採用事例もあるとのこと、情報ありがとうございます。

最短一致.*?を使う(hogeが現れたらそこで探索を打ち切る)ほうが、効率的な正規表現パターンになります。

指摘ありがとうございます。パフォーマンス比較もしていただいて参考になります。後ほど、本文にも追記しようと思います。

ログインするとコメントできます