スポンサーリンク

JavaScriptの動かないコード(中級編) 正規表現の括弧キャプチャを,グローバルで繰り返しmatchできない (gオプションを付けると部分文字列の抽出が無効)


以下のJavaScriptコードが,意図した動作をしないのは,なぜですか。(制限時間1分)


やりたい事:

  • (試行1)では,文字列からトップレベルドメインを1つだけ抽出する。
    • これは上手くいく。
  • (試行2)では,文字列からトップレベルドメインを全て抽出する。
    • これは正常に動作しない。なぜなのか。

<script>

// 元の文字列
var str = "URLは,http://a.com/ と,https://b.jp/ です。よろしくね。";


/*
  (試行1)
  正規表現にgオプションを付けず,
  情報を1個だけ抽出してみる。
*/


// トップレベルドメインを抜き出す。
var m;
m = str.match( new RegExp( "https?://[^\\/]+\\.([^\\.\\/]+)/" ) );

	// httpとhttpsの両方に対応。
	// ドメイン名の最後の部分だけを,カッコで抜き出す。
	// つまり,ドメイン中の最後のドット以降の文字列を抜き出す。


// 配列の0番目には,マッチした文字列全体が入っている。
//alert( m[0] );  =>  "http://a.com/"

// 配列の1番目には,括弧でキャプチャした部分が入っている。
alert( "トップレベルドメインは" + m[1] );  // => "com"



/*
  (試行2)
  正規表現にgオプションを付けて,
  マッチした情報を全て抽出してみる。
*/


// 正規表現にgオプションを付ける。
m = str.match( new RegExp( "https?://[^\\/]+\\.([^\\.\\/]+)/", "g" ) );

// 括弧でキャプチャした部分を2つとも表示。
alert( "1つ目にマッチしたトップレベルドメインは" + m[1] );
alert( "2つ目にマッチしたトップレベルドメインは" + m[2] );


</script>

発生する不具合

試行1では,意図した通りに正規表現が動き,

「com」というトップレベルドメイン(TLD)を抜き出せる。

抽出成功だ。


しかし,試行2では

  • 1つ目にマッチしたトップレベルドメインはhttps://b.jp/
  • 2つ目にマッチしたトップレベルドメインはundefined

と表示されてしまう。


正規表現にg,つまりグローバルオプションを付けたら

文字列の中でマッチするものを全て拾ってくれるはずだ。


なのに,「全てマッチ」できておらず,

括弧によるキャプチャもできていない。


どうすればよいだろうか。

不具合の原因: String.matchにgオプションを付けると,括弧でキャプチャできない

前述のコードで,(試行2)で返却された配列の中身をみると

となっている。


つまり,グローバルオプションは正常に機能している

マッチするものを全て結果に含めているからだ。

四章第三回 正規表現2 — JavaScript初級者から中級者になろう — uhyohyo.net
http://uhyohyo.net/javascript/4_3.html

gオプションというものがあります。

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

つまり,当てはまるところ全てにマッチします。


問題は,括弧でキャプチャできない,ということ。

ほんとうは「com」と「jp」の部分だけを返したい。

しかし,それができない。


実はこれは,match()というメソッドの仕様だ。


String.match() で正規表現にグローバルオプションを付けると,

正規表現「全体で」マッチを試みて,

マッチした結果がすべて配列の中に格納して返却される。


その際に,括弧でキャプチャした結果は

配列の中に含まれない。


match()でグローバルオプションを付けると,

括弧で部分文字列をキャプチャすることは不可能,ということ。

[JavaScript]String.matchとRegExp.execと後方参照 - chalcedonyの外部記憶装置・出張版
http://d.hatena.ne.jp/chalcedony_htn/20090315/1237121111

gオプションありの場合の動作(String.match):

返り値は、「マッチしたすべての文字列全体」の配列。

要するに、部分文字列は配列に含まれないということ。


この仕様を知らないと,

返却される配列をforループでスキャンしても

何一つ得られず悩む・・・ということがある。


カッコのキャプチャを複数回やりたい場合,

matchは使えない。

では,かわりにどうすればよいか?

対処法: RegExp.exec() を使え

match() のかわりに,exec() を使おう。

matchより少し面倒だけど・・・。


exec()なら,グローバルで括弧キャプチャできる。

<script>

// 元の文字列
var str = "URLは,http://a.com/ と,https://b.jp/ です。よろしくね。";


/*
  (試行2)改良版。
  マッチするすべてのTLDを列挙。
*/


// 正規表現にgオプションを付ける。
var re = new RegExp( "https?://[^\\/]+\\.([^\\.\\/]+)/", "g" );

// execメソッドが値を返し続ける限り,
// whileでループを回し続ける。
var arr;
while( 
	// execの結果は毎回,配列として受け取る。
	( arr = re.exec( str ) )
	!=  
	null
){

	alert( "正規表現全体でマッチした文字列は" + arr[0] );

	// 括弧でキャプチャした文字列も入っている。
	alert( "トップレベルドメインは" + arr[1] );

}


</script>

while文を書くのに抵抗があるかもしれないが,

これも必要なことなので,涙をのんで受け入れよう。

【Javascript】正規表現でgフラグを付けても phpのpreg_match_all()のようなことができない at softelメモ
https://www.softel.co.jp/blogs/tech/archives/2972

  • Javascriptの正規表現のmatch()はgフラグをつけているかどうかで戻り値が違う。
    • String.match でgをつけると,キャプチャはどうしようもない。
  • なので、 正規表現で “g” フラグを使用する場合、同じ文字列で成功するマッチを見つけるために exec メソッドを複数回使う


JavaScript の正規表現についてまとめてみた | JavaScript | daily memorandum 3.0.0
http://lovepeers.org/2014/06/24/javascript-regex/

  • 正規表現が”g”フラグを含んでいる場合、マッチしたものすべてを含む配列を返す。
    • この時、()でマッチさせた部分文字列を取り出すことは出来なくなる。
  • ”g”フラグと()による部分文字列の取り出しを同時に使う必要がある場合は、RegExp.exec() の方を使うべし。