uhyohyo.net

JavaScript初級者から中級者になろう

質問と回答

寄せられた質問に対して回答します。

String#matchの複数マッチについて

回答日:

String#matchでgオプションをつけると、返り値でグループ化の情報が落ちる。複数マッチさせた上グループ化の情報も得たい。

String#matchは四章第三回で解説しました。このときは、matchの戻り値について、グループ化した部分などは、配列から得られなくなりますと解説しました。

これは仕様ですのでどうしようもありませんから、複数マッチの場合はひとずつつループを回して対処します。

普通にループで回す方法

一つの方法は、1回のmatchで1つずつマッチさせて、その部分を文字列から取り除いて再びマッチさせる方法です。マッチさせる文字列が破壊されますので、コピーしてから行いましょう。

//携帯電話の番号(090-xxxx-xxxx)にマッチさせた上、前半と後半に分けるサンプル
var str="090-1234-5678 , 090-8888-8888 / 090-9876-5432 090-0000-0000";

var result;	//matchの結果を入れる変数

while(result = str.match(/^.*?090-(\d{4})-(\d{4})/)){
  //もうマッチしない場合はresultにnullが入るのでループが終わる
  console.log(result);	//何かに使用する(ここでは表示するだけ)

  str = str.slice(result[0].length);	//strから、マッチした部分を取り除く
}

出力結果は次のような感じです。

["090-1234-5678", "1234", "5678"]
[" , 090-8888-8888", "8888", "8888"]
[" / 090-9876-5432", "9876", "5432"]
[" 090-0000-0000", "0000", "0000"]

ポイントは、matchの引数の最初についている^.*?です。^というのは文字列の一番最初からマッチせよという意味で、.は任意の文字で、*は0回以上の繰り返し、そして*についた?は最短マッチです。最短マッチというのは、なるべく短くマッチするということですから、.が何にでもマッチするとはいえ、マッチすべき部分がその中に含まれてしまう(見逃してしまう)ことはありません。

そして結果の配列の1番目と2番目に欲しい情報が入っていることがわかります。

配列の0番目を見ると、マッチさせたい090-の部分の前にごちゃごちゃと何かが入っているのが分かります。これが.*?にマッチした部分です。ちなみに、最短マッチの?がないと以下のような結果になってしまいます。

["090-1234-5678 , 090-8888-8888 / 090-9876-5432 090-0000-0000", "0000", "0000"]

.*?の部分に全部含まれてしまい(?がないと最長マッチになってなるべく長く含めようとするため)、一番最後のしか得られません。

そして最後の

str = str.slice(result[0].length);

ですが、sliceは四章第一回で解説しています。

sliceは、文字列の(第一引数)番目から(第二引数)番目まで抜き出すという意味ですが、第二引数を省略した場合は最後までです。

ここで,result[0]というのはさっき見た通り、今回マッチした部分の全体です。つまり、ここはもうマッチしたから要らないわけです。

さらに、今回の正規表現には最初に^がついていますから、result[0]には文字列の最初から何文字かが入っていることが保証されています。

ですから、result[0]の文字のぶんだけ文字列の最初を取り去ってしまえばいいのです。以上がこのサンプルの動作の説明です。

matchの結果のindexを利用する方法

上の方法は、.*?がなんか邪魔です。そこでこれを用いない方法も紹介します。

//携帯電話の番号(090-xxxx-xxxx)にマッチさせた上、前半と後半に分けるサンプル
var str="090-1234-5678 , 090-8888-8888 / 090-9876-5432 090-0000-0000";

var result;	//matchの結果を入れる変数

while(result = str.match(/090-(\d{4})-(\d{4})/)){
  //もうマッチしない場合はresultにnullが入るのでループが終わる
  console.log(result);	//何かに使用する(ここでは表示するだけ)

  str = str.slice(result.index+result[0].length);	//strから、マッチした部分を取り除く
}

結果は以下のとおりです。

["090-1234-5678", "1234", "5678"]
["090-8888-8888", "8888", "8888"]
["090-9876-5432", "9876", "5432"]
["090-0000-0000", "0000", "0000"]

このソースがさっきと違う点は、正規表現から余計な部分が消えたという点と、sliceの引数にresult.indexを足しているという点です。

結果を見ても、result[0]も余計なものがくっついていなくて使いやすくなっています。

ところで、resultというのはstr.matchの戻り値ですから、配列のはずですが、実はString#matchの戻り値の配列にはindexという特別なプロパティが追加されています(実は、もう一つinputというのもありますが紹介しません)。

このindexというのは、文字列の最初から数えて何番目にマッチしたかということです。例えば、

"12345abcde67890"
     ~~~

という文字列で"abc"にマッチさせた場合、

var result="12345abcde67890".match(/abc/);
console.log(result.index);	//5

abcの最初の文字aがあるのは5番目ですから(最初が0番目であることに注意)、indexプロパティには5が入っています。

これを用いれば、ここで示されたindexより前の部分はマッチしなかったのですからもう要りません。それに、今回マッチしたresult[0]の部分の長さを足して、その部分を捨ててしまおうというのが、

result.index+result[0].length

の意味です。

Regexp#execとlastIndexを使う方法 おすすめ

lastIndexとは、正規表現オブジェクトがもつプロパティです。これを用いると次のように書けます。

//携帯電話の番号(090-xxxx-xxxx)にマッチさせた上、前半と後半に分けるサンプル
var str="090-1234-5678 , 090-8888-8888 / 090-9876-5432 090-0000-0000";

var regexp= /090-(\d{4})-(\d{4})/g;
var result;	//matchの結果を入れる変数

while(result = regexp.exec(str)){
  //もうマッチしない場合はresultにnullが入るのでループが終わる
  console.log(result);	//何かに使用する(ここでは表示するだけ)
}

この方法は、str.sliceを用いてstrを書き換える必要がなくなり、とてもすっきりしています。

ポイントは、execというメソッドです。これはtestと同じく正規表現オブジェクトがもつメソッドで、String#matchと同じです。

ただし、見ての通り、String#matchとは、文字列と正規表現オブジェクトの関係が逆になっています。

また、正規表現にgオプションがついている場合は2つの挙動は変わります。String#matchのほうは前に紹介した通り、複数のマッチ結果を全て配列にして返します。

Regexp#execのほうは、gがついても挙動はそれほど変わりません。ただし、gがある場合、正規表現オブジェクトのlastIndexプロパティが変化します。lastIndexというのは、次回のマッチを開始する位置で、最初は0です。

そしてこのlastIndexが、ちょうど今回マッチした部分の次の位置にセットさせるのです。

つまり、簡単にいうと、同じ正規表現オブジェクトで(lastIndexを保持するため)、RegExp#execを使ってマッチさせると、マッチさせるたびに次のところがマッチして、全部終わったらちゃんとnullを返してくれます。これは、毎回lastIndexが変化するからで、Regexp#matchをgオプションつきの正規表現オブジェクトで呼び出したときに特有の動作です。