実践JavaScript RegExp編

「プログラミング経験はそこそこだけど、JavaScriptはあんまり」な人向け社内勉強会資料vol.3

Shogo Ohta, 2009-06-30

基礎編

正規表現とは

正規表現(せいきひょうげん、regular expression)とは、文字列の集合を一つの文字列で表現する方法の一つである。正則表現(せいそくひょうげん)とも呼ばれ、形式言語理論の分野では比較的こちらの訳語の方が使われる。正規表現 - WikiPedia

文字列の集合を一つの文字列で表現する→一定のルールに従って(短い)文字列で(長い)文字列を表現する

"regular expression".match(/[a-z]+/g) // ["regular", "expression"]

JavaScriptの正規表現

RegExp - MDCの正規表現における特殊文字より抜粋

\
エスケープシーケンス
^
最初
$
最後
*
直前の文字の 0 回以上の繰り返し
+
直前の文字の 1 回以上の繰り返し
?
直前の文字の0回か1回の繰り返し
.
改行文字(\n、\r、 \u2028、あるいは、\u2029)を除いたあらゆる 1 文字
(x)
x にマッチし、マッチしたものを記憶します
(?:x)
x にマッチしますが、マッチしたものを記憶しません
x(?=y)
x に続いて y が現れる場合にのみ、x にマッチします【肯定的前方先読み】
x(?!y)
x に続いて y が現れない場合にのみ、x にマッチします【訳注: 否定的前方先読み】
x|y
x または y にマッチ
{n}
直前の文字がちょうど n 回現れているものにマッチ
{n,}
直前の文字が少なくとも n 回現れているものにマッチ
{n,m}
直前の文字が少なくとも n 回、多くとも m 回現れているものにマッチ
[xyz]
文字の集合
[^xyz]
文字の集合の否定または補集合
\d
アルファベットにおける数字 (digit character)にマッチします
\D
アルファベットにおける数字以外の文字にマッチします
\n
改行(LF) (linefeed) にマッチ
\r
行頭復帰(CR) (carriage return) にマッチ
\s
スペース (space) 、タブ、改ページ、改行、その他のユニコードでのスペースを含む、単一の空白文字にマッチ
\S
空白以外の単一の文字にマッチ
\t
タブ (tab) にマッチ
\w
アンダースコアを含む、任意のアルファベットの文字にマッチ
\W
アルファベットではない、あらゆる文字にマッチします
\n
その正規表現の (左の括弧から数えて)n 番目の括弧に囲まれた部分にマッチする最後の部分文字列への後方参照 (back reference) です。
\0
NUL 文字にマッチします
\xhh
hh2 桁の16 進表現)で表される文字にマッチします
\uhhhh
hhhh( 4 桁の 16 進表現)で表される Unicode 値の文字にマッチします

正規表現リテラルと文字列

正規表現リテラルは正規表現をJavaScript中に書く際の簡易記法です

new RegExp('\\d')

/\d/

と同等

文字列リテラルで正規表現を書く場合、\は\\として2重にエスケープする必要がある。これは文字列リテラルが\をエスケープシーケンスとして使用し、さらに正規表現でも\をエスケープシーケンスとして使用するので、2回エスケープが必要になるためである。

逆に、正規表現リテラルでは/でクオートしているので、/自体を使用したい場合は/をエスケープする必要がある。

つまり、文字列リテラルか、正規表現リテラルかでエスケープすべき文字が異なるので注意が必要です。

RegExpオブジェクトとStringメソッド

RegExpオブジェクトはtest、execというメソッドを持つ。

var exp = new RegExp('\\d');
console
.log(exp.test('1')); // true
console
.log(exp.exec('1')); // ["1"]

Stringオブジェクトは正規表現関連のメソッドとしてmatch、replace、split, searchを持つ。(searchはマッチした位置を数値で返します。使い道は特にないのであまり使用しません…)

var str = '1';
console
.log(str.match(/\d/)); // ["1"]
console
.log(str.replace(/\d/,'a')); // a
console
.log('1,,2,3:4-5'.split(/\D/));
 
//  ["1", "", "2", "3", "4", "5"]
 
//  ただし、IEは
 
//  ["1", "2", "3", "4", "5"]
 
// になる。空の要素は省略されるので注意が必要です。

RegExp.testは単純にマッチするか否かという真偽を判定して真偽値を返すので、正規表現関連のメソッドでもっとも高速に動作します。splitでの正規表現はIEの挙動の違いなどもあって、あまり使用しないほうが良いです(正規表現ではなく、文字列でsplitするなら特に問題はない)。

