uhyohyo.net

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

四章第三回 正規表現2

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

正規表現の利用

正規表現の利用方法として前回紹介したのはtestメソッドでした。これはある文字列がパターンにマッチするかどうかを判定するだけでした。実は、正規表現の利用法は他にもあります。

match

matchというメソッドがあります。これは、前回のtestと同じように、正規表現と文字列をマッチング(マッチさせること)するものです。

ただし、testの場合とは逆で文字列のメソッドであり、引数に正規表現を指定します。つまり、文字列.match(正規表現)という形です。

testの返り値はマッチしたかどうかの真偽値でしたが、matchの戻り値は違います。

マッチしなかった場合matchの戻り値はnullであり、マッチした場合の戻り値は配列です。この配列の0番目の要素には、マッチした文字列が入っています。

マッチした文字列とはどういうことかというと、例えば、a.{5}cという正規表現が、


aabcbcacbuccb
 ^^^^^^^

という部分にマッチしたとき、この配列の0番目には"abcbcac"が入っています。実際、次のコードを実行すると["abcbcac"]という配列が表示されます。


console.log("aabcbcacbuccb".match(/a.{5}c/));

matchにはさらに機能があります。それは前回解説したグループ化と関わりがあります。グループ化とは、一部分を括弧で囲んでひとまとめにすることでした。グループ化の方法は( )で囲む方法と(?: )で囲む方法がありました。

実は、前者の( )で囲むグループ化が正規表現に存在するとき、マッチ時にグループ化した部分だけを個別に取得することができます。例えば、上のa.{5}cを、a(.{5})cとした場合、括弧で囲んだ.{5}の部分だけを、マッチした全体とは別に取得できます。

つまり、この正規表現が

aabcbcacbuccb
 ^^^^^^^

という部分にマッチしたとき、matchの返り値である配列の0番目の要素はマッチした全体である"abcbcac"です。さらに、配列の1番目にはグループ化された.{5}に対応する部分、すなわち"bcbca"が入っています。

次のサンプルで確かめてみましょう。

var str = "aabcbcacbuccb";
var result = str.match(/a(.{5})c/);
console.log(result[0]); // "abcbcac"
console.log(result[1]); // "bcbca"

ちなみに、

a(.{5})c(.{3})

のように括弧が2つ以上ある場合も、1つめの括弧の中は1番目の要素に、2つめは2番目……のようにいくつでも取得することができます。

このように、グループ化した部分を別に取得することをキャプチャと言います。結局のところ、グループ化に( )(?: )の2種類があった理由は、前者はキャプチャするグループ、後者はキャプチャしないグループという使い分けのためです。キャプチャが目的でないグループ化には(?: )を使いましょう。そのほうが、キャプチャの処理が発生しない分だけ( )よりも速度やメモリ使用量の面で有利と考えられます。

matchの利用例

さて、このmatchはかなり応用がきくメソッドで、正規表現を扱う際はtestと並んでよく使うメソッドです。たとえば、'<img src="aaa.gif">'のような、開始タグの文字列があったとして、ここからタグ名(今回の場合"img")だけを抜き出したいとします。このとき、どのようにしたらいいと思いますか?

matchを利用して、正規表現を使えばいいのです。いくらでもちゃんとやりようはありますが、例えば次の正規表現を使います。

<(\S+)\s+.+>

<があって、その次に\S(=空白文字以外)がいくつかあって、その次に空白がいくつかあって、さらに次には任意の文字がいくつかあって、最後に>がある」という意味の正規表現ですね。

上のタグの場合だと、<の部分が最初の<で、次のimg(\S+)に当てはまり、次のスペースが\s+になります。

src="aaa.gif"の部分は.+が、最後の>には>が当てはまることになります。括弧でグループ化されているのはimgの部分だから、うまくいってますね。

今の正規表現は簡単すぎて片手に収まらないほど問題がありますが、例なのであまり気にしないでください。正規表現を利用するときは自らの需要に合わせて正規表現を作りましょう。

replace

ではもう1つメソッドを紹介します。それはreplaceです。これは、正規表現で検索して、マッチした部分を別の文字列に置き換えるというメソッドです。

文字列.replace(正規表現オブジェクト,置き換え後文字列)

返り値は、置き換えた後の文字列です。

サンプルは、


var str = "aaa1234bcd567efg8999h";		//もともとの文字列
var result = str.replace(/\d+/, "数字");	//置き換え後の文字列
console.log(result); // "aaa数字bcd567efg8999h"

という感じです。replaceの引数は、1つめが/\d+/で、2つめが"数字"ですね。

この正規表現オブジェクトは、\dが「数字1文字」を表すから、それが1つ以上ということなので、1234、555、0、29……といった、数字が何文字か連なったものにマッチします。

マッチしたところが2つめの引数に置き換わるということは、例えば

aaa1234bcd567efg8999h
   ^^^^
   

