第10章: 正規表現

前の章のさまざまな箇所で、文字列の値からパターンを探す必要がありました。第4章では、日付の一部である数字が見つかる正確な位置を記述することで、文字列から日付の値を抽出しました。その後、第6章では、文字列内のある種の文字、たとえばHTML出力でエスケープする必要がある文字を見つけるための、特に醜いコードをいくつか見ました。

正規表現は、文字列内のパターンを記述するための言語です。それらは、JavaScript(および、さまざまな他のプログラミング言語でも、何らかの形で)に組み込まれた、小さく独立した言語を形成します。それは非常に読みやすい言語ではありません ― 大きな正規表現は完全に読めなくなる傾向があります。しかし、それは便利なツールであり、文字列処理プログラムを非常に簡素化することができます。


文字列が引用符で囲まれて記述されるのと同様に、正規表現のパターンはスラッシュ(/)で囲まれて記述されます。これは、式内のスラッシュをエスケープする必要があることを意味します。

var slash = /\//;
show("AC/DC".search(slash));

searchメソッドはindexOfに似ていますが、文字列の代わりに正規表現を検索します。正規表現で指定されたパターンは、文字列ではできないことがいくつかできます。まず、要素の一部を単一の文字以上と一致させることができます。第6章では、ドキュメントからマークアップを抽出する際に、文字列内の最初のアスタリスクまたは開き中括弧を見つける必要がありました。それはこのように行うことができました。

var asteriskOrBrace = /[\{\*]/;
var story =
  "We noticed the *giant sloth*, hanging from a giant branch.";
show(story.search(asteriskOrBrace));

[]の文字は、正規表現内で特別な意味を持っています。それらは文字のセットを囲むことができ、それらは「これらの文字のいずれか」を意味します。ほとんどの非英数字は正規表現内で特別な意味を持つため、実際の文字を参照するために使用する場合は常にバックスラッシュ1でエスケープすることをお勧めします。


頻繁に必要となる文字のセットには、いくつかのショートカットがあります。ドット(.)は「改行ではない任意の文字」を意味するために使用でき、エスケープされた「d」(\d)は「任意の数字」を意味し、エスケープされた「w」(\w)は任意の英数字(なぜかアンダースコアを含む)と一致し、エスケープされた「s」(\s)は任意の空白(タブ、改行、スペース)文字と一致します。

var digitSurroundedBySpace = /\s\d\s/;
show("1a 2 3d".search(digitSurroundedBySpace));

エスケープされた「d」、「w」、および「s」は、大文字に置き換えて、反対の意味にすることができます。たとえば、\Sは空白ではない任意の文字と一致します。[]を使用すると、パターンを^文字で開始することで反転させることができます。

var notABC = /[^ABC]/;
show("ABCBACCBBADABC".search(notABC));

ご覧のとおり、正規表現がパターンを表現するために文字を使用する方法は、A)非常に短く、B)非常に読みにくくなります。


例 10.1

"XX/XX/XXXX"形式の日付と一致する正規表現を作成します。ここで、Xは数字です。文字列"born 15/11/2003 (mother Spot): White Fang"に対してテストします。

var datePattern = /\d\d\/\d\d\/\d\d\d\d/;
show("born 15/11/2003 (mother Spot): White Fang".search(datePattern));

パターンが文字列の先頭で開始するか、末尾で終了することを確認する必要がある場合があります。このために、特別な文字^$を使用できます。最初の文字は文字列の先頭に一致し、2番目の文字は末尾に一致します。

show(/a+/.test("blah"));
show(/^a+$/.test("blah"));

最初の正規表現は、a文字を含む任意の文字列と一致し、2番目の正規表現は、完全にa文字で構成される文字列のみと一致します。

正規表現はオブジェクトであり、メソッドがあることに注意してください。testメソッドは、指定された文字列が式と一致するかどうかを示すブール値を返します。

コード\bは、「単語境界」と一致します。これは、句読点、空白、または文字列の先頭または末尾の場合があります。

show(/cat/.test("concatenate"));
show(/\bcat\b/.test("concatenate"));

パターンの部分を、複数回繰り返すことができるようにすることができます。要素の後にアスタリスク(*)を付けると、ゼロを含む任意の回数繰り返すことができます。プラス(+)も同じですが、パターンが少なくとも1回発生する必要があります。疑問符(?)は、要素を「オプション」にします。つまり、ゼロ回または1回発生する可能性があります。

var parenthesizedText = /\(.*\)/;
show("Its (the sloth's) claws were gigantic!".search(parenthesizedText));

