第9章正規表現
元麻は言った。「木の目に逆らって切るときは、多くの力が必要である。問題の目に逆らってプログラムするときは、多くのコードが必要である。」
プログラミングのツールやテクニックは、混沌とした進化の中で生き残り、広まっていく。必ずしも美しく、優れたものが勝つわけではなく、例えば、他の成功したテクノロジーと統合されるなど、適切なニッチの中で十分に機能するものが勝つのである。
この章では、そのようなツールの一つである正規表現について議論する。正規表現は、文字列データにおけるパターンを記述する方法である。これは、JavaScriptや他の多くの言語やツールの一部である、小さく独立した言語を形成している。
正規表現は非常に扱いにくく、非常に便利である。その構文は暗号めいており、JavaScriptが提供するプログラミングインターフェースは扱いにくい。しかし、それらは文字列を検査および処理するための強力なツールである。正規表現を正しく理解することで、より効果的なプログラマーになることができる。
正規表現の作成
正規表現はオブジェクトの一種である。これは、RegExp
コンストラクターで構築するか、パターンをスラッシュ (/
) 文字で囲むことでリテラル値として記述することができる。
var re1 = new RegExp("abc"); var re2 = /abc/;
これらの正規表現オブジェクトはどちらも、同じパターンを表している。つまり、a 文字の後に b が続き、その後に c が続く。
RegExp
コンストラクターを使用する場合、パターンは通常の文字列として記述されるため、バックスラッシュには通常の規則が適用される。
パターンがスラッシュ文字の間に出現する2番目の表記法では、バックスラッシュの扱いがやや異なる。まず、スラッシュはパターンを終了させるため、パターンの一部にしたいスラッシュの前にはバックスラッシュを置く必要がある。さらに、特殊文字コード (\n
など) の一部ではないバックスラッシュは、文字列の場合のように無視されるのではなく、保持され、パターンの意味を変更する。疑問符やプラス記号などの一部の文字は、正規表現では特別な意味を持ち、文字自体を表す場合はバックスラッシュを前に付ける必要がある。
var eighteenPlus = /eighteen\+/;
正規表現を書くときに、どの文字にバックスラッシュによるエスケープが必要かを正確に知るには、特殊な意味を持つすべての文字を知る必要がある。当面はこれは現実的ではないかもしれないので、迷ったときは、文字、数字、空白以外の文字の前にはバックスラッシュを置くこと。
マッチのテスト
正規表現オブジェクトには、多くのメソッドがある。最も単純なものはtest
である。これに文字列を渡すと、文字列に式中のパターンのマッチが含まれているかどうかを示すブール値を返す。
console.log(/abc/.test("abcde")); // → true console.log(/abc/.test("abxde")); // → false
非特殊文字のみで構成される正規表現は、単にその文字の並びを表す。テスト対象の文字列のどこかにabcが発生した場合(開始時だけでなく)、test
はtrue
を返す。
文字のセットのマッチング
文字列にabcが含まれているかどうかを調べることは、indexOf
を呼び出すことで行うこともできる。正規表現を使用すると、それを超えてより複雑なパターンを表現できる。
任意の数字をマッチさせたいとする。正規表現では、角かっこで文字のセットを囲むと、その式の部分が角かっこの間の文字のいずれかにマッチするようになる。
console.log(/[0123456789]/.test("in 1992")); // → true console.log(/[0-9]/.test("in 1992")); // → true
角かっこの中で、2つの文字の間のダッシュ (-
) を使用して、文字のUnicode番号によって順序が決定される文字の範囲を示すことができる。文字0から9はこの順序で互いに隣り合っている(コード48から57)ため、[0-9]
はそれらすべてをカバーし、任意の数字にマッチする。
独自の内蔵ショートカットを持つ共通文字グループがいくつかある。数字もその一つである。\d
は[0-9]
と同じ意味である。
\d |
任意の数字文字 |
\w |
英数字(「単語文字」) |
\s |
任意の空白文字(スペース、タブ、改行、および同様のもの) |
\D |
数字ではない文字 |
\W |
非英数字 |
\S |
非空白文字 |
. |
改行を除く任意の文字 |
そのため、次のような式で30-01-2003 15:20のような日付と時刻の形式にマッチさせることができる
var dateTime = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/; console.log(dateTime.test("30-01-2003 15:20")); // → true console.log(dateTime.test("30-jan-2003 15:20")); // → false
これは完全にひどく見える。バックスラッシュが多すぎて、表現されている実際のパターンを見つけにくくするバックグラウンドノイズが発生している。この式のわずかに改善されたバージョンを後で確認する。
これらのバックスラッシュコードは、角かっこ内でも使用できる。たとえば、[\d.]
は、任意の数字またはピリオド文字を意味する。ただし、角かっこ内で使用すると、ピリオド自体が特殊な意味を失うことに注意する必要がある。同じことが、+
などの他の特殊文字にも当てはまる。
文字のセットを反転させる場合、つまり、セット内の文字以外の任意の文字にマッチさせたい場合、開き角かっこの後にキャレット (^
) 文字を記述できる。
var notBinary = /[^01]/; console.log(notBinary.test("1100100010100110")); // → false console.log(notBinary.test("1100100010200110")); // → true
パターンの部分の繰り返し
これで、単一の数字にマッチする方法がわかった。では、1つ以上の数字のシーケンスである整数全体にマッチさせたい場合はどうすればよいだろうか?
正規表現で何かの後にプラス記号 (+
) を付けると、その要素が複数回繰り返される可能性があることを示す。したがって、/\d+/
は1つ以上の数字の文字にマッチする。
console.log(/'\d+'/.test("'123'")); // → true console.log(/'\d+'/.test("''")); // → false console.log(/'\d*'/.test("'123'")); // → true console.log(/'\d*'/.test("''")); // → true
アスタリスク (*
) は同様の意味を持つが、パターンが0回マッチすることも許可する。アスタリスクが付いたものは、マッチを妨げることはない。マッチする適切なテキストが見つからない場合は、0個のインスタンスにマッチするだけである。
疑問符は、パターンの部分を「オプション」にし、0回または1回発生する可能性があることを意味する。次の例では、u 文字が発生することが許可されているが、パターンはそれが欠落している場合にもマッチする。
var neighbor = /neighbou?r/; console.log(neighbor.test("neighbour")); // → true console.log(neighbor.test("neighbor")); // → true
パターンが正確な回数発生する必要があることを示すには、中かっこを使用する。たとえば、要素の後に{4}
を付けると、正確に4回発生する必要がある。この方法で範囲を指定することも可能である。{2,4}
は、要素が少なくとも2回、最大4回発生する必要があることを意味する。
以下は、1桁と2桁の両方の日、月、および時間を許可する、日付と時刻のパターンの別のバージョンである。また、わずかに読みやすくなっている。
var dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/; console.log(dateTime.test("30-1-2003 8:45")); // → true
中かっこを使用するときは、カンマの後の数字を省略することで、オープンエンドの範囲も指定できる。そのため、{5,}
は5回以上を意味する。
部分式のグループ化
一度に複数の要素に対して*
や+
のような演算子を使用するには、かっこを使用できる。かっこで囲まれた正規表現の一部は、それに続く演算子に関する限り、単一の要素としてカウントされる。
var cartoonCrying = /boo+(hoo+)+/i; console.log(cartoonCrying.test("Boohoooohoohooo")); // → true
最初の+
文字と2番目の+
文字は、それぞれbooとhooの2番目のoにのみ適用される。3番目の+
はグループ全体(hoo+)
に適用され、そのようなシーケンスを1つ以上マッチさせる。
前の例の式の末尾にあるi
により、この正規表現は大文字と小文字を区別しなくなり、パターン自体はすべて小文字であるにもかかわらず、入力文字列の大文字のBにマッチさせることができる。
マッチとグループ
test
メソッドは、正規表現をマッチさせるための最も単純な方法である。これは、マッチしたかどうかだけを示し、それ以外のことは示さない。正規表現には、exec
(実行)メソッドもある。これは、マッチが見つからなかった場合はnull
を返し、そうでない場合はマッチに関する情報を含むオブジェクトを返す。
var match = /\d+/.exec("one two 100"); console.log(match); // → ["100"] console.log(match.index); // → 8
exec
から返されたオブジェクトには、マッチが成功した文字列のどこから始まるかを示すindex
プロパティがある。それ以外の場合、オブジェクトは(実際には)文字列の配列のように見え、最初の要素はマッチした文字列である。前の例では、これは探していた数字のシーケンスである。
console.log("one two 100".match(/\d+/)); // → ["100"]
正規表現にかっこでグループ化された部分式が含まれている場合、それらのグループにマッチしたテキストも配列に表示される。マッチ全体が常に最初の要素である。次の要素は、最初のグループ(式の先頭のかっこが最初にあるもの)にマッチした部分、次に2番目のグループ、というようになる。
var 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"]
グループは、文字列の一部を抽出するのに役立ちます。文字列に日付が含まれているかどうかを検証するだけでなく、それを抽出して日付を表すオブジェクトを構築したい場合は、数字パターンを括弧で囲み、exec
の結果から直接日付を取り出すことができます。
ただし、その前に、JavaScriptで日付と時刻の値を格納する推奨方法について簡単に説明します。
日付型
JavaScriptには、日付、正確には時間を表すための標準的なオブジェクト型があります。それはDate
と呼ばれます。単にnew
を使用して日付オブジェクトを作成すると、現在の日時を取得できます。
console.log(new Date()); // → Wed Dec 04 2013 14:24:57 GMT+0100 (CET)
console.log(new Date(2009, 11, 9)); // → Wed Dec 09 2009 00:00:00 GMT+0100 (CET) console.log(new Date(2009, 11, 9, 12, 59, 59, 999)); // → Wed Dec 09 2009 12:59:59 GMT+0100 (CET)
JavaScriptでは、月の番号はゼロから始まる(したがって、12月は11)という規則を使用していますが、日の番号は1から始まります。これは紛らわしくて馬鹿げています。注意してください。
最後の4つの引数(時、分、秒、ミリ秒)はオプションであり、指定しない場合はゼロと見なされます。
タイムスタンプは、1970年の開始からのミリ秒数として保存され、1970年より前の時間には負の数が使用されます(その頃に発明された「Unix時間」によって設定された規則に従っています)。日付オブジェクトのgetTime
メソッドは、この数値を返します。想像できるように、これは大きな数値です。
console.log(new Date(2013, 11, 19).getTime()); // → 1387407600000 console.log(new Date(1387407600000)); // → Thu Dec 19 2013 00:00:00 GMT+0100 (CET)
Date
コンストラクターに単一の引数を渡すと、その引数はミリ秒数として扱われます。新しいDate
オブジェクトを作成してgetTime
を呼び出すことで、現在のミリ秒数を取得できますが、Date.now
関数を呼び出すことでも取得できます。
日付オブジェクトには、getFullYear
、getMonth
、getDate
、getHours
、getMinutes
、getSeconds
などのメソッドがあり、それらのコンポーネントを抽出できます。また、getYear
もありますが、これはあまり役に立たない2桁の年(93
や14
など)を返します。
関心のある式の部分を括弧で囲むことで、文字列から日付オブジェクトを簡単に作成できるようになりました。
function findDate(string) { var dateTime = /(\d{1,2})-(\d{1,2})-(\d{4})/; var match = dateTime.exec(string); return new Date(Number(match[3]), Number(match[2]) - 1, Number(match[1])); } console.log(findDate("30-1-2003")); // → Thu Jan 30 2003 00:00:00 GMT+0100 (CET)
単語と文字列の境界
残念ながら、findDate
は、文字列"100-1-30000"
から無意味な日付00-1-3000も喜んで抽出します。マッチは文字列内のどこでも発生する可能性があるため、この場合は2番目の文字から始まり、最後から2番目の文字で終わります。
マッチが文字列全体に及ぶように強制したい場合は、マーカー^
と$
を追加できます。キャレットは入力文字列の先頭に一致し、ドル記号は末尾に一致します。したがって、/^\d+$/
は1つ以上の数字だけで構成される文字列に一致し、/^!/
は感嘆符で始まる任意の文字列に一致し、/x^/
はいずれの文字列にも一致しません(文字列の先頭の前に*x*が存在することはあり得ないからです)。
一方、日付が単語境界で開始および終了することを確認したいだけの場合は、マーカー\b
を使用できます。単語境界は、文字列の先頭または末尾、または一方の側に単語文字(\w
など)があり、もう一方の側に非単語文字がある文字列内の任意のポイントにすることができます。
console.log(/cat/.test("concatenate")); // → true console.log(/\bcat\b/.test("concatenate")); // → false
境界マーカーは実際の文字を表すものではないことに注意してください。これは、パターン内で表示される場所で特定の条件が満たされる場合にのみ、正規表現が一致することを強制するだけです。
選択パターン
テキストに数字だけでなく、数字の後に*pig*、*cow*、または*chicken*のいずれかの単語、またはそれらの複数形が続くかどうかを知りたいとします。
3つの正規表現を記述して順番にテストすることもできますが、より良い方法があります。パイプ文字(|
)は、その左側のパターンと右側のパターンの間の選択を示します。したがって、次のように記述できます。
var animalCount = /\b\d+ (pig|cow|chicken)s?\b/; console.log(animalCount.test("15 pigs")); // → true console.log(animalCount.test("15 pigchickens")); // → false
括弧は、パイプ演算子が適用されるパターンの部分を制限するために使用でき、複数のそのような演算子を互いに隣り合わせに配置して、3つ以上のパターン間の選択を表現できます。
マッチングのメカニズム
正規表現は、フロー図として考えることができます。これは、前の例の家畜式の図です。
図の左側から右側へのパスが見つかれば、式は文字列に一致します。文字列内の現在の位置を保持し、ボックスを通過するたびに、現在の位置の後の文字列の部分がそのボックスに一致することを確認します。
したがって、正規表現で"the 3 pigs"
をマッチさせようとすると、フローチャートの進捗状況は次のようになります。
-
位置5では、1つのパスは2番目の(数字)ボックスの前にループバックし、もう1つのパスは単一の空白文字を保持するボックスを通過します。ここには数字ではなく空白があるため、2番目のパスを選択する必要があります。
-
これで、位置6(「pigs」の開始)と、図の3方向分岐にいます。ここには「cow」または「chicken」は見られませんが、「pig」は見られるので、その分岐を選択します。
-
位置9では、3方向分岐の後、1つのパスは*s*ボックスをスキップして最後の単語境界に直接進み、もう1つのパスは*s*に一致します。ここには単語境界ではなく*s*文字があるため、*s*ボックスを通過します。
-
位置10(文字列の末尾)にいるため、単語境界にのみ一致できます。文字列の末尾は単語境界としてカウントされるため、最後のボックスを通過し、この文字列を正常に一致させました。
概念的には、正規表現エンジンは、次のように文字列内のマッチを検索します。文字列の先頭から開始し、そこでマッチを試みます。この場合、そこには単語境界があるため、最初のボックスは通過しますが、数字がないため、2番目のボックスで失敗します。次に、文字列の2番目の文字に進み、そこで新しいマッチを開始しようとします...そして、マッチが見つかるか、文字列の末尾に到達して、実際にはマッチがないと判断するまで続けます。
バックトラッキング
正規表現/\b([01]+b|\d+|[\da-f]+h)\b/
は、*b*が続く2進数、接尾辞文字のない通常の10進数、または*h*が続く16進数(つまり、16進数、10から15の数字を表す文字*a*から*f*を使用)のいずれかに一致します。これが対応する図です。
この式をマッチングすると、入力に実際には2進数が含まれていなくても、上部(2進数)分岐に入ることがよくあります。たとえば、文字列"103"
をマッチングするとき、3で現在間違った分岐にいることが明らかになります。文字列は式に*一致します*が、現在いる分岐には一致しません。
したがって、マッチャーは*バックトラック*します。分岐に入るとき、現在の位置(この場合は、図の最初の境界ボックスのすぐ後の、文字列の先頭)を覚えておき、現在の分岐がうまくいかない場合は戻って別の分岐を試すことができるようにします。文字列"103"
の場合、3文字に遭遇した後、10進数の分岐を試み始めます。この分岐は一致するため、結局一致が報告されます。
マッチャーは完全一致が見つかるとすぐに停止します。これは、複数の分岐が文字列に一致する可能性がある場合、最初の分岐(正規表現で分岐が表示される順序で並べられます)のみが使用されることを意味します。
バックトラッキングは、+や*
などの繰り返し演算子でも発生します。/^.*x/
を"abcxe"
とマッチングすると、.*
の部分は最初に文字列全体を消費しようとします。次に、エンジンはパターンに一致させるために*x*が必要なことに気付きます。文字列の末尾を過ぎても*x*がないため、アスタリスク演算子は1文字少なく一致させようとします。しかし、マッチャーはabcx
の後に*x*も見つけられないため、再度バックトラックし、アスタリスク演算子をabc
のみに一致させます。*これで*必要な場所に*x*が見つかり、位置0から4までのマッチが成功したと報告します。
非常に多くのバックトラッキングを行う正規表現を作成することは可能です。この問題は、パターンがさまざまな方法で入力の一部に一致できる場合に発生します。たとえば、2進数の正規表現の記述中に混乱した場合、誤って/([01]+)+b/
のようなものを記述してしまう可能性があります。
もし、末尾にbの文字がない、0と1の長い列にマッチしようとすると、マッチャーは最初に数字がなくなるまで内側のループを処理します。次に、bがないことに気づくと、1つ前の位置に戻り、外側のループを1回実行し、再び諦め、内側のループからさらに1つ前の位置に戻ろうとします。この2つのループを可能なすべての経路で試行し続けます。これは、文字が追加されるごとに作業量が倍増することを意味します。数十文字の場合でも、結果のマッチには事実上永遠に時間がかかります。
replaceメソッド
文字列値には、文字列の一部を別の文字列で置換するために使用できるreplace
メソッドがあります。
console.log("papa".replace("p", "m")); // → mapa
最初の引数は正規表現でも構いません。その場合、正規表現の最初のマッチが置換されます。正規表現にg
オプション(グローバルの意味)を追加すると、文字列内のすべてのマッチが置換され、最初のマッチだけではありません。
console.log("Borobudur".replace(/[ou]/, "a")); // → Barobudur console.log("Borobudur".replace(/[ou]/g, "a")); // → Barabadar
1つのマッチを置換するか、すべてのマッチを置換するかの選択が、replace
への追加の引数または別のメソッドreplaceAll
を提供することによって行われるのが賢明だったでしょう。しかし、残念なことに、その選択は正規表現のプロパティに依存しています。
replace
で正規表現を使用することの真の力は、置換文字列でマッチしたグループを参照できるという事実にあります。たとえば、Lastname, Firstname
形式で、1行に1人の名前が含まれる大きな文字列があるとします。これらの名前を交換し、コンマを削除して単純なFirstname Lastname
形式にするには、次のコードを使用できます。
console.log( "Hopper, Grace\nMcCarthy, John\nRitchie, Dennis" .replace(/([\w ]+), ([\w ]+)/g, "$2 $1")); // → Grace Hopper // John McCarthy // Dennis Ritchie
置換文字列の$1
と$2
は、パターン内の括弧で囲まれたグループを参照します。$1
は最初のグループにマッチしたテキストで置換され、$2
は2番目のグループで置換されます。以下、$9
まで続きます。マッチ全体は$&
で参照できます。
replace
の2番目の引数として、文字列ではなく関数を渡すこともできます。置換ごとに、マッチしたグループ(およびマッチ全体)を引数として関数が呼び出され、その戻り値が新しい文字列に挿入されます。
var s = "the cia and fbi"; console.log(s.replace(/\b(fbi|cia)\b/g, function(str) { return str.toUpperCase(); })); // → the CIA and FBI
var stock = "1 lemon, 2 cabbages, and 101 eggs"; function minusOne(match, amount, unit) { amount = Number(amount) - 1; if (amount == 1) // only one left, remove the 's' unit = unit.slice(0, unit.length - 1); else if (amount == 0) amount = "no"; return amount + " " + unit; } console.log(stock.replace(/(\d+) (\w+)/g, minusOne)); // → no lemon, 1 cabbage, and 100 eggs
これは、文字列を受け取り、数字とそれに続く英数字の単語のすべての出現箇所を見つけ、そのような出現箇所を1つずつ減らした文字列を返します。
(\d+)
グループは関数のamount
引数になり、(\w+)
グループはunit
にバインドされます。関数はamount
を数値に変換します(これは常に機能します。なぜなら\d+
にマッチしたからです)。そして、1つまたはゼロしか残っていない場合にいくつかの調整を行います。
欲張り
replace
を使用して、JavaScriptコードからすべてのコメントを削除する関数を記述するのは難しくありません。これが最初の試みです。
function stripComments(code) { return code.replace(/\/\/.*|\/\*[^]*\*\//g, ""); } console.log(stripComments("1 + /* 2 */3")); // → 1 + 3 console.log(stripComments("x = 10;// ten!")); // → x = 10; console.log(stripComments("1 /* a */+/* b */ 1")); // → 1 1
or演算子の前の部分は、2つのスラッシュ文字の後に、改行文字以外の任意の数の文字が続くものにマッチします。複数行コメントの部分はより複雑です。[^]
(文字の空集合に含まれない任意の文字)を、任意の文字にマッチする方法として使用します。ここではドットを使用することはできません。なぜなら、ブロックコメントは新しい行で続くことがあり、ドットは改行文字にマッチしないからです。
しかし、前の例の出力は間違っているようです。なぜでしょうか?
私がバックトラッキングのセクションで説明したように、式の[^]*
部分は最初に可能な限り多くマッチします。これにより、パターンの次の部分が失敗すると、マッチャーは1文字戻ってそこから再試行します。例では、マッチャーは最初に文字列の残りの部分全体にマッチしようとし、そこから戻ります。4文字戻った後に*/
の出現を見つけてマッチします。これは私たちが望んでいたものではありません。目的は、1つのコメントにマッチすることで、コードの最後まで進んで最後のブロックコメントの終わりを見つけることではありません。
この動作のため、反復演算子(+
、*
、?
、および{}
)は欲張りであると言います。つまり、可能な限り多くマッチし、そこからバックトラックします。それらの後に疑問符を付けると(+?
、*?
、??
、{}?
)、それらは欲張りでなくなり、可能な限り少ないマッチから開始し、残りのパターンがより小さいマッチに適合しない場合にのみ、より多くマッチします。
そして、それはまさにこの場合に私たちが望むものです。アスタリスクに*/
につながる最小の文字の範囲にマッチさせることで、1つのブロックコメントのみを消費します。
function stripComments(code) { return code.replace(/\/\/.*|\/\*[^]*?\*\//g, ""); } console.log(stripComments("1 /* a */+/* b */ 1")); // → 1 + 1
正規表現プログラムの多くのバグは、欲張り演算子を意図せずに使用したことが原因で、欲張りでない演算子の方がうまく機能する場合があります。反復演算子を使用する場合は、最初に欲張りでないバリアントを検討してください。
RegExpオブジェクトの動的な作成
コードを作成するときに、マッチする必要がある正確なパターンがわからない場合があります。テキストの中でユーザーの名前を探し、それを強調するためにアンダースコア文字で囲みたいとします。プログラムが実際に実行されるまで名前がわからないため、スラッシュベースの表記を使用することはできません。
ただし、文字列を作成し、その文字列でRegExp
コンストラクターを使用できます。例を次に示します。
var name = "harry"; var text = "Harry is a suspicious character."; var regexp = new RegExp("\\b(" + name + ")\\b", "gi"); console.log(text.replace(regexp, "_$1_")); // → _Harry_ is a suspicious character.
\b
境界マーカーを作成するときは、スラッシュで囲まれた正規表現ではなく、通常の文字列で記述しているため、2つのバックスラッシュを使用する必要があります。RegExp
コンストラクターの2番目の引数には、正規表現のオプションが含まれています。この場合は、グローバルおよび大文字と小文字を区別しないための"gi"
です。
しかし、ユーザーがオタクのティーンエイジャーであるため、名前が"dea+hl[]rd"
の場合はどうでしょうか?それは、実際にはユーザーの名前と一致しない、意味をなさない正規表現になります。
これを回避するために、信頼できない文字の前にバックスラッシュを追加できます。アルファベット文字の前にバックスラッシュを追加することは、\b
や\n
のようなものが特別な意味を持つため、悪い考えです。ただし、英数字または空白でないものをすべてエスケープするのは安全です。
var name = "dea+hl[]rd"; var text = "This dea+hl[]rd guy is super annoying."; var escaped = name.replace(/[^\w\s]/g, "\\$&"); var regexp = new RegExp("\\b(" + escaped + ")\\b", "gi"); console.log(text.replace(regexp, "_$1_")); // → This _dea+hl[]rd_ guy is super annoying.
searchメソッド
文字列のindexOf
メソッドは、正規表現で呼び出すことはできません。しかし、正規表現を期待する別のメソッドsearch
があります。indexOf
と同様に、式が見つかった最初のインデックスを返し、見つからなかった場合は-1を返します。
console.log(" word".search(/\S/)); // → 2 console.log(" ".search(/\S/)); // → -1
残念ながら、(indexOf
への2番目の引数で可能なように)マッチが特定のオフセットから開始する必要があることを示す方法はなく、それはしばしば役立つでしょう。
lastIndexプロパティ
同様に、exec
メソッドは、文字列内の特定の場所からの検索を開始するための便利な方法を提供しません。しかし、不便な方法を提供します。
正規表現オブジェクトにはプロパティがあります。そのようなプロパティの1つはsource
で、式が作成された元の文字列が含まれています。もう1つのプロパティはlastIndex
で、限定された状況下で次のマッチが開始される場所を制御します。
これらの状況とは、正規表現がグローバル(g
)オプションを有効にしており、マッチがexec
メソッドを介して発生する必要があるということです。繰り返しになりますが、より健全な解決策は、exec
に渡す追加の引数を許可することだったでしょうが、正気はJavaScriptの正規表現インターフェイスを定義する特性ではありません。
var pattern = /y/g; pattern.lastIndex = 3; var match = pattern.exec("xyzzy"); console.log(match.index); // → 4 console.log(pattern.lastIndex); // → 5
マッチが成功した場合、exec
の呼び出しは、lastIndex
プロパティをマッチの後にポイントするように自動的に更新します。マッチが見つからなかった場合、lastIndex
はゼロに戻ります。これは、新しく構築された正規表現オブジェクトが持つ値でもあります。
複数のexec
呼び出しにグローバルな正規表現値を使用すると、lastIndex
プロパティへのこれらの自動更新が問題を引き起こす可能性があります。正規表現が、前の呼び出しから残ったインデックスから誤って開始されている可能性があります。
var digit = /\d/g; console.log(digit.exec("here it is: 1")); // → ["1"] console.log(digit.exec("and now: 1")); // → null
グローバルオプションのもう1つの興味深い効果は、文字列のmatch
メソッドの動作を変更することです。グローバルな式で呼び出すと、exec
によって返されるものに似た配列を返す代わりに、match
は文字列内のパターンのすべてのマッチを見つけ、マッチした文字列を含む配列を返します。
console.log("Banana".match(/an/g)); // → ["an", "an"]
したがって、グローバルな正規表現には注意が必要です。それらが必要なケース(replace
の呼び出しや、lastIndex
を明示的に使用する場合など)は、通常、それらを使用したい唯一の場所です。
マッチのループ処理
一般的なパターンは、lastIndex
とexec
を使用して、ループ本体でマッチオブジェクトにアクセスできる方法で、文字列内のパターンのすべての出現箇所をスキャンすることです。
var input = "A string with 3 numbers in it... 42 and 88."; var number = /\b(\d+)\b/g; var match; while (match = number.exec(input)) console.log("Found", match[1], "at", match.index); // → Found 3 at 14 // Found 42 at 33 // Found 88 at 40
これは、代入式(=
)の値が代入された値であるという事実を利用したものです。したがって、while
文の条件としてmatch = number.exec(input)
を使用することで、各反復の開始時にマッチを実行し、その結果を変数に保存し、マッチが見つからなくなるとループを停止します。
INIファイルの解析
章を締めくくるにあたり、正規表現が必要となる問題を見ていきましょう。インターネットから敵に関する情報を自動的に収集するプログラムを作成していると想像してください。(ここでは実際にそのプログラムを作成するのではなく、設定ファイルを読み込む部分だけを作成します。がっかりさせて申し訳ありません。)設定ファイルは次のようになります。
searchengine=http://www.google.com/search?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 [gargamel] fullname=Gargamel type=evil sorcerer outputdir=/home/marijn/enemies/gargamel
この形式(実際には広く使用されている形式で、通常はINIファイルと呼ばれます)の正確なルールは次のとおりです。
私たちのタスクは、このような文字列を、それぞれname
プロパティと設定の配列を持つオブジェクトの配列に変換することです。各セクションに1つずつ、そして最上位のグローバル設定用に1つ、このようなオブジェクトが必要になります。
この形式は行ごとに処理する必要があるため、ファイルを個別の行に分割するのが良い出発点です。 第6章でstring.split("\n")
を使用してこれを行いました。ただし、一部のオペレーティングシステムでは、行を区切るために改行文字だけでなく、キャリッジリターン文字の後に改行文字("\r\n"
)を使用します。 split
メソッドは正規表現を引数として許可しているため、/\r?\n/
のような正規表現で分割して、行間に"\n"
と"\r\n"
の両方を許可する方法で分割できます。
function parseINI(string) { // Start with an object to hold the top-level fields var currentSection = {name: null, fields: []}; var categories = [currentSection]; string.split(/\r?\n/).forEach(function(line) { var match; if (/^\s*(;.*)?$/.test(line)) { return; } else if (match = line.match(/^\[(.*)\]$/)) { currentSection = {name: match[1], fields: []}; categories.push(currentSection); } else if (match = line.match(/^(\w+)=(.*)$/)) { currentSection.fields.push({name: match[1], value: match[2]}); } else { throw new Error("Line '" + line + "' is invalid."); } }); return categories; }
このコードは、ファイル内のすべての行を調べて、「現在のセクション」オブジェクトを更新します。まず、/^\s*(;.*)?$/
式を使用して、行を無視できるかどうかを確認します。どのように機能するか分かりますか?括弧で囲まれた部分はコメントに一致し、?
は空白のみを含む行にも一致するようにします。
行がコメントでない場合、コードは行が新しいセクションを開始するかどうかを確認します。その場合、新しい現在のセクションオブジェクトを作成し、後続の設定が追加されます。
最後に意味のある可能性は、行が通常のセッティングであり、コードが現在のセクションオブジェクトに追加することです。
行がこれらの形式のいずれにも一致しない場合、関数はエラーをスローします。
式が一部ではなく行全体に一致するように、^
と$
が繰り返し使用されていることに注意してください。これらを省略すると、ほとんどの場合機能しますが、一部の入力に対して奇妙な動作をするコードになります。これは追跡が難しいバグになる可能性があります。
パターンif (match = string.match(...))
は、while
の条件として代入を使用するトリックに似ています。 match
の呼び出しが成功するかどうか確信が持てないことが多いため、このテストを行うif
ステートメント内でのみ結果のオブジェクトにアクセスできます。 if
フォームの快適なチェーンを壊さないために、マッチの結果を変数に代入し、その代入をif
ステートメントのテストとしてすぐに使用します。
国際文字
JavaScriptの最初の単純な実装と、この単純なアプローチが後に標準的な動作として定められたという事実のため、JavaScriptの正規表現は、英語にない文字についてはかなり鈍感です。たとえば、JavaScriptの正規表現に関する限り、「単語文字」は、ラテンアルファベットの26文字(大文字または小文字)と、何らかの理由でアンダースコア文字のみです。 éやβなどの、間違いなく単語文字であるものは、\w
には一致せず(そして、非単語カテゴリである大文字の\W
には一致します)。
奇妙な歴史的な偶然により、\s
(空白)にはこの問題はなく、Unicode標準が空白と見なす、改行しないスペースやモンゴル語の母音区切り文字などのすべての文字に一致します。
他のプログラミング言語の一部の正規表現の実装には、「すべての大文字」、「すべての句読点」、「制御文字」などの特定のUnicode文字カテゴリに一致する構文があります。このようなカテゴリのサポートをJavaScriptに追加する計画がありますが、残念ながら近い将来に実現する見込みはないようです。
まとめ
正規表現は、文字列内のパターンを表すオブジェクトです。独自の構文を使用してこれらのパターンを表現します。
/abc/ |
文字のシーケンス |
/[abc]/ |
文字セットからの任意の文字 |
/[^abc]/ |
文字セットにない任意の文字 |
/[0-9]/ |
文字範囲の任意の文字 |
/x+/ |
パターンx の1回以上の出現 |
/x+?/ |
1回以上の出現、非貪欲 |
/x*/ |
0回以上の出現 |
/x?/ |
0回または1回の出現 |
/x{2,4}/ |
2回から4回の出現 |
/(abc)/ |
グループ |
/a|b|c/ |
複数のパターンのいずれか |
/\d/ |
任意の数字文字 |
数字 |
英数字(「単語文字」) |
単語文字 |
任意の空白文字 |
/./ |
改行を除く任意の文字 |
/\b/ |
単語の境界 |
/^/ |
入力の開始 |
/$/ |
入力の終了 |
正規表現には、指定された文字列が一致するかどうかをテストするtest
メソッドがあります。また、一致が見つかった場合は、一致したすべてのグループを含む配列を返すexec
メソッドがあります。このような配列には、一致が開始された場所を示すindex
プロパティがあります。
文字列には、正規表現に一致させるためのmatch
メソッドと、正規表現を検索して一致の開始位置のみを返すsearch
メソッドがあります。 replace
メソッドは、パターンのマッチを置換文字列で置き換えることができます。または、replace
に関数を渡すこともできます。この関数は、マッチテキストとマッチしたグループに基づいて置換文字列を作成するために使用されます。
正規表現には、終了スラッシュの後に記述されるオプションを含めることができます。 i
オプションはマッチを大文字と小文字を区別しないようにし、g
オプションは式をグローバルにします。これにより、特に、replace
メソッドは最初のインスタンスだけでなく、すべてのインスタンスを置き換えるようになります。
RegExp
コンストラクターを使用して、文字列から正規表現値を作成できます。
正規表現は扱いにくいハンドルが付いた鋭いツールです。一部のタスクを非常に簡素化しますが、複雑な問題に適用するとすぐに管理不能になる可能性があります。それらの使い方を知ることの一部は、それらがまともな形で表現できないものを無理に押し込もうとする衝動に抵抗することです。
練習問題
これらの練習に取り組む過程で、正規表現の不可解な動作によって混乱し、不満を感じることはほぼ避けられません。場合によっては、debuggex.comのようなオンラインツールに式を入力して、その視覚化が意図したものと対応しているかどうかを確認し、さまざまな入力文字列にどのように応答するかを実験すると役立つ場合があります。
正規表現ゴルフ
コードゴルフは、特定のプログラムをできるだけ少ない文字数で表現しようとするゲームに使用される用語です。同様に、正規表現ゴルフは、与えられたパターンに一致する、できるだけ小さな正規表現を記述する練習であり、そのパターンのみに一致させることです。
次の各項目について、指定されたいずれかのサブストリングが文字列に出現するかどうかをテストする正規表現を記述してください。正規表現は、記述されたサブストリングの1つを含む文字列のみに一致する必要があります。明示的に言及されない限り、単語の境界について心配する必要はありません。式が機能したら、さらに小さくできるかどうかを確認してください。
ヘルプについては、章のまとめの表を参照してください。いくつかのテスト文字列で各解決策をテストします。
// Fill in the regular expressions verify(/.../, ["my car", "bad cats"], ["camper", "high art"]); verify(/.../, ["pop culture", "mad props"], ["plop"]); verify(/.../, ["ferret", "ferry", "ferrari"], ["ferrum", "transfer A"]); verify(/.../, ["how delicious", "spacious room"], ["ruinous", "consciousness"]); verify(/.../, ["bad punctuation ."], ["escape the dot"]); verify(/.../, ["hottentottententen"], ["no", "hotten totten tenten"]); verify(/.../, ["red platypus", "wobbling nest"], ["earth bed", "learning ape"]); function verify(regexp, yes, no) { // Ignore unfinished exercises if (regexp.source == "...") return; yes.forEach(function(s) { if (!regexp.test(s)) console.log("Failure to match '" + s + "'"); }); no.forEach(function(s) { if (regexp.test(s)) console.log("Unexpected match for '" + s + "'"); }); }
引用符スタイル
あなたが物語を書き、対話部分をマークするために全体を通してシングルクォーテーションマークを使ったと想像してください。今、あなたは対話の引用符をすべてダブルクォーテーションマークに置き換えたいと考えていますが、aren’tのような短縮形に使われているシングルクォーテーションマークは保持したいとします。
これら2種類の引用符の使用法を区別するパターンを考え、適切な置換を行うreplace
メソッドの呼び出しを作成してください。
var text = "'I'm the cook,' he said, 'it's my job.'"; // Change this call. console.log(text.replace(/A/g, "B")); // → "I'm the cook," he said, "it's my job."
再び数字
一連の数字は、単純な正規表現/\d+/
で一致させることができます。
JavaScriptスタイルの数字のみに一致する式を作成してください。数字の前にオプションのマイナスまたはプラス記号、小数点、指数表記 (5e-3
または1E10
) をサポートする必要があります。また、指数の前にはオプションの符号が付いていることに注意してください。また、ドットの前または後に数字がある必要はありませんが、数字はドット単独であってはなりません。つまり、.5
と5.
は有効なJavaScriptの数字ですが、単独のドットは有効ではありません。
// Fill in this regular expression. var number = /^...$/; // Tests: ["1", "-1", "+15", "1.55", ".5", "5.", "1.3e2", "1E-4", "1e+12"].forEach(function(s) { if (!number.test(s)) console.log("Failed to match '" + s + "'"); }); ["1a", "+-1", "1.2.3", "1+1", "1e4.5", ".5.", "1f5", "."].forEach(function(s) { if (number.test(s)) console.log("Incorrectly accepted '" + s + "'"); });