マッチした部分を抜き出して利用する場合、String.matchが扱いやすいのでよく利用される。ただし、matchは引数に文字列を渡した場合、暗黙的に正規表現に変換される点には注意が必要です。

var str = "index?id=123";
var str2 = "index?ids=123";
var id = str.match("id=(\\d+)")[1];//(1)
console
.dir(RegExp);
var m,id2 = '';
if ((m = str2.match("id=(\\d+)"))) {
        id2
= m[1];
}
var id3 = '', exp = /id=(\d+)/;
if (exp.test(str)) {
        id3
= exp.exec(str)[1];
}
console
.log([id,id2,id3]);

正規表現を実行するとその結果はRegExpの$1~$9に代入される。この値を使うコードを見かけることも多いが、これはオススメできない。後々になってmatchとRegExp.$1を参照する間に処理を追加して、その間に正規表現を使用しする処理が入ると予期せぬ結果になる。

var str = "index?id=123";
if (str.match('id=(\d+)')){
        some_function
();// ここで正規表現が使われると結果が変わる危険がある
        id
= RegExp.$1
}

matchの結果は配列になるので、(1)のように書くことで1つ目の()の中身を取り出すことが出来る。ただし、この方法ではmatchしなかった場合にnullに対してプロパティ参照をしてしまうのでエラーになる可能性がある。

そこで、マッチの結果を変数に代入し、代入できていたらそこから値を取り出すという処理か、マッチングするかtestを行い、成功したら値を取り直すという処理が望ましい。

ちなみに下記のようにして強引に一行で書くことも可能

var str2 = "index?ids=123";
var id = (str2.match("id=(\\d+)")||'')[1];
var m,id2 =  ((m = str2.match("id=(\\d+)"))) ? m[1] : '';

正規表現リテラルでgを付けるか、RegExpの第二引数でgを渡すと一度の正規表現ですべてのマッチングを取得できる。

var strs = "id=123&id=456&id=789";
var ids = strs.match(/id=\d+/g)||[];
console
.log(ids);

String.replaceは文字列を置換するメソッドで頻繁に使用される。特に第二引数に関数を渡すことで柔軟な処理ができる。

var data = {
        name
:'simple template',
        discription
:'シンプル且つクロスブラウザなテンプレートです'
};
var tmpl = "簡易テンプレートの実装例:#{name}\n説明:#{discription}";
console
.log(tmpl.replace(/#\{([^}]+)\}/g,function(_,$_){
       
return data[$_] || '';
}));

String.prototype.fill=function(data){
       
return this.replace(/#\{([^}]+)\}/g, fill);
       
function fill(_,$_){
               
return data[$_] || '';
       
}
};
console
.log(tmpl.fill(data));

実践編

URLへのマッチング

URLにマッチする正規表現を考える

var strs = [
'http://albert2005.co.jp/',
'http://www.albert2005.co.jp/'
];
strs
.forEach(function(str){
    console
.log(/^https?:\/\/(www\.)?albert2005\.co\.jp/.exec(str));
});

さらに、パラメータから特定の値を取り出す

var strs = [
   
'/foo/item?NO=4837',
   
'/foo/item?hoge=1&NO=4837',
   
'/foo/item?NO=20309&hoge=2'
];
strs
.forEach(function(str){
    console
.log(/^\/foo\/item\?NO=(\d+)/.exec(str));
    console
.log(/^\/foo\/item\?(?:.*)NO=(\d+)/.exec(str));
});

さらに、肯定的前方先読み、否定的前方先読みで特定の値を取り出す

var strs = [
   
'/1234/id/',
   
'/12343/',
   
'/1234/notid/'
];
strs
.forEach(function(str){
    console
.log(/^\/(\d+)(?=\/id)/.exec(str));
    console
.log(/^\/(\d+)\/(?!notid)/.exec(str));
});

問題

サブドメインに関わらずマッチさせるには?

var strs = [
   
'http://www.albert2005.co.jp',
   
'http://albert2005.co.jp',
   
'http://sub.albert2005.co.jp'
];
strs
.forEach(function(str){
    console
.log(/^http/.exec(str));
});
var strs = [
   
'http://www.albert2005.co.jp',
   
'https://www.albert2005.co.jp',
   
'http://albert2005.co.jp',
   
'http://sub.albert2005.co.jp',
   
'http://sub2.sub.albert2005.co.jp'
];
strs
.forEach(function(str){
    console
.log(/^https?:\/\/(?:[^.]+\.)*albert2005\.co\.jp$/.exec(str));
});