スポンサーリンク

JavaScriptの動かないコード(中級編)正規表現をwhile内で定義すると無限ループで固まるエラー (execでグローバルなマッチだと,処理がフリーズ)


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


やりたい事:

  • 複数の文字列がある。
  • それぞれの文字列から,URLのトップレベルドメイン(TLD)を全て抜き出す。
  • 抽出のために,RegExp.exec() メソッドで,正規表現にグローバルオプションを付けて実行する。
<script>

// 検索対象の文字列たち
var strs = [
	"URLは,http://a.com/ と,https://b.jp/ です。よろしくね。",
	"どうぞhttp://c.com/においで下さい。",
	"http://d.jp/ から移転しました。こちら→https://e.info/ "
];


/*
  マッチするすべてのTLDを列挙
*/


// 検索対象のすべての文字列を,順番に検査する。
for( var i = 0; i < strs.length; i ++ ){

	// この一つの文字列の中に含まれる
	// 全てのTLDを抽出。
	var str = strs[i];


	// execメソッドが値を返し続ける限り,
	// whileでループを回し続ける。
	var arr;
	while( 
		( 
			// execの結果は毎回,配列として受け取る。
			arr = (
				// 正規表現にgオプションを付け,
				// 文字列内で複数回マッチさせる。
				/https?:\/\/[^\/]+\.([^\.\/]+)/g
			).exec( str ) 
		)
		!=
		null
	){

		// 括弧でキャプチャした文字列を表示
		alert( "トップレベルドメインは" + arr[1] );

	}


}


</script>


発生する不具合

上述のコードを実行すると,

まず「com」というトップレベルドメインが表示される。

1つめのURLのTLDを正しく抽出できている。


しかし,どういうわけか,それが永遠に繰り返される。


ダイアログを抑制すると,

ブラウザがフリーズし,無限ループに陥ってしまう。


execメソッドの使い方は間違っていないはずなのだが,

while文の部分で,意図せぬ問題が起きているようだ。


1回目の判定はうまくいっているが,

2回目以降,処理がスムーズに次へと進んでいないようだ。

不具合の原因

このエラーの原因は,

  • 「正規表現オブジェクトは,生成のタイミングで,状態が初期化されるから」

ということ。


下記の2つは同じ意味を持つ。

  • new RegExp(~, "g")
  • /~/g


前者は,オブジェクトの明示的な定義と初期化。

後者は,正規表現リテラル。


つまり後者の記法であっても,意味は前者となり

新しくnewでオブジェクトを生成しているのと同じ

ということになるのだ。


そうすると,newの時点で,何もかも初期化される。

「いま,文字列の中でどこまでマッチしたか?」

などの情報もクリアされてしまい,はじめからやり直し。


whileループの内部でnewすると,

ループが次に進むタイミングで毎回,最初からやり直しになる。

だから無限ループになったのだ。


whileループの中で,正規表現リテラルを書くな。

これは有名な禁じ手である。


とくに,whileの条件部分に,

変数定義をついでに埋め込むことができるようなレベルの

中級プログラマーが陥りやすい罠だ。

.exec() | JavaScript 日本語リファレンス | js STUDIO
http://js.studio-kingdom.com/javascript/regexp/exec

例:連続したマッチを見つける 。

もし、正規表現で"g"フラグを使用したのであれば、
同じ文字列内の連続一致を検出するために、
execメソッドを複数回使用することができます。
 
 
注意: whileの条件に正規表現リテラル(または正規表現コンストラクタ)を置かないでください。

各反復処理時にlastIndexプロパティが
リセットされる傾向のあるマッチがあると、
無限ループになってしまいます。


JavaScript正規表現メモ。 - こせきの技術日記
http://koseki.hatenablog.com/entry/20090530/JsIdiom
ループの中に,gオプションが付いた正規表現リテラルを書かない。

ループの中でg付きの正規表現をexecすると、無限ループになる可能性がある。

リテラルが「状態を持つ」とは夢にも思わず,

ループ内にうっかり埋め込んだが最後。。。


execメソッドが動く時には,

「どこまでマッチ作業が終わったか」という情報が

正規表現オブジェクトの中に保管され,

その情報をインクリメントしながらexecが動いているのだ。

この点を忘れないように。

[JavaScript]String.matchとRegExp.execと後方参照 - chalcedonyの外部記憶装置・出張版
http://d.hatena.ne.jp/chalcedony_htn/20090315/1237121111
gオプションありの場合の動作(Regexp.exec)

返り値は、最初にマッチした文字列全体+キャプチャした文字列の配列。

ただし、一度実行すると、正規表現オブジェクトに「次回検索開始位置」が設定される(lastIndexプロパティ)。

なので、同じオブジェクトでマッチングを繰り返すと、そのたびに返り値は変わる

対処法

解決策は,正規表現の定義を「whileループの外にくくりだす」こと。


検索したい文字列が変わったら,その都度,正規表現を生成し直す。

なぜなら,その文字列の先頭から検索作業をやり直したいから。


そういうわけで,

「forループの中だがwhileループの外」という位置で

正規表現を生成する必要がある。


正しく動くコードは下記の通り。

<script>

// 検索対象の文字列たち
var strs = [
	"URLは,http://a.com/ と,https://b.jp/ です。よろしくね。",
	"どうぞhttp://c.com/においで下さい。",
	"http://d.jp/ から移転しました。こちら→https://e.info/ "
];


/*
  マッチするすべてのTLDを列挙
*/



// 検索対象のすべての文字列を,順番に検査する。
for( var i = 0; i < strs.length; i ++ ){

	// この一つの文字列の中に含まれる
	// 全てのTLDを抽出。
	var str = strs[i];


	// 正規表現にgオプションを付ける。
	var re = new RegExp( "https?://[^\\/]+\\.([^\\.\\/]+)/", "g" );
		// 無限ループを避けるために
		// whileの外で定義。


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

		// 括弧でキャプチャした文字列を表示
		alert( "トップレベルドメインは" + arr[1] );

	}


}


</script>

二重ループにともなって,

変数の値や状態がどのように遷移するか

厳密に追いかけてみれば,理解できるだろう。


「モノは状態を持つ。」

これはオブジェクト指向プログラミングの基本でもある。


とはいえ,正規表現でサクサクと文字をスクレイピングしてると,

単にmatch()で配列が返るだけのコーディングを繰り返してしまい

whileの必要性に迫られる機会が少ない。


今回の例は,良い練習になったのではないだろうか?