Regular Expressions

Eloquent JavaScript Chapter 9 の読書ノート。

JavaScript における正規表現について述べた章だ。正規表現については JavaScript とは別に集中的に学ぶほうが学習効率が良いだろう。

Creating a regular expression

正規表現はオブジェクトの一種だ。次の二通りの生成方法がある。

  • コンストラクター RegExp を使う。

  • スラッシュ文字 / で囲んだリテラル値として書く。

let re1 = new RegExp("abc");
let re2 = /abc/;
  • 下の記法のほうはバックスラッシュを含めるときはそれを(やはりバックスラッシュで)エスケープする必要がある。

  • 正規表現のメタキャラクターはバックスラッシュを使ってエスケープする。

Testing for matches

正規表現のメソッドで最も単純なものは test だ。引数の文字列がパターンに合致していれば true を返す。

console.assert(/abc/.test("abcde"));
console.assert(! /abc/.test("abxde"));
  • Python の re.match() に相当するのだろう。

Sets of characters

  • ある文字列が abc などの文字列を含むかどうかを判定するには、単に文字列メソッドの indexOf を使ってもできる。正規表現を使えば、より一般的なパターンの文字列を表現することができる。

  • JavaScript でも正規表現の角括弧機能、文字グループを使える。

    • /[0123456789]/

    • /[0-9]/

  • よく使われる文字グループには、独自の組み込み略記法が用意されている。

記号

意味

\d

アラビア数字

\w

アルファベットおよびアラビア数字

\s

ホワイトスペース(スペース、タブ、改行など)を表す文字

\D

\d に合致しない文字

\W

\w に合致しない文字

\S

\s に合致しない文字

.

改行文字でない文字

  • このバックスラッシュコードは、角括弧の中でも使用できる。

    • ただし角括弧内に . を書くとメタキャラクターとしての意味を失う。

  • 文字のセットを反転させるには、セット内の文字以外の任意の文字にマッチさせたいことを表現する。

  • 角括弧内に ^ を置いて「~以外の文字」の意味を与える機能も使える。

Repeating parts of a pattern

  • 正規表現でパターンの後ろにプラス記号 + を付けると、その要素が 1 回以上回繰り返される可能性があることを示す。したがって /\d+/ は、一つまたは複数の数字に合致する。

  • 星印 * も同様の意味を持つが、パターンがゼロ回合致することも可能だ。

  • 疑問符 ? はパターンの一部をオプションにする。つまり、0 回または 1 回だけの出現に合致する。

    let neighbor = /neighbou?r/;
    console.assert(neighbor.test("neighbour"));
    console.assert(neighbor.test("neighbor"));
    
  • あるパターンが正確な回数だけ現れることを示すには中括弧を使う。

    • /\d{4}/ は数字 4 個。

    • /\d{1,2}/ は数字 1 個または 2 個。

    • /\d{5,}/ は数字 5 個以上。

Grouping subexpressions

  • 量指定演算子を一度に複数使用するには、括弧を使用する必要がある。

  • 正規表現の中で括弧で囲まれた部分は、それに続く演算子に関してはひとかたまりに扱われる。

let cartoonCrying = /boo+(hoo+)+/i;
console.log(cartoonCrying.test("Boohoooohoohooo"));
  • 1 番目と 2 番目の +boohoo の最後の o にのみそれぞれ適用される。

  • 3 番目の + はグループ hoo+ 全体に適用され、このような一つ以上の配列に合致する。

  • /pattern/i の最後の i は、大文字小文字を区別しないマッチングを指定する。

Matches and groups

  • 正規表現のメソッド exec はマッチしなかった場合は null を返し、それ以外の場合は合致情報を表すオブジェクトを返す。

  • exec から返されたオブジェクトには、文字列のどこからマッチしたのかを示す index プロパティーがある。

  • それ以外のオブジェクトは文字列の配列だ。

  • 文字列のメソッド match は正規表現を引数にとり、上記と同じことをする。

  • 正規表現に括弧で括られた部分式が含まれている場合、それらのグループに合致したテキストも合致情報の配列に出てくる。

    • 合致した全体が常に最初の要素となる。

    • 次の要素は、最初のグループに合致した部分となり、次に 2 番目のグループ、というようになる。

      let quotedText = /'([^']*)'/;
      console.log(quotedText.exec("she said 'hello'")); // → ["'hello'", "hello"]
      
    • グループが全くマッチしない場合は出力配列でのそのグループの位置には undefined となる。

    • 同様に、あるグループが複数回合致した場合、最後のものだけが配列に入る。

      console.log(/bad(ly)?/.exec("bad")); // → ["bad", undefined]
      console.log(/(\d)+/.exec("123")); // → ["123", "3"]
      
      • この二行目はおかしい感じがする。