必要な場合は、要素が発生する回数についてより正確にするために、中括弧を使用できます。中括弧の間の数字({4})は、発生する必要のある回数を正確に示します。間にコンマのある2つの数字({3,10})は、パターンが最初の数字と同じ回数以上、2番目の数字と同じ回数以下発生する必要があることを示します。同様に、{2,}は2回以上の発生を意味し、{,4}は4回以下の発生を意味します。

var datePattern = /\d{1,2}\/\d\d?\/\d{4}/;
show("born 15/11/2003 (mother Spot): White Fang".search(datePattern));

/\d{1,2}//\d\d?/という部分は、どちらも「1桁または2桁の数字」を表します。


例 10.2

メールアドレスと一致するパターンを作成します。簡単にするために、@の前後の部分は英数字と文字.-(ドットとダッシュ)のみを含むことができ、アドレスの最後の部分、最後のドットの後の国コードは英数字のみを含むことができ、2文字または3文字の長さでなければならないと仮定します。

var mailAddress = /\b[\w\.-]+@[\w\.-]+\.\w{2,3}\b/;

show(mailAddress.test("kenny@test.net"));
show(mailAddress.test("I mailt kenny@tets.nets, but it didn wrok!"));
show(mailAddress.test("the_giant_sloth@gmail.com"));

パターンの最初と最後にある\bは、2番目の文字列が一致しないことを確認します。


正規表現の一部を括弧でグループ化できます。これにより、複数の文字で*などを使用できます。例えば

var cartoonCrying = /boo(hoo+)+/i;
show("Then, he exclaimed 'Boohoooohoohooo'".search(cartoonCrying));

その正規表現の末尾のiはどこから来たのでしょうか?閉じスラッシュの後、「オプション」を正規表現に追加できます。ここでのiは、式が大文字と小文字を区別しないことを意味し、パターンの小文字のBが文字列の大文字のBと一致することを許可します。

パイプ文字(|)は、パターンが2つの要素の間で選択できるようにするために使用されます。例えば

var holyCow = /(sacred|holy) (cow|bovine|bull|taurus)/i;
show(holyCow.test("Sacred bovine!"));

多くの場合、パターンを探すことは、文字列から何かを抽出する最初のステップにすぎません。前の章では、この抽出は文字列のindexOfメソッドとsliceメソッドを多数呼び出すことによって行われました。正規表現の存在を認識したので、代わりにmatchメソッドを使用できます。文字列が正規表現と照合されると、一致が失敗した場合は結果がnullになり、成功した場合は一致した文字列の配列になります。

show("No".match(/Yes/));
show("... yes".match(/yes/));
show("Giant Ape".match(/giant (\w+)/i));

返された配列の最初の要素は、常にパターンに一致した文字列の部分です。最後の例が示すように、パターンに括弧で囲まれた部分がある場合、それらが一致する部分も配列に追加されます。多くの場合、これにより、文字列の部分の抽出が非常に簡単になります。

var parenthesized = prompt("Tell me something", "").match(/\((.*)\)/);
if (parenthesized != null)
  print("You parenthesized '", parenthesized[1], "'");

例 10.3

第4章で作成した関数extractDateを書き直します。この関数は、文字列が与えられると、前に見た日付形式に従うものを探します。そのような日付が見つかった場合は、値をDateオブジェクトに入れます。それ以外の場合は、例外をスローします。日または月が1桁のみで記述された日付を受け入れるようにします。

function extractDate(string) {
  var found = string.match(/(\d\d?)\/(\d\d?)\/(\d{4})/);
  if (found == null)
    throw new Error("No date found in '" + string + "'.");
  return new Date(Number(found[3]), Number(found[2]) - 1,
                  Number(found[1]));
}

show(extractDate("born 5/2/2007 (mother Noog): Long-ear Johnson"));

このバージョンは前のバージョンよりもわずかに長いですが、実際に行っていることをチェックし、無意味な入力が与えられたときに通知するという利点があります。これは正規表現なしでははるかに困難でした ― 数字が1桁か2桁か、ダッシュが正しい場所にあるかどうかを調べるために、indexOfをたくさん呼び出す必要があったでしょう。


第6章で見た文字列値のreplaceメソッドには、最初の引数として正規表現を指定できます。

print("Borobudur".replace(/[ou]/g, "a"));

正規表現の後のg文字に注目してください。これは「グローバル」を表し、パターンと一致する文字列のすべての部分を置き換える必要があることを意味します。このgを省略すると、最初の"o"だけが置き換えられます。

場合によっては、置き換えられた文字列の一部を保持する必要があることがあります。たとえば、1行に1つの名前が「LastName, FirstName」形式で含まれている人々の名前を含む大きな文字列があるとします。これらの名前を交換し、コンマを削除して、単純な「FirstName LastName」形式にしたいと考えています。