の部分がマッチしたとき、その部分が

aaa数字bcd567efg8999h

のように置き換わるということです。結果、できあがる文字列は"aaa数字bcd567efg8999h"ということになります。

ちなみに、「567」や「8999」の部分も\d+の条件には当てはまるのですが、今回置き換えられたのは1234の部分だけです。その理由は、このような場合replaceは最初にマッチしたひとつしか置き換えないためです。また、マッチするときは文字列の前のほうから見ていくため、今回最初にマッチするのは「1234」となります。

正規表現のフラグ

実は、正規表現に対してはフラグというものを設定することができます。正規表現にフラグを設定すると、細かい動作が変わります。

iフラグ

iフラグというものがあります。このフラグがあると、マッチするときにアルファベット等の大文字と小文字を区別しなくなります。つまり、例えば/abc/という正規表現が、AbCというような文字列にもマッチするようになります。

では、iフラグはどうやってつけるかというと、正規表現オブジェクトを作るときに/正規表現/iとします。正規表現の終わりを示すスラッシュの後に、iという文字がつきました。これで、iフラグがつきます。

これは、"ABCDE".match(/abc/i)のように使えます。

gフラグ

もう1つ、gフラグというものがあります。つけ方はiフラグと同じで、スラッシュの後ろに付けます。両方のオプションを付けることもできて、その場合は/正規表現/igのように並べます(フラグの順番は自由)。

このgフラグがついていると、その正規表現オブジェクトを使ってマッチングしたとき、複数回マッチします

複数回マッチするというのがどういうことかは、メソッドによって変わります。一番わかりやすいのはさっき紹介したreplaceメソッドです。

先ほど、replaceは最初にマッチした部分のみ置き換えると説明しましたが、それは正規表現にgフラグがない場合です。gフラグがある場合、マッチする部分全てを置き換えるという動作に変わります。つまり、次のようになります。


var str = "aaa1234bcd567efg8999h";		//もともとの文字列
var result = str.replace(/\d+/g, "数字");	//置き換え後の文字列
console.log(result); // "aaa数字bcd数字efg数字h"

先ほどとは異なり、1234の部分だけでなく567と8999も"数字"に置き換わっています。

次に、matchメソッドは、返ってくる配列の中身が変わります。gオプションがついていない場合は、上で解説したように0番目の要素にはマッチした文字列全体が、1番目以降にはグループ化した箇所の文字列が入っているのでした。

これが、gフラグがつくと、マッチした文字列が、0番目から順番に入っているようになります。つまり、

"aaa1234bcd567efg8999h".match(/\d+/g)

で返ってくる配列は["1234", "567", "8999"]となります。なぜなら、この文字列に対して

aaa1234bcd567efg8999h
   ^^^^   ^^^   ^^^^
 

のようにマッチするから、です。

matchでgフラグを使う場合、キャプチャは行われなくなります。

最後に前回紹介したtestですが、このメソッドはただマッチするかどうかを調べるだけなので、gフラグがついても動作は変わりません。

ここで紹介したiとgが代表的なフラグです。他にm、y、uがありますがここでは説明しません。

replaceとグループ化

上で、replaceでマッチした部分を別の文字列に置き換えるということを解説しました。実は、置き換え後の文字列をマッチした文字列に合わせて変えることが可能です。それには2つの方法があります。

例えば、上ではreplaceで\d+にマッチする部分を"数字"に置き換えましたが、これを、置き換える代わりにもとの数字を残して"数""字"で囲みたいとしましょう。例えば、"1234"という文字列にマッチしたなら、これを"数1234字"というふうに置き換えたいとします。

このときは、次のようにします。

"aaa1234bcd567efg8999h".replace(/(\d+)/g, "数$1字");

置き換え後の文字列に中にある$1という部分がポイントです。replaceの置き換え後文字列中に$ + 数字の記法があると、これはキャプチャされた文字列に置き換わります。$1は1番目の(つまり最初の)グループ化に対応します。グループ化が2つ以上あるなら、$2、$3、……と対応します。

また、$&とすると、これはマッチした部分の全体になります。つまり、今回の場合はグループ化を使わなくても同じことが次のようにできます。

"aaa1234bcd567efg8999h".replace(/\d+/g, "数$&字");

今回のサンプルではgオプションをつけたので、当てはまるところ全てにマッチして結果は"aaa数1234字bcd数567字efg数8999字h"となります。

もう1つの方法は、replaceの第2引数に置き換え後の文字列ではなく関数を与える方法です。この方法では、マッチした文字列をどう置き換えるかを関数が決定します。そのため、より柔軟な処理が可能です。まず、上のサンプルと同じことを関数でやってみると、次のようになります。


"aaa1234bcd567efg8889h".replace(/\d+/g, function(str){
  return "数" + str + "字";
}); // "aaa数1234字bcd数567字efg数8889字h"

