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の必要性に迫られる機会が少ない。
今回の例は,良い練習になったのではないだろうか?