uhyohyo.net

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

質問と回答1

このページの最終更新日:

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

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

回答日:

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

String#match四章第三回で解説しました。このときは、matchの戻り値について、matchでgオプションを使う場合、キャプチャは行われなくなります。と解説しました。

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

普通にループで回す方法

一つの方法は、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);

ですが、これはstrから今回マッチした部分を全て除くという意味です。

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

ここで、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という特別なプロパティが追加されています。

この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

の意味です。

ちなみに、String#matchの返り値の配列にはもうひとつinputというプロパティがあり、これは検索対象の文字列全体です。

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がある場合は、正規表現オブジェクト(今回はregexp)のlastIndexプロパティが変化します。lastIndexというのは、次回のマッチを開始する位置で、最初は0です。

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

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