The Date class

JavaScript では日付オブジェクトを Date コンストラクターで生成する。

new Date;
new Date(2009, 11, 9);
new Date(2009, 11, 9, 12, 59, 59, 999);
  • 紛らわしいことに月番号は 0 から始まる。

  • 最後の 4 つの引数は時間、分、秒、ミリ秒で省略可能。

  • タイムスタンプは、1970 年の開始時点からのミリ秒数として保存される。これは、同時期に発明された Unix 時間で定められた規則に従っている。

    • 以前の時間には負の数を使用できる。

    • Date オブジェクトのメソッド getTime は、この数値を返す。

      console.log(new Date(2013, 11, 19).getTime()); // → 1387407600000
      console.log(new Date(1387407600000));
      
  • Date コンストラクターに引数をただ一つ与えた場合、その引数は、ミリ秒単位のカウントとして扱われる。

  • Date オブジェクトには次のようなメソッドがあり、それぞれ名前に対応する成分を返す。

    • getFullYear

    • getMonth

    • getDate

    • getHours

    • getMinutes

    • getSecurity

    • getYear: これは 1900 年から 98 年または 119 年を引いたもので、ほとんど役に立たない。

本書では文字列から正規表現を用いて日付オブジェクトを生成する方法が示されているが略。

Word and string boundaries

  • キャレット ^ は入力文字列の先頭に合致する。

  • ドル記号 $ は入力文字列の末尾に合致する。

  • \b は単語の境界位置に合致する。文字ではなく位置に作用することに注意。

    • 単語の境界とは、文字列の始点、終点、または文字列の中で一方に単語の文字 \w があり、もう一方に非単語の文字 \W がある位置のいずれかを指す。

Choice patterns

パイプ文字 | は、左と右のパターンの選択を表す。

  • 括弧を使うと、パイプ演算子が適用されるパターンの部分を限定できる。

  • 複数のパイプ演算子を並べることで、二つ以上の選択肢を表現できる。

let animalCount = /\b\d+ (pig|cow|chicken)s?\b/;
console.assert(animalCount.test("15 pigs"));
console.assert(!animalCount.test("15 pigchickens"));

The mechanics of matching

正規表現エンジンについて説明している。この節の内容に JavaScript 固有のものはない。

Backtracking

正規表現エンジンのバックトラック(後戻り法)と、それにまつわる問題点について述べている。この節の内容に JavaScript 固有のものはない。

The replace method

  • メソッド String.replace は文字列の一部を別の文字列に置換する。

    • 第一引数には単純な文字列だけではなく正規表現を指定してもかまわない。その場合には、最初の合致部分しか置換しない。

    • ただし、正規表現に g オプションがあれば、すべての合致部分を置換する。

console.assert("papa".replace("p", "m") == "mapa");
console.assert("Borobudur".replace(/[ou]/, "a") == "Barobudur");
console.assert("Borobudur".replace(/[ou]/g, "a") == "Barabadar");
  • replace と一緒に正規表現を使うことの真の力は、置換文字列に合致したグループを参照することができるという事実から引き出される。

  • 置換文字列の $1$2 は、パターン内の括弧で囲まれたグループを参照している。以下、同様に $9 まで対応する番号のグループを参照する。

  • 一致したテキスト全体は $& で参照する。

  • 文字列ではなく関数を replace の第二引数として指定することもできる。置換のたびに、合致したグループ(全体も含む)とともに関数を呼び出し、その戻り値が新しい文字列を挿入する。

    • Python にも同様の機能がある。

