2022年6月7日

先読みと後読み

場合によっては、別の文字列が後続または先行するパターンに一致するものだけを見つけたい場合があります。

そのためには、「先読み」と「後読み」と呼ばれる特別な構文があり、まとめて「ルックアラウンド」と呼ばれます。

まずは、1 turkey costs 30€ のような文字列から価格を見つけましょう。つまり、数字の後に 記号が続きます。

先読み

構文は X(?=Y) で、「X を探しますが、Y が後続する場合にのみ一致します」という意味です。XY の代わりに任意のパターンを使用できます。

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

let str = "1 turkey costs 30€";

alert( str.match(/\d+(?=€)/) ); // 30, the number 1 is ignored, as it's not followed by €

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

X(?=Y) を探す場合、正規表現エンジンは X を見つけ、その直後に Y があるかどうかを確認します。そうでない場合、潜在的な一致はスキップされ、検索は続行されます。

より複雑なテストも可能です。たとえば、X(?=Y)(?=Z) は次のことを意味します。

  1. X を見つける。
  2. YX の直後にあるかどうかを確認する(そうでない場合はスキップする)。
  3. ZX の直後にあるかどうかを確認する(そうでない場合はスキップする)。
  4. 両方のテストに合格した場合、X は一致し、そうでない場合は検索を続行します。

言い換えれば、そのようなパターンは、YZ が同時に後続する X を探していることを意味します。

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

たとえば、\d+(?=\s)(?=.*30) は、スペース (?=\s) が後続し、その後にどこかで 30 がある (?=.*30)\d+ を探します。

let str = "1 turkey costs 30€";

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

私たちの文字列では、それはまさに数字 1 に一致します。

負の先読み

同じ文字列から価格ではなく数量が欲しいとしましょう。それは数字 \d+ で、 が後続しません。

そのためには、負の先読みを適用できます。

構文は X(?!Y) で、「X を検索しますが、Y が後続しない場合にのみ一致します」という意味です。

let str = "2 turkeys cost 60€";

alert( str.match(/\d+\b(?!€)/g) ); // 2 (the price is not matched)

後読み

後読みのブラウザ互換性

注意: 後読みは、Safari、Internet Explorer などの V8 以外のブラウザではサポートされていません。

先読みでは、「後続するもの」の条件を追加できます。

後読みも似ていますが、後ろを見ます。つまり、前に何かがある場合にのみパターンに一致させることができます。

構文は次のとおりです。

  • 正の後読み: (?<=Y)X は、X に一致しますが、前に Y がある場合にのみ一致します。
  • 負の後読み: (?<!Y)X は、X に一致しますが、前に Y がない場合にのみ一致します。

たとえば、価格を米ドルに変更してみましょう。ドル記号は通常数字の前にあるので、$30 を探すには、(?<=\$)\d+ を使用します。つまり、$ が先行する金額です。

let str = "1 turkey costs $30";

// the dollar sign is escaped \$
alert( str.match(/(?<=\$)\d+/) ); // 30 (skipped the sole number)

そして、数量、つまり $ が先行しない数字が必要な場合は、負の後読み (?<!\$)\d+ を使用できます。

let str = "2 turkeys cost $60";

alert( str.match(/(?<!\$)\b\d+/g) ); // 2 (the price is not matched)

キャプチャグループ

一般に、ルックアラウンド括弧内の内容は結果の一部になりません。

たとえば、パターン \d+(?=€) では、 記号は一致の一部としてキャプチャされません。これは当然のことです。数字 \d+ を探しているのに対し、(?=€) が後続する必要があるかどうかをテストするだけです。

しかし、状況によっては、ルックアラウンド式も、またはその一部もキャプチャしたい場合があります。それは可能です。その部分を別の括弧で囲むだけです。

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

let str = "1 turkey costs 30€";
let regexp = /\d+(?=(€|kr))/; // extra parentheses around €|kr

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

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

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

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

まとめ

先読みと後読み(一般に「ルックアラウンド」と呼ばれます)は、前後のコンテキストに応じて何かを一致させたい場合に役立ちます。

単純な正規表現の場合は、同様のことを手動で行うことができます。つまり、コンテキストに関係なくすべてに一致させ、ループ内でコンテキストによってフィルタリングします。

str.match (フラグ g なし) と str.matchAll (常に) は、index プロパティを持つ配列として一致を返すため、テキスト内の正確な位置がわかり、コンテキストを確認できます。

しかし、一般的にルックアラウンドの方が便利です。

ルックアラウンドの種類

パターン 種類 一致
X(?=Y) 正の先読み Y が後続する場合の X
X(?!Y) 負の先読み 負の先読み
Y が後続しない場合の X (?<=Y)X 正の後読み
Y の後の X (?<!Y)X 負の後読み

Y の後ではない X

負でない整数を見つける

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

負でないもの(ゼロは許可)のみを探す正規表現を作成します。

let regexp = /your regexp/g;

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

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

解答

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

負の後読みを前に付けることで負の数を除外できます: (?<!-)\d+.

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

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

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

ただし、今試してみると、もう1つ「余分な」結果に気付くかもしれません。

ご覧のとおり、-18 から 8 に一致します。これを除外するには、正規表現が別の(一致しない)数字の途中から数字のマッチングを開始しないようにする必要があります。

別の負の後読みを指定することでこれを行うことができます: (?<!-)(?<!\d)\d+。これで (?<!\d) により、一致が別の数字の後に開始されないことが保証されます。まさに必要なことです。

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

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

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

ヘッドの後に挿入

HTMLドキュメントを含む文字列があります。

<body> タグの直後に <h1>Hello</h1> を挿入する正規表現を書いてください。タグには属性が含まれている場合があります。

let regexp = /your regular expression/;

let str = `
<html>
  <body style="height: 200px">
  ...
  </body>
</html>
`;

str = str.replace(regexp, `<h1>Hello</h1>`);

例えば

<html>
  <body style="height: 200px"><h1>Hello</h1>
  ...
  </body>
</html>

その後、str の値は次のようになります。

<body> タグの後に挿入するには、まずそれを見つける必要があります。 dafür können wir das reguläre Ausdrucksmuster <body.*?> verwenden.

このタスクでは、<body> タグを変更する必要はありません。その後ろにテキストを追加するだけです。

let str = '...<body style="...">...';
str = str.replace(/<body.*?>/, '$&<h1>Hello</h1>');

alert(str); // ...<body style="..."><h1>Hello</h1>...

これがその方法です。

置換文字列 $& は、一致自体、つまり <body.*?> に対応するソーステキストの部分を意味します。それはそれ自体と <h1>Hello</h1> に置き換えられます。

let str = '...<body style="...">...';
str = str.replace(/(?<=<body.*?>)/, `<h1>Hello</h1>`);

alert(str); // ...<body style="..."><h1>Hello</h1>...

別の方法は、後読みを使用することです。

ご覧のとおり、この正規表現には後読み部分しかありません。

  • これは次のように機能します。
  • テキスト内のすべての位置で。
  • それが <body.*?> が先行しているかどうかを確認します。

そうであれば、一致があります。

タグ <body.*?> は返されません。この正規表現の結果は文字通り空の文字列ですが、<body.*?> が先行する位置でのみ一致します。

そのため、<body.*?> が先行する「空行」を <h1>Hello</h1> に置き換えます。それが <body> の後の挿入です。

チュートリアルマップ

前のレッスン次のレッスン

コメント
  • コメントする前にこれを読んでください…
  • 改善のための提案がある場合は、コメントする代わりにGitHubのissueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。