JavaScriptの正規表現のコンセプトを理解する(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

JavaScriptの正規表現のコンセプトを理解する(翻訳)

本チュートリアルでは、JavaScriptのコンセプト全体を明らかにし、正規表現の深淵に迫ります。

JavaScriptの正規表現

JavaScriptの正規表現は、パターンといくつかのフラグによってできています。フラグはすべて、結果にどのように影響を与えるかを指定するシンプルな論理値になっており、必要に応じてオンまたはオフにします。興味深いできことが起きるのは、もちろんパターンの中であり、実に複雑かつ強力な文字列検索を定義できます。しかしパターンを解説する前に、JavaScriptで正規表現が動作するコンテキストのあたりを説明することにします。

JavaScriptではすべてがオブジェクトになっているので、正規表現もRegExpというオブジェクトです。このオブジェクトの作成方法には次の2とおりがあります。


  • 正規表現リテラル

var query = /パターン/フラグ; 2つの/ではさまれた内容はすべてパターンとして扱われ、フラグはその後ろに置かれます。このように正規表現リテラルを直接変数に代入する方法はパフォーマンスに優れていますが、最終的に正規表現がハードコードされることになるので、パターンを組み立てて使いたい場合にはあまり向いていません。

  • 正規表現コンストラクタ

var query = new RegExp('パターン', 'フラグ'); 値を渡すコンストラクタ関数であり、引数の「パターン」や「フラグ」に変数が使えます。これは(正規表現リテラルより)遅いのですが、ユーザー入力からパターンを動的に生成するのに向いています。


ここではRegExpオブジェクトを使います。これはメインのObject「クラス」の拡張であり、デフォルトのプロパティをすべて含むほか、独自のプロパティも持っています。


  • RegExp.source

(string): 検索に使うパターンであり、実際に使われるコンテキストに置かれるとわけがわからなくなるやつです。

  • RegExp.lastIndex

(int): 次の検索の開始位置を示す数値です。文字列の2文字目から検索を開始したい場合は1に設定します(数字は0から開始されるため)。デフォルトは0です。検索で最初のマッチが得られると、このインデックスは、メインの文字列でマッチした部分より後の最初の文字を指します。たとえば(長さが)3文字の語を検索し、2度目の文字から始まる文字列にパターンがマッチすると、lastIndexは7に設定されます。このコンセプトについては後で明らかにします。


RegExpのその他の独自プロパティには、渡したフラグによって設定された値が保存されます。

  • RegExp.ignoreCase

i(論理値): 読んで字のごとく、検索または表示される文字列の大文字小文字の違いを無視します。

  • RegExp.global

g(論理値): このフラグを指定しないと、何度実行しても最初の結果だけを返します。gフラグを指定すると、文字列全体をパターンで検索し、すべての結果を返します。

  • RegExp.multiline

m(論理値): 複数行を個別の文字列として扱い、^(文字列の冒頭)や$(文字列の末尾)が各行に適用されます。mフラグを指定しない場合、これらは文字列全体の冒頭と末尾のみにマッチします。

  • RegExp.sticky

y(論理値): lastIndexより後のマッチのみを検索します。現時点ではFirefoxでしかサポートされていませんが、ES6の仕様に含まれたので、他のブラウザでも実装されるかもしれません。


それでは簡単な例をご覧に入れましょう。このパターンは「Geeks Trick」に完全一致するマッチを検索し、複数ブラウザと互換性のある3つのフラグ(igm)が設定されています。フラグの順序に制限はありません。

  var myRegExp = /Geeks Trick/igm;
  alert("Source: " + myRegExp.source + 
        "\n Ignore Case : " + myRegExp.ignoreCase +  
        "\n Global : " + myRegExp.global + 
        "\n Multiline : " + myRegExp.multiline +
        "\n Last Index: " + myRegExp.lastIndex)

/Geeks Trick/ig は‘Geeks Trick‘の完全一致をすべて検索(g)するが、大文字小文字は区別せず(i)、改行を文字列の境界として扱う(m

RegExp関数(exec()test()match()search()replace()split()

これらの関数はRegExpオブジェクトを受け取って、文字列に対して何らかの処理を行って結果を得るのに使われます。検索、存在チェック、新規文字列の作成、新規配列のビルドができるので、私にとってこれらの関数ですべて用は足ります。このセクションの各例では/\a\w+/(aで始まるすべての単語にマッチする)というシンプルなパターンを使うことにします。対象の文字列には「aa ab ac」を使い、それぞれの関数の働きをわかりやすく示します。各サンプルコードはコンソールに貼って動かすこともできますが、その際には先に以下の値をコンソールに貼っておいてください。

var stringToSearch = 'aa ab ac';
var myRegExp = /a\w+/g;

/a\w+/g は、aで始まる1文字以上(+)の単語(\w)をすべて返す(gフラグ)

exec()

検索が複数の部分文字列にマッチする可能性がある場合、この関数を使うことですべての結果を列挙できます。この関数はループ内で使うのがベストですが、以下の例では(ループせずに)明示的に実行しています。この関数はグローバルマッチのgフラグをRegExpに設定したときだけ機能します。gフラグが設定されていないとlastIndexプロパティが更新されず、最初の結果のところで止まってしまいますので、ループの際は設定を忘れずに。
最後の結果に到達するとexec()nullを返し、最初に戻って検索を開始します。

console.log( myRegExp.lastIndex); // 0
console.log( myRegExp.exec(stringToSearch) ); //["aa", index: 0, input: "aa ab ac"]
console.log( myRegExp.lastIndex) // 2
console.log( myRegExp.exec(stringToSearch) ); //["ab", index: 3, input: "aa ab ac"]
console.log( myRegExp.lastIndex) // 5
console.log( myRegExp.exec(stringToSearch) ); //["ac", index: 6, input: "aa ab ac"]
console.log( myRegExp.lastIndex) // 8
console.log( myRegExp.exec(stringToSearch) ); // null
console.log( myRegExp.lastIndex); // 0
console.log( myRegExp.exec(stringToSearch) ); // ["aa", index: 0, input: "aa ab ac"]
console.log( myRegExp.lastIndex) // 2
//and so on
  • index : 検索対象の文字列から得た結果の最初の文字を指します。上の例で言うと、最初は元の文字列の冒頭がaaなのでindexは0になり、次は元の文字列のabの最初の文字が3文字目なので3になります。

  • input: 元の文字列です。

test()

パターンが文字列に実際にマッチするかどうかをチェックしたいとします。この関数は、マッチする場合にtrue、マッチしない場合にfalseを単に返します。実にシンプルです。注意として、RegExpにグローバル検索のgフラグが設定されている場合は文字列全体を探索し、exec()の場合とまったく同じようにlastIndexを更新しますので、gフラグのグローバルパターンが使われた後でtest()を行う場合はRegExp.lastIndex = 0を設定しておくのがよいでしょう。こうすることで、うっかり元の文字列の末尾からチェックを開始して、マッチするはずのものがマッチしないという事態を防ぐことができます。

次の例もコンソールに貼って試すことができます(先の2つの変数を貼ってあれば、再度貼る必要はありません)。何回か動かしてみて、動作を確認してみましょう。

console.log( myRegExp.test(stringToSearch) ); //true 
console.log( myRegExp.lastIndex); //n

グローバル設定を外したvar myRegExp = /\a\w+/;パターンも貼って同じ例を実行し、動作がどのように変わるかを確かめてみましょう。

match()

この関数は複数の結果を配列で返すので、結果の個数をカウントしたい場合に便利です。ただし、これもグローバルフラグgが前提です。このフラグが設定されていないと、最初の結果しか取れませんのでご注意ください。exec()test()は、RegExpオブジェクトのプロパティ(RegExp.function('ここに文字列を貼って渡す');)を呼ぶ)ですが、この後の4つの関数(matchsearchreplacesplit)はStringオブジェクトのプロパティなので呼び出しの形式(訳注: レシーバと引数)が逆になる点にご注意ください。

stringToSearch.match(myRegExp); // ["aa","ab","ac"]

search()

これはtest()に似た関数であり、文字列がパターンとマッチするかどうかをチェックできますが、最初のマッチのインデックスを返す点が異なります。しかも、グローバルフラグgを設定したりlastIndexを最初のマッチより後に設定したりしても動作は変わりません。また、マッチしない場合には-1を返してそのことを示します。

myRegExp.lastIndex = 5;          // インデックスを末尾に設定 
stringToSearch.search(myRegExp); // それでもマッチするので0が返る
stringToSearch.search(/ad/);     // 見つからないので-1を返す

replace()

検索と置換を行います。RegExpは文字列の一部へのマッチに使われ、定義した別の文字列に置き換えられます。replace()関数は2つの引数を取ります。ここでは1つ目にRegExpオブジェクト、2つ目に置き換え後の文字列を渡します(stringToSearch.replace(myRegExp, 'replacement!');)。

var newString = stringToSearch.replace(myRegExp, 'replacement!');
console.log(stringToSearch);    // "aa ab ac"
console.log(newString);         // "replacement! replacement! replacement!"

注意: 置き換え文字列を指定しない場合、「undefined」という文字列が使われます。これに気づいたのは私だけでしょうか?置き換えで部分文字列を削除したい場合は、2番目の引数に空文字列''を指定してください。

訳注: JavaScriptの仕様として、引数を明示しない場合は暗黙的にundefinedが渡されます。

split()

1つの文字列を分割して部分文字列の配列にしたい場合に使います。分割箇所はマッチで定義します。コンマのように簡単なものから、複雑な文字列向けの複雑な変数まで使えます。

myRegExp = / /;                     // スペースで分割
var spaceSeparatedArray = stringToSearch.split(myRegExp); 
console.log(spaceSeparatedArray);   //["aa", "ab", "ac"]

split()は上のように、グローバルフラグgを指定していなくてもグローバルに機能する点にご注意ください。

関連記事

Ruby 2.4.1新機能: Onigmo正規表現の非包含演算子(?~ )をチェック

正規表現の先読み・後読み(look ahead、look behind)を活用しよう

正規表現: 文字クラス [ ] 内でエスケープしなくてもよい記号

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