var names = "Picasso, Pablo\nGauguin, Paul\nVan Gogh, Vincent";
print(names.replace(/([\w ]+), ([\w ]+)/g, "$2 $1"));

置換文字列の$1$2は、パターンの括弧で囲まれた部分を参照します。$1は、最初の括弧のペアと一致したテキストに置き換えられ、$2は2番目のペアで置き換えられ、最大$9まで続きます。

パターンに9つ以上の括弧付きの部分がある場合、これは機能しなくなります。ただし、文字列の一部を置き換えるもう1つの方法があり、他のいくつかの難しい状況でも役立ちます。replaceメソッドに指定された2番目の引数が文字列値ではなく関数値である場合、一致が見つかるたびにこの関数が呼び出され、一致したテキストは関数が返すものに置き換えられます。関数に与えられる引数は、matchによって返される配列で見つかった値と同様に、一致した要素です。最初の引数は一致全体であり、その後にパターンの括弧付きの部分ごとに1つの引数が続きます。

function eatOne(match, amount, unit) {
  amount = Number(amount) - 1;
  if (amount == 1) {
    unit = unit.slice(0, unit.length - 1);
  }
  else if (amount == 0) {
    unit = unit + "s";
    amount = "no";
  }
  return amount + " " + unit;
}

var stock = "1 lemon, 2 cabbages, and 101 eggs";
stock = stock.replace(/(\d+) (\w+)/g, eatOne);

print(stock);

例 10.4

最後のトリックは、第6章のHTMLエスケーパーをより効率的にするために使用できます。それがこのようであったことを覚えているかもしれません。

function escapeHTML(text) {
  var replacements = [["&", "&"], ["\"", """],
                      ["<", "&lt;"], [">", "&gt;"]];
  forEach(replacements, function(replace) {
    text = text.replace(replace[0], replace[1]);
  });
  return text;
}

同じことを行う新しい関数escapeHTMLを作成しますが、replaceを1回だけ呼び出します。

function escapeHTML(text) {
  var replacements = {"<": "&lt;", ">": "&gt;",
                      "&": "&amp;", "\"": "&quot;"};
  return text.replace(/[<>&"]/g, function(character) {
    return replacements[character];
  });
}

print(escapeHTML("The 'pre-formatted' tag is written \"<pre>\"."));

replacementsオブジェクトは、各文字をそのエスケープされたバージョンに関連付けるための簡単な方法です。このように使用することは安全です(つまり、Dictionaryオブジェクトは必要ありません)。使用されるプロパティは、/[<>&"]/式によって一致するものだけであるためです。


コードを記述している間に照合する必要があるパターンがわからない場合があります。メッセージボード用の(非常に単純な)わいせつフィルターを作成しているとします。わいせつな言葉を含まないメッセージのみを許可したいと考えています。ボードの管理者は、受け入れられないと考える単語のリストを指定できます。

テキスト中で特定の単語セットをチェックする最も効率的な方法は、正規表現を使用することです。単語リストが配列としてある場合、次のように正規表現を構築できます。

var badWords = ["ape", "monkey", "simian", "gorilla", "evolution"];
var pattern = new RegExp(badWords.join("|"), "i");
function isAcceptable(text) {
  return !pattern.test(text);
}

show(isAcceptable("Mmmm, grapes."));
show(isAcceptable("No more of that monkeybusiness, now."));

単語の前後に\bパターンを追加することで、「grapes」という単語が不適切と分類されないようにすることができます。しかし、そうすると2番目の単語も受け入れられることになり、おそらくそれは正しくありません。わいせつフィルタを正しく実装するのは難しく(そして通常、非常に煩わしく、良いアイデアとは言えません)。

RegExpコンストラクタの最初の引数はパターンを含む文字列で、2番目の引数を使用して大文字小文字を区別しない、またはグローバルな検索を設定できます。パターンを保持する文字列を構築する際には、バックスラッシュに注意する必要があります。通常、文字列が解釈されるときにバックスラッシュは削除されるため、正規表現自体に含める必要があるバックスラッシュはエスケープする必要があります。

var digits = new RegExp("\\d+");
show(digits.test("101"));

正規表現について最も重要なことは、それが存在し、文字列操作コードの能力を大幅に向上させることができるということです。正規表現は非常に難解なので、初めて使用する際には、おそらく10回ほど詳細を調べる必要があるでしょう。粘り強く続けることで、すぐにそれらはまるでオカルトの呪文のように見える表現を、いとも簡単に書けるようになるでしょう。

(コミックはRandall Munroeによるものです。)

  1. この場合、バックスラッシュは実際には必要ありませんでした。なぜなら、文字は[]の間に出現するからです。しかし、とにかくエスケープしておくと、それについて考える必要がないので簡単です。