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)で返却された配列の中身をみると
- m[0] : "http://a.com/"
- m[1] : "https://b.jp/"
となっている。
つまり,グローバルオプションは正常に機能している。
マッチするものを全て結果に含めているからだ。
四章第三回 正規表現2 — JavaScript初級者から中級者になろう — uhyohyo.net
http://uhyohyo.net/javascript/4_3.htmlgオプションというものがあります。
このgオプションがついていると、その正規表現オブジェクトを使ってマッチングしたとき、複数回マッチします。
つまり,当てはまるところ全てにマッチします。
問題は,括弧でキャプチャできない,ということ。
ほんとうは「com」と「jp」の部分だけを返したい。
しかし,それができない。
実はこれは,match()というメソッドの仕様だ。
String.match() で正規表現にグローバルオプションを付けると,
正規表現「全体で」マッチを試みて,
マッチした結果がすべて配列の中に格納して返却される。
その際に,括弧でキャプチャした結果は
配列の中に含まれない。
match()でグローバルオプションを付けると,
括弧で部分文字列をキャプチャすることは不可能,ということ。
[JavaScript]String.matchとRegExp.execと後方参照 - chalcedonyの外部記憶装置・出張版
http://d.hatena.ne.jp/chalcedony_htn/20090315/1237121111gオプションありの場合の動作(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() の方を使うべし。