2020年1月7日

先読みと後読み(Lookahead/lookbehind)

別のパターンが続く、あるいは先行するパターンにマッチするものだけを探したいことがあります。

そのための特別な構文があり、それは “先読み”, “後読み” と呼ばれ、まとめて “lookaround” と呼ばれます。

まず、1 turkey costs 30€ といった文字列から価格を探してみましょう。つまり: 数値の後に 記号が続きます。

先読み(Lookahead)

構文は: X(?=Y)で、“X を探すけど、Y が続く場合にだけマッチする” を意味します。XY は任意のパターンになります。

整数値の後に が続く場合、正規表現は \d+(?=€) となります:

let str = "1 turkey costs 30€";

alert( str.match(/\d+(?=€)/) ); // 30, 数値 1 は € が続いてないので無視されます

注意: 先読みは単なるテストであり、括弧の中身 (?=...) は結果 30 には含まれません。

X(?=Y) を探すとき、正規表現エンジンは X を見つけ、次にその直後に Y があるかをチェックします。もしなければ、マッチはスキップされ検索が続きます。

もっと複雑なテストも可能です。例えば、X(?=Y)(?=Z) は次の意味になります:

  1. X を見つけます。
  2. YX の直後であるかチェックします(そうでなければスキップします)。
  3. ZX の直後であるかチェックします(そうでなければスキップします)。
  4. 両方のテストが通れば、マッチになります。

言い換えると、このようなパターンは X の後に Y, Z が同時に続くことを意味します。

これは、パターン YZ が相互に排他的でない場合にのみ可能です。

例えば、\d+(?=\s)(?=.*30) は直後にスペースがあり、その以降のどこかで 30 がある \d+ を探します。:

let str = "1 turkey costs 30€";

alert( str.match(/\d+(?=\s)(?=.*30)/) ); // 1

上の文字列では、数値 1 に正確にマッチします。

否定先読み(Negative lookahead)

同じ文字列から、価格ではなく数量がほしいとしましょう。それは数値 \d+ で、 が続かないものとします。

このために、否定先読みが適用できます。

構文は X(?!Y) で、意味は “X を探すが、Y が続かない場合のみ” です。

let str = "2 turkeys cost 60€";

alert( str.match(/\d+(?!€)/) ); // 2 (価格部分はスキップされました)

後読み(Lookbehind)

先読みでは “先(何が続くか)” を条件に付け加えることができます。

後読みも似ていますが、これは後ろを見ます。つまり、パターンの前に何かがある場合にのみマッチさせるといったことを可能にします。

構文は次の通りです:

  • 肯定後読み(Positive lookbehind): (?<=Y)XX の前に Y がある場合にのみマッチすることを意味します。
  • 否定後読み(Negative lookbehind): (?<!Y)XX の前に Y がない場合にのみマッチすることを意味します。

例えば、価格をUSドルに変えてみましょう。ドル記号は通常数値の前なので、$30 を探すには (?<=\$)\d+$ で始まる数量 – を使います。:

let str = "1 turkey costs $30";

// ドル記号は \$ にエスケープされます
alert( str.match(/(?<=\$)\d+/) ); // 30 (数値単体はスキップされます)

また、数量 – $ から始まらない数値 – が必要な場合、否定後読み (?<!\$)\d+ が利用できます。:

let str = "2 turkeys cost $60";

alert( str.match(/(?<!\$)\d+/) ); // 2 (価格はスキップされます)

キャプチャグループ

一般的に、lookaround の括弧内の内容は結果の一部にはなりません。

E.g. パターン \d+(?=€) では、 記号はマッチの一部としてキャプチャはされません。これは自然なことです: 私たちは \d+ を探している一方で、(?=€) は単に が続くかどうかのテストです。

しかし、状況によっては同様に lookaround 式、またはその一部をキャプチャしたいかもしれません。これは可能です。単に追加で括弧で囲めばよいです。

以下の例では、金額と一緒に通貨記号 (€|kr) がキャプチャされます。:

let str = "1 turkey costs 30€";
let regexp = /\d+(?=(€|kr))/; // €|kr の周りに追加の括弧

alert( str.match(regexp) ); // 30, €

後読みの場合も同じです:

let str = "1 turkey costs $30";
let regexp = /(?<=(\$|£))\d+/;

alert( str.match(regexp) ); // 30, $

サマリ

先読み(Lookahead)と後読み(lookbehind)(あわせて “lookaround” と呼ばれます)は、その前後のコンテキストに応じて何かをマッチさせたい場合に役立ちます。

単純な正規表現の場合は手動で同様のことができます。つまり: 任意のコンテキストですべてにマッチさせた後、ループでフィルタします。

str.match (フラグ g なし)と str.matchAll (常に)は index プロパティをもつ配列として一致を返すため、テキストの中での正確な位置が分かりコンテキストを確認できることを覚えておいてください。

ですが、一般的には lookaround のほうが便利です。

Lookaround のタイプ:

パターン タイプ マッチ
X(?=Y) 肯定先読み(Positive lookahead) X(Y が後に続く場合)
X(?!Y) 否定先読み(Negative lookahead) X(Y が後に続かない場合)
(?<=Y)X 肯定後読み(Positive lookbehind) X(Y の後の場合)
(?<!Y)X 否定後読み(Negative lookbehind) X(Y の後でない場合)

タスク

整数の文字列があります。

非負のものだけを探す正規表現を作成してください(ゼロも許可します)。

使用例:

let regexp = /your regexp/g;

let str = "0 12 -5 123 -18";

alert( str.match(regexp) ); // 0, 12, 123

整数の正規表現は \d+ です。

否定後読みを先頭に追加することで負を除外できます: (?<!-)\d+.

ですが、試してみると1つの “余分な” 結果に気づくかもしれませ。:

let regexp = /(?<!-)\d+/g;

let str = "0 12 -5 123 -18";

alert( str.match(regexp) ); // 0, 12, 123, 8

ご覧の通り、-18 から 8 がマッチしています。これを除外するには、正規表現が別の(マッチしていない)数の途中からではない数値から一致を開始していることを保証する必要があります。

別の否定後読みを指定することで実現できます: (?<!-)(?<!\d)\d+。ここで (?<!-)(?<!\d)\d+ は、一致は別の数字の後からは始まらないことを保証し、我々が必要としていることです。

また、これらは1つの後読みに結合することもできます:

let regexp = /(?<![-\d])\d+/g;

let str = "0 12 -5 123 -18";

alert( str.match(regexp) ); // 0, 12, 123
チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。