第二引数には、

function(str){
  return "数" + str + "字";
}

という関数が渡されていることに注意してください。この関数は、引数strを受け取って、その前後を"数""字"で囲った文字列を返します。

察しがついているとは思いますが、正規表現にマッチした文字列があったときにこの関数が呼び出されます。その際、引数にマッチした部分の文字列が渡されます。そして、マッチ部分はその関数呼び出しの返り値で置き換えられます。

ちなみに、関数を用いる場合もグループ化によりキャプチャした部分を得ることが可能です。それは関数の第2引数以降に渡されます。例えば、今の置き換えの逆を行うには次のようにします。


"aaa数1234字bcd数567字efg数8889字h".replace(/数(\d+)字/g, function(str, num){
  return num;
}); // "aaa1234bcd567efg8889h"

この場合、正規表現/数(\d+)字/g"数1234字"にマッチして関数が呼び出されたとすると、引数strには"数1234字"が入り、numには1番目のグループでキャプチャされた"1234"が入ります。

最後に、関数ではないとできない例を紹介しておきます。マッチした数値を2倍にしてみましょう。


"aaa1234bcd567efg8889h".replace(/\d+/g, function(str){
  return parseInt(str) * 2;
}); // "aaa2468bcd1134efg17778h"

この例では、数値を2倍にするために掛け算を使っていますが、parseInt(基礎第4回で紹介)が必要な点に注意しましょう。掛け算は数値に対して行われますが、引数strはマッチした文字列が渡されるので、文字列を数値に変換しないといけません。なお、この例では関数の返り値が数値になってしまいますが、そのような場合は自動的に文字列に変換されます。

最長マッチと最短マッチ

最後に少し正規表現の記法に戻って、最長マッチと最短マッチというものについて解説します。

例えば、上で解説したreplaceを使って" 〜 "で囲まれている部分を「 〜 」で囲むように置き換える処理を作ってみたいと思います。

" 〜 "で囲まれている部分を表す正規表現は、次のようになりますね。

/".*"/

最初と最後に"があって、その間は.*だから、任意の文字が何個かあるか、もしくはないということです。

囲まれている部分は置き換え時に使うので、

/"(.*)"/

のようにグループ化します。replaceで実際に置き換えるには、文字列.replace(/"(.*)"/g, "「$1」")とします。" 〜 "で囲まれた部分が複数あった場合も全て置き換えたいので、今回はgオプションをつけてみました。

実はこれでは問題があります。実際に" 〜 "で囲まれた文字列が2つある'"aaaaa". "bbbbb".'という文字列を置き換えるとどうなるでしょうか。


'"aaaaa".  "bbbbb".'.replace(/"(.*)"/g, "「$1」"); // '「aaaaa".  "bbbbb」.'

期待していた結果は'「aaaaa」. 「bbbbb」.'となることですが、実際の結果は'「aaaaa". "bbbbb」.'となってしまいます。

理由は、正規表現中の.*の部分が最長マッチになっていることです。最長マッチとは、なるべく長くマッチするということです。

今回の例では、".*"はまず文字列中の

"aaaaa".  "bbbbb".
^^^^^^^
 

の部分にマッチしてほしかったわけですが、この位置から始まるマッチとしてはこれは最長ではありません。実際、次のほうがより長くマッチできます。

"aaaaa".  "bbbbb".
^^^^^^^^^^^^^^^^^

これを期待通りの動作にする方法は2つあります。まず、今回説明したい方から解説します。

それは、この最長マッチという性質を変えるという方法です。理想的なのは、なるべく短くなるようにマッチするというような性質です。この性質は、最短マッチなどとよばれます。

実は、最長マッチをこの最短マッチに変える方法はちゃんとあります。それは、+*などの、繰り返しを表す記号の直後に、?をつけます。注意してほしいのが、この?は、前回紹介した「直前の文字が0回または1回」という意味の?とは別物であるということです。あくまで、+*などの繰り返しの記号のあとに?をつけるのです。

つまり、/"(.*?)"/という正規表現になります。

今回のサンプルは、これで期待通りの結果になります。


'"aaaaa".  "bbbbb".'.replace(/"(.*?)"/g, "「$1」"); // '「aaaaa」.  「bbbbb」.'

さて、2つめの方法ですが、こっちはけっこう単純です。

.*の部分が、任意の文字だからといって、"が出てきてもそれも含んでどんどん先までマッチしてしまったことが期待通りにならない原因なので、この.が0回以上繰り返すという部分を、"以外の文字が0回以上繰り返すというようにすればいいのです。

すると、この正規表現がマッチする" 〜 "の間の文字に"が含まれることはなくなるので、期待通りの動作になります。


'"aaaaa".  "bbbbb".'.replace(/"([^"]*)"/g, "「$1」"); // '「aaaaa」.  「bbbbb」.'

正規表現の説明は以上です。