knowledge base

マークアップ/フロントエンドエンジニアのWEB制作における備忘録です。平日はWEB屋、休日は社会人劇団の主宰・劇作家をしています。

正規表現の後方参照まとめ

使用するメソッド

置換などで活躍する正規表現ですが、サブマッチ文字列を参照して利用する(後方参照)場面も時おりあります。

個人的にも理解が曖昧な部分があったため、整理がてらまとめました。

正規表現によるマッチングを行うには、RegExp.exec とString.matchがあります。

目的によってこの両者を使い分けます。 挙動の大きな違いは、正規表現内にgオプションがついているかいないか。 gオプションがなければ同じ挙動をします。(ある場合の挙動の違いは後述します) これらのメソッドの特徴は次の通り。

  1. サブマッチ文字列はRegExp.$nに自動的に代入されている
  2. 戻り値はマッチング結果を含んだ配列
  3. マッチングしなかったらnullが返る

RegExp.$nは使用しない

サンプルなどでよく見かけるやりかたに、RegExp.$nがありますが、実のところ推奨されない方法だそうです。

[ex. 任意のアルファベットが1回以上、任意の数字が1回以上出現する文字列を探す]

var target = 'abc123';
if(target.match(/([a-z]+)([0-9]+)/)){
    console.log(RegExp.$1); // 'abc'が出力される
    console.log(RegExp.$2); // '123'が出力される
}else{
    console.log('no match');
}

RegExp.$nとは()で分けられたサブマッチ文字列が格納される変数のようなもので(正確にはオブジェクトのプロパティ)、nには1から始まる整数が入ります。

([a-z]+)は左から1番目なのでRegExp.$1、([0-9]+)は左から2番目なのでRegExp.$2にそのマッチング結果が格納されています。

直感的で使いやすいのですが、以下の理由からRegExp.$nは使わないほうがよいそうです。

[ex. 任意のアルファベットが1回以上、任意の数字が1回以上出現する文字列を探す]

var target = 'abc123';
if(target.match(/([a-z]+)([0-9]+)/)){
 /* ここで正規表現を使う処理が加わるとRegExp.$nに異なる結果が代入される */
    console.log(RegExp.$1); // 'abc'が出力されない場合がある
    console.log(RegExp.$2); // '123'が出力されない場合がある
}else{
    console.log('no match');
}

RegExp.$nはマッチングのたびに生成されるものではありません。

一つしか存在しない変数のようなものだと思ってください。

修正が加わらないと決まっているソースでもない限り、拡張性に乏しい方法であるといえます。

メソッドの戻り値を利用する

実は(というか先述しているのですが)メソッドの戻り値として、マッチング結果を含んだ配列(厳密にはArrayオブジェクト)が返されます。

この配列を専用の変数にいちど格納してから、参照するという方法がよいそうです。 RegExp.execとString.matchともに戻り値の配列には次のような値が格納されています。

※ gオプションがない場合のみ 戻り値をArrayという名前の変数で受け取ったと仮定します。

  • Array[0]・・・マッチした文字列全体
  • Array[1]以降・・・()でグループ化したサブマッチ文字列
  • Array.index・・・マッチした場所。0から数えて何字目か
  • Array.input・・・検索対象の文字列

これを踏まえて、先ほどの例と同じ挙動をするには次のようにするとよいそうです。

var target = 'abc123';
var result = [];
if(result = target.match(/([a-z]+)([0-9]+)/)){
    console.log(result[0]); // 'abc123'が出力される
    console.log(result[1]); // 'abc'が出力される
    console.log(result[2]); // '123'が出力される
}else{
    console.log('no match');
}

仮にマッチしなかった場合、変数resultにはnullが格納されて暗黙的なfalseとなるため、'no match'の文字列が出力されます。

このようにすれば、仮に正規表現を用いる処理が追加されたとしても、変数resultの値が上書きされない限り出力結果に影響が及ぶことはありません。

ちなみにRegExp.execではこのように書きます。

var target = 'abc123';
var regexp = new RegExp('([a-z]+)([0-9]+)');
var result = [];
if (result = regexp.exec(target)){
    console.log(result[0]); // 'abc123'が出力される
    console.log(result[1]); // 'abc'が出力される
    console.log(result[2]); // '123'が出力される
}else{
    console.log('no match');
}

gオプションを含む場合

挙動の違いは下記の出力結果をご覧ください。 まずは、String.matchのケースから。

var regexp = /([A-N])([A-N]{2})[A-N]/g;
var target = "ABCDEFGHIJKLMN";
var result = [];
if(result = target.match(regexp)){
    console.log(result[0]); // 'ABCD'が出力される
    console.log(result[1]); // 'EFGH'が出力される
    console.log(result[2]); // 'IJKL'が出力される
}else{
    console.log('no match');
}

String.match()は一度で行末まで検査するため、サブマッチ文字列は戻り値の配列には格納されていません。

もちろんindexやinputも参照できません。

ただし、RegExp.$1には'I'が、RegExp.$2には'JK'が格納されているようです。

最後にマッチした部分文字列が入るようなので、たとえば([A-N]{2})のマッチ結果を次々と求めるには別途ロジックを組む必要があります。

次に、RegExp.exec()のケースを見てみます。

var regexp = /([A-N])([A-N]{2})[A-N]/g;
var target = "ABCDEFGHIJKLMN";
var result = [];
if(result = regexp.exec(target)){
    console.log(result[0]); // 'ABCD'が出力される
    console.log(result[1]); // 'A'が出力される
    console.log(result[2]); // 'BC'が出力される
    console.log(result.index); // '0'が出力される
}else{
    console.log('no match');
}

RegExp.execは、一度で行末まで検査することはありません。

挙動はgオプションなしの場合とほとんど変わらず、サブマッチ文字列の取得は容易です。

次の例をご覧ください。

var regexp = /([A-N])([A-N]{2})[A-N]/g;
var target = "ABCDEFGHIJKLMN";

var result1 = regexp.exec(target); // ["ABCD", "A", "BC"]
var index1 = result1.index;  // 0

var result2 = regexp.exec(target); // ["EFGH", "E", "FG"]
var index2 = result2.index;  // 4

var result3 = regexp.exec(target); // ["IJKL", "I", "JK"]
var index3 = result3.index;  // 8

唯一gオプションなしの場合と異なるのは、indexが更新されるという点です(どうやら正規表現オブジェクトのlastIndexという値が更新されているようです)。

ということで、サブマッチ文字列を行末まで検査するには次のようになります。

今回は([A-N]{2})を取り出します。

var regexp = /([A-N])([A-N]{2})[A-N]/g;
var target = "ABCDEFGHIJKLMN";
var result = [];
while (result = regexp.exec(target)) {
    console.log(result[2]); // 順に'BC','FG','JK'が出力される
}

とりあえず、String.match()は一度の実行で行末まで検査するのに対して、RegExp.execは何度も実行しないと行末まで検査しないという認識でよさそうです。

ちなみにgオプションありのサンプルコードはこちらから拝借しました。

d.hatena.ne.jp

ありがとうございました。