Greed

  • 文字列から特定の部分文字列、パターンに合致する部分文字列を削除するのにもメソッド replace が使われる。第二引数を空文字列にすればよい。

  • 本書の失敗版デモコードにある「コメントに合致する正規表現」のうち、C 言語スタイルのほうの正規表現に注目したい。

    function stripComments(code) {
        return code.replace(/\/\/.*|\/\*[^]*\*\//g, "");
    }
    

    任意の文字を表す部分を [^] で表している。ここでは単にメタキャラクター . を使うことはできない。C 言語型コメントは新しい行に続けることができ、メタキャラクター . は改行文字には合致しないからだ。

繰り返し演算子 +, *, ?, {m,n} は貪欲であると言う。可能な限り長い合致部分を求めて、そこから後戻り法を適用するという意味だ。これらの演算子の後に ? が付いた変種 +?, *?, ??, {m,n}? を使うと、これらの演算子は非貪欲型となり、可能な限り少ない量のマッチングから始めて、残ったパターンが小さい方の合致部分に合わない場合にのみ、さらにマッチングを試みる。

  • 正規表現で繰り返し演算子を使うときは、まず非貪欲型を検討すること。

Dynamically creating RegExp objects

  • 正規表現の一部を変数にしたい場合には RegExp コンストラクターと文字列演算をうまく組み合わせるといい。

  • ただし、そのような変数に正規表現メタキャラクターが含まれている場合には、適宜エスケープをする必要があるだろう。

The search method

  • メソッド String.indexOf は正規表現を使って呼び出すことはできない。

  • メソッド String.search は正規表現が使える。このメソッドは indexOf と同様に正規表現が見つかった最初のインデックスを返し、見つからなかった場合は -1 を返す。

console.assert("  word".search(/\S/) == 2);
console.log("    ".search(/\S/) == -1);

The lastIndex property

正規表現オブジェクトのプロパティーを二つ説明している。

  • source は正規表現が作成された文字列を含む。

  • lastIndex は、ある限られた状況下で、次のマッチを開始する場所を制御する。

    • その状況とは、正規表現に g または y オプションが有効である必要があり、そしてマッチがメソッド exec を通じて見つかる必要があるというものだ。

let pattern = /y/g;
pattern.lastIndex = 3;
let match = pattern.exec("xyzzy");
console.assert(match.index == 4);
console.assert(pattern.lastIndex == 5);
  • 合致する場合は lastIndex が自動的に更新され、マッチの直後を指すようになる。

  • 合致しない場合は lastIndex はゼロに戻される。これは新しく構築された正規表現オブジェクトのそれの値でもある。

g オプションと y オプションの違いは、

  • y が有効な場合は lastIndex から直接始まる場合にしかマッチングが成功しない。

  • g が有効なの場合は、合致部分を先に探す。

let global = /abc/g;
console.log(global.exec("xyz abc")); // → ["abc"]
let sticky = /abc/y;
console.log(sticky.exec("xyz abc")); // → null
  • 複数の exec 呼び出しに共通の正規表現値を使用する場合、lastIndex の自動更新が問題となる。誤って前の呼び出しから残されたインデックスで開始してしまうかもしれないからだ。

  • オプション g には文字列のメソッド match の動作を変えるという効果もある。g を指定して呼び出すと exec が返すのと同じような配列を返すのではなく、文字列内のパターンのすべての合致部分を見つけ、それら合致文字列からなる配列を返す。

Looping over matches

次の構文でループで回す。

let input = "A string with 3 numbers in it... 42 and 88.";
let number = /\b\d+\b/g;
let match;
while (match = number.exec(input)) {
    console.log("Found", match[0], "at", match.index);
}
  • C 言語と同様に while ループの条件の代入式全体は代入後の左辺の値を返す。

  • match が真に変換される条件は match.index の値で決まるようだ。

Parsing an INI file

いわゆる INI ファイルを読むコードを JavaScript で正規表現を使って書く。

searchengine=https://duckduckgo.com/?q=$1
spitefulness=9.7
; comments are preceded by a semicolon...
; each section concerns an individual enemy
[larry]
fullname=Larry Doe
type=kindergarten bully
website=http://www.geocities.com/CapeCanaveral/11451
[davaeorn]
fullname=Davaeorn
type=evil wizard
outputdir=/home/marijn/enemies/davaeorn

正確な文法は次のとおり:

  • 空行とセミコロンで始まる行を無視する。

  • 角括弧で囲まれる行を新しいセクションの開始位置とする。

  • 英数字の識別子の後に = を付けた行があれば、その設定を現在のセクションに追加する。

  • それ以外のものは無効とする。

これを JavaScript のオブジェクトに変換したい。JSON 的なデータ構造を意図している。

  • 一行ごとに処理するべきなので、ファイルを一行ごとに分割することから始める。 String.split を用いる。ただし区切り文字は改行文字そのものではなく、正規表現 \r?\n を指定する。

    • split の戻り値に即 forEach を適用していて見栄えが良い。

    • そのループの中で前述の条件にそれぞれ対応する matchtest を複数回試みている。

International characters

  • JavaScript の正規表現は、英語に存在しない文字についてはかなりお粗末だ。

    • JavaScript の 正規表現では単語の文字 \w とはラテンアルファベットの大文字と小文字、十進数の数字、そしてなぜかアンダースコアからなる集合だ。é やß のようなものには、単語文字であるにもかかわらず合致しない。

    • 大文字のほうの \W には合致するが、それでは意味が合わない。

  • 文字セット \s にはこの問題がない。Unicode 規格が空白文字とみなすすべての文字に合致する。例えば non-breaking space やモンゴル語の母音分離記号なども合致する。

  • 正規表現は既定ではコード単位で動作する。したがって、二つのコード単位で構成されている文字に対しては、おかしな動作をする。

  • u オプションを付加すれば Unicode 文字列に対しても動作する。

console.assert(! /🍎{3}/.test("🍎🍎🍎"));
console.assert(! /<.>/.test("<🌹>"));
console.assert(/<.>/u.test("<🌹>"));

Unicode オプションを有効にした正規表現で規格で指定された \p{Property=Value} のパターン?を使用することもできる。

console.assert(/\p{Script=Greek}/u.test("α"));
console.assert(! /\p{Script=Arabic}/u.test("α"));
console.assert(/\p{Alphabetic}/u.test("α"));
console.assert(! /\p{Alphabetic}/u.test("!"));

Summary

  • 正規表現は文字列中のパターンを表現するオブジェクトだ。これらのパターンを表現する独自の言語を使う。

  • 正規表現には各種メソッドがある。

  • 文字列にも正規表現を受け取るメソッドがある。

  • 正規表現にはオプションがあり、/ の後ろにそれを指定する。

  • 正規表現は鋭利なツールでありながら、扱いづらい。ある種の作業はひじょうに簡単になるが、複雑な問題に適用するとすぐに手に負えなくなる。正規表現ではうまく表現できないことを正規表現に当てはめようとしないことも大切だ。

Exercises

Debuggex のようなオンラインツールを使うと、正規表現の視覚化が意図したものと一致するかどうかを確認したり、さまざまな入力文字列に対する反応を試したりするのに役立つことがある。

Regexp golf

問題 正規表現ゴルフとは、与えられたパターンにマッチする、できるだけ小さな正規表現を書くゲームだ。

次の各項目について、与えられた部分文字列のいずれかが文字列の中に存在するかどうかを調べる正規表現を書け。正規表現は次のものにマッチしなければならない。文字列のみにマッチしなければならない。

正規表現は、記述された部分文字列のいずれかを含む文字列のみに合致する必要がある。

明示的に言及されていない限り、単語の境界は気にしないでよい。表現がうまくいったら、それ以上小さくできないか考えろ。

  1. car and cat

  2. pop and prop

  3. ferret, ferry, and ferrari

  4. Any word ending in ious

  5. A whitespace character followed by a period, comma, colon, or semicolon

  6. A word longer than six letters

  7. A word without the letter e (or E)

解答 問題の趣旨は .+ とか (car|cat) のような露骨な正規表現に甘えるなと言っている。

// 1. car and cat
/ca[rt]/

// 2. pop and prop
/pr?op/

// 3. ferret, ferry, and ferrari
/ferr(et|y|ari)/

// 4. Any word ending in ious
/\b\w*ious\b/

// 5. A whitespace character followed by a period, comma, colon, or semicolon
/\s(?=[.,:;])/

// 6. A word longer than six letters
/\b\w{7,}\b/

// 7. A word without the letter e (or E)
/\b[_0-9a-df-z]\b+/i

Quoting style

問題 小説を書いていて、台詞に単一引用符を使っていたとする。ここで、台詞の引用符をすべて二重引用符に置換したいが、aren’t などの短縮形に使われている単一引用符は残しておきたいとする。

この二種類の引用符の使い方を区別するパターンを考え、適切な置換を行うメソッド replace の呼び出しを作れ。

解答 短縮形内の引用符か否かを「引用符の直前と直後の文字が両方とも区切り位置でない」に決め打ちする。

text.replace(/(\B'\b|\b'\B)/g, '"');

ただし、これは '90s などのパターンと、複数形と所有格が複合した単語に含まれる引用符も置換する。

Numbers again

問題 JavaScript スタイルの数値のみに合致する正規表現を書け。

数字の前に正負符号、十進数のドット、指数表記(5e-3 または 1E10)をサポートし、さらに指数の前に符号を付けることができなければならない。

また、ドットの前後に数字がある必要はないが、数がドットだけであることはあり得ないことに注意しろ。つまり、.55. は JavaScript の数として有効だが、「ドットだけの数」は有効ではない。

解答 問題文では JavaScript の数値と言っているが、簡単のために十進数のみに絞る。

/[+-]?((\d+(\.\d*)?)|(\.\d+))([eE][+-]?\d+)?/

急所は「ドットだけの数」を避けるパターンを記述できるかどうかで、最初の丸括弧にそれを表現した。

以上