正規表現とは何か?一言でまとめると:正規表現はマッチングパターンであり、文字をマッチさせるか、位置をマッチさせるかのいずれかです。
文字のマッチング#
あいまいマッチング#
正規表現は正確なマッチングだけでなく、あいまいなマッチングも実現できます。あいまいなマッチングは横方向と縦方向に分かれます。
横方向のあいまいマッチング#
横方向のあいまいマッチングとは、正規表現がマッチできる文字列の長さが固定されていないことを指します。その実現方法は量詞を使用することで、例えば {m, n} は連続して最小 m 回、最大 n 回出現することを示します。
const regex = /ab{2,4}c/g
const string = 'abc abbc abbbc abbbbc abbbbbc'
console.log(string.match(regex)) // ["abbc", "abbbc", "abbbbc"]
正規表現の g
修飾子はグローバルマッチを示し、「すべて」を強調し、「最初の」ではありません。
// グローバル修飾子がない場合
const regex = /ab{2,4}c/
const string = 'abc abbc abbbc abbbbc abbbbbc'
console.log(string.match(regex))
// ["abbc", index: 4, input: "abc abbc abbbc abbbbc abbbbbc", groups: undefined]
縦方向のあいまいマッチング#
縦方向のあいまいマッチングとは、正規表現がマッチする文字列において、特定の位置の文字が特定の文字でない場合を指します。その実現方法は文字クラスを使用し、例えば [abc]
はその文字が "a"、"b"、"c" のいずれかであることを示します。
const regex = /a[123]b/g
const string = 'a0b a1b a2b a3b a4b'
console.log(string.match(regex)) // ["a1b", "a2b", "a3b"]
文字クラス#
文字クラスと呼ばれますが、実際にはマッチするのは 1 文字だけです。例えば、文字クラス [abc]
は 1 文字だけをマッチします。文字クラスには範囲表現、除外法、簡略形式があります。
範囲表現#
文字クラス [0-9a-zA-Z]
は数字と大文字小文字の任意の 1 文字を示します。ハイフン "-" には特別な意味があるため、"a"、"-"、"c" のいずれかの文字をマッチさせるには次のように書くことができます:[-az]、[az-]、[a\-z]
、ハイフンは先頭または末尾に置くか、エスケープする必要があります。
除外文字クラス#
除外文字クラス(反義文字クラス)は "a"、"b"、"c" 以外の任意の 1 文字を示します。文字クラスの最初の位置に ^
(キャレット)を置くことで反転を示します。^ は範囲表現と組み合わせて使用できます。
簡略形式#
正規表現の簡略形式は以下の通りです:
文字クラス | 意味 |
---|---|
\d | [0-9]、数字を示す |
\D | [^0-9]、非数字を示す |
\w | [0-9a-zA-Z_]、数字、大文字小文字の文字とアンダースコアを示す |
\W | [^0-9a-zA-Z_]、非単語文字を示す |
\s | [\t\v\n\r\f]、空白文字を示す |
\S | [^ \t\v\n\r\f]、非空白文字を示す |
. | ワイルドカード |
注意が必要です:[ \t\v\n\r\f]
はそれぞれ空白文字、水平タブ、垂直タブ、改行、キャリッジリターン、ページ送りを示します。
ワイルドカード .
はほぼ任意の文字を示すことができます。改行、キャリッジリターン、行区切り、段落区切りを除きます。任意の文字をマッチさせたい場合は、組み合わせの書き方を使用できます:[\d\D]
、[\w\W]
、[\s\S]
および [^]
のいずれかを使用します。
量詞#
簡略形式#
量詞 | 意味 |
---|---|
{m, n} | m 回から n 回出現することを示す |
{m,} | 最低 m 回出現することを示す |
{m} | {m, m} と同等で、m 回出現することを示す |
? | {0, 1} と同等で、出現するかしないかを示す |
+ | {1,} と同等で、少なくとも 1 回出現することを示す |
\* | {0,} と同等で、任意の回数出現することを示す |
貪欲マッチと惰性マッチ#
貪欲マッチはできるだけ多くをマッチさせます。以下のように表現されます:
const regex = /\d{2,5}/g
const string = '123 1234 12345 123456'
console.log(string.match(regex))
// ["123", "1234", "12345", "12345"]
量詞の後に ?
を加えることで惰性マッチを実現し、できるだけ少なくマッチさせます。以下のように表現されます:
const regex = /\d{2,5}?/g
const string = '123 1234 12345 123456'
console.log(string.match(regex))
// ["12", "12", "34", "12", "34", "12", "34", "56"]
複数選択分岐#
複数選択分岐は複数のサブパターンのいずれかをサポートします。具体的な形式は次の通りです:(p1|p2|p3)、ここで p1、p2、p3 はサブパターンで、|
(パイプ)で区切られています。注意が必要です:複数選択分岐は左から右に惰性マッチされ、前のマッチが成功した後は後のパターンは試みられません。サブパターンの順序を変更することでマッチの結果を変えることができます。
const regex = /good|goodbye/g
const string = 'goodbye'
console.log(string.match(regex))
// ["good"]
実例応用#
ファイルパスのマッチング#
ファイルパスの形式は ドライブ:\フォルダ\フォルダ\フォルダ\
です。ドライブをマッチさせるには:[a-zA-Z]:\\
。ファイル名またはフォルダ名をマッチさせるには、特定の特殊文字を含まない必要があり、合法な文字を示すために除外文字クラスを使用し、ファイル名またはフォルダ名は空であってはならず、少なくとも 1 文字が必要で、量詞 +
を使用します。フォルダは任意の回数出現でき、最後はファイルである可能性があり、フォルダであってはならず、\\
を付ける必要はありません。
const regex = /^[a-zA-Z]:\\([^\\:*<>|"?\r\n/]+\\)*([^\\:*<>|"?\r\n/]+)?$/
console.log(regex.test('F:\\study\\regular expression.pdf'))
id のマッチング#
const regex = /id=".*"/
const string = '<div id="container" class="main"></div>'
// id="container" class="main"
量詞 .
はワイルドカードで、ダブルクォーテーションをマッチさせることができ、貪欲マッチでもあるため、エラーが発生します。これを惰性マッチに変更できます:
const regex = /id=".*?"/
しかし、上記の正規表現は効率が悪いです。なぜなら、そのマッチング原理は「バックトラッキング」の概念を設計しているからです。最適解は次の通りです:
const regex = /id="[^"]*"/
位置のマッチング#
位置の概念#
位置(アンカー)は隣接する文字の間の位置です。位置は空の文字列として理解できます。ES5 では、合計で 6 つのアンカーがあります:^
、$
、\b
、\B
、(?=p)
、(?!p)
。
^
は先頭をマッチし、複数行マッチの場合は行の先頭をマッチします。&
は末尾をマッチし、複数行マッチの場合は行の末尾をマッチします。\b
は単語境界をマッチし、つまり\w
と\W
、^
、$
の間の位置を示します。\B
は非単語境界をマッチします。- (?=p) は正の先読みアサーション(positive lookahead)で、パターン p の前の位置をマッチします。
- (?!p) は負の先読みアサーション(negative lookahead)で、非 p の前の位置をマッチします。
実例応用#
数字の千位区切り#
千位区切りの挿入位置は、3 桁ごとの数字の前であり、先頭位置ではありません。
const result = '123456789'
const regex = /(?!^)(?=(\d{3})+$)/g
console.log(result.replace(regex, ','))
// 123,456,789
パスワードの検証#
パスワードの長さは 6-12 桁で、数字と大文字小文字の文字で構成されますが、必ず 2 種類以上の文字を含む必要があります。まず、6-12 桁の数字と大文字小文字の文字をマッチさせることを考えます。
const regex = /^[0-9A-Za-z]{6-12}$/g
次に、少なくとも 2 種類の文字を含むかどうかを判断する必要があります。2 つの解法があります。
第一の解法:まず数字が含まれているかどうかを判断します。正規表現は次のように表現できます:
const regex = /(?=.*[0-9])^[0-9A-Za-z]{6-12}$/
重要なのは (?=.*[0-9])^
を理解することです。この正規表現は、任意の数の文字の後に数字があることを示します。大文字と小文字の文字を同時に含む必要がある場合は、次のように表現できます:
const regex = /(?=.*[0-9])(?=.*[A-Z])^[0-9A-Za-z]{6-12}$/
最終的な正規表現は次のように表現できます:
const regex = /((?=.*[0-9])(?=.*[A-Z])|(?=.*[0-9])(?=.*[a-z])|(?=.*[A-Z])(?=.*[a-z]))^[0-9A-Za-z]{6-12}$/
const str1 = '123456'
const str2 = '123456a'
const str3 = 'abcdefgA'
console.log(str1, regex.test(str1)) // false
console.log(str2, regex.test(str2)) // true
console.log(str3, regex.test(str3)) // true
第二の解法:「少なくとも 2 種類の文字を含む」ということは、全てが数字、大文字、小文字のいずれかであってはならず、全てが数字であってはならないことを示します。全てが数字であってはならないことは次のように表現できます:
const regex = /(?!^[0-9]{6-12}$)^[0-9A-Za-z]{6-12}$/
したがって、最終的な正規表現は次のように表現できます:
const regex = /(?!^[0-9]{6,12}$)(?!^[A-Z]{6,12}$)(?!^[a-z]{6,12}$)^[0-9A-Za-z]{6,12}$/
括弧の役割#
グループ化と分岐構造#
括弧はグループ化を提供し、参照に使用されます。参照には 2 種類あります:JavaScript での参照と正規表現での参照です。グループ化と分岐構造は、括弧の最も直接的な機能であり、括弧内が全体であることを強調します。つまり、サブ式を提供します。
// グループ化の例、ab が全体であることを強調
const regex1 = /(ab)+/g
// 分岐の例、分岐構造が全体であることを強調
const regex = /this is (ab|cd)/g
グループ参照#
括弧でグループ化することで、データの抽出や置換操作を行うことができます。データ抽出の例として、yyyy-mm-dd 形式の日付を抽出する場合:
const regex = /(\d{4})-(\d{2})-(\d{2})/g
const date = '2018-01-01'
const regex = /(\d{4})-(\d{2})-(\d{2})/
const date = '2018-01-01'
console.log(regex.exec(date))
// console.log(date.match(regex))
// ["2018-01-01", "2018", "01", "01", index: 0, input: "2018-01-01", groups: undefined]
console.log(RegExp.$1, RegExp.$2, RegExp.$3)
// 2018 01 01
拡張:JavaScript では、exec
と match
メソッドの機能はほぼ同じですが、主に 2 つの違いがあります:
exec
は RegExp クラスのメソッドであり、match
は String クラスのメソッドです。exec
は最初の一致する文字列のみをマッチさせますが、match
は g 修飾子の設定に応じて動作が異なります。非グローバルマッチの場合、両者の動作は一致します。
さらに、括弧でのグループ化は、yyyy-mm-dd を dd-mm-yyyy に置換する操作を簡単に行うことができます:
const date = '2018-01-31'
const regex = /^(\d{4})-(\d{2})-(\d{2})$/
const result = date.replace(regex, '$3-$2-$1')
console.log(result) // 31-01-2018
// 等価
const result2 = data.replace(regex, function () {
return RegExp.$3 + '-' + RegExp.$2 + '-' + RegExp.$1
})
// 等価
const result3 = data.replace(regex, function (match, year, month, day) {
return day + '-' + month + '-' + year
})
逆参照#
JavaScript でグループを参照するだけでなく、正規表現内で参照することもできます。つまり逆参照です。例えば、日付をマッチさせる場合:
const date1 = '2018-01-31'
const date2 = '2018-01.31'
const regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/
console.log(regex.test(date1)) // true
console.log(regex.test(date2)) // false
括弧がネストされている場合、最初に出現した左括弧の順序がグループの順序となります。逆参照には 3 つのヒントがあります:
- ヒント 1:10 のような表現がある場合、これは 10 番目のグループを示し、1 と 0 を示すのではありません。後者を示す必要がある場合は、非キャプチャ括弧を使用して (?:1) 0 または 1 (?:0) のように表現します。
- ヒント 2:存在しないグループを参照する場合、逆参照の文字そのものをマッチします。例えば、2 は 2 をマッチし、バックスラッシュはエスケープを示します。
- ヒント 3:グループの後に量詞がある場合、最後にキャプチャされたデータがグループとして扱われます。
非キャプチャ括弧#
前述の例では、括弧内のグループまたはキャプチャデータは、後で参照するために使用されるキャプチャグループおよびキャプチャ分岐と呼ばれます。括弧の元の機能のみを使用したい場合は、非キャプチャ括弧 (?:p)
および (?:p1|p2|p3)
を使用できます。
バックトラッキングの原理#
バックトラッキング法は試行錯誤法とも呼ばれ、その基本的な考え方は次のとおりです:問題のある状態(初期状態)から出発し、その状態から到達可能なすべての「状態」を検索します。ある道が「終わり」に達したとき(前進できない場合)、1 歩または数歩後退し、別の可能な「状態」から出発して検索を続けます。すべての「パス」(状態)を試すまでこのプロセスを繰り返します。このようにして「前進」し、「バックトラッキング」を繰り返して解を探す方法を「バックトラッキング法」と呼びます。
「バックトラッキング法」は本質的に深さ優先アルゴリズムです。例えば、正規表現 /ab{1,3}/c
を使用して文字列 'abbc' をマッチさせる場合、そのマッチングプロセスは次のようになります:
正規表現のバックトラッキング
図の 5 番目のステップは赤色で示されており、マッチングが成功しなかったことを示しています。この時、b {1,3} はすでに 2 文字 "b" にマッチしており、3 番目を試みる準備をしていると、次の文字が "c" であることがわかります。したがって、b {1,3} はすでにマッチしたと見なされます。その後、状態は以前の状態に戻り、最後にサブ式 c を使用して文字 "c" をマッチさせます。この時、全体の式が成功したことになります。図の 6 番目のステップは「バックトラッキング」と呼ばれます。
これは貪欲マッチの場合のバックトラッキングです。惰性マッチにもバックトラッキングが存在します。もう一つの惰性マッチの例を示します:
const string = '12345'
const regex = /^(\d{1,3}?)(\d{1,3})$/
console.log(string.match(regex))
// => ["1234", "12", "2345", index: 0, input: "12345"]
惰性マッチであっても、全体のマッチが成功するために、最初のグループは 1 文字を多く割り当てられます。その全体のマッチングプロセスは次のようになります:
惰性正規表現のバックトラッキング
さらに、分岐構造も一種のバックトラッキングと見なすことができ、現在の分岐がマッチ条件を満たさない場合、別の分岐に切り替えます。
バックトラッキングのいくつかの状況を比喩的に類比すると:
- 貪欲量詞の「試す」戦略は:服を買うときに値切ることです。値段が高すぎるので、少し安くしてもらえますか?ダメなら、さらに安くしてもらえます。
- 惰性量詞の「試す」戦略は:物を売るときに値上げすることです。少なすぎるので、もう少し多くあげます。まだ少ないので、もう少しあげます。
- 分岐構造の「試す」戦略は:3 軒の店を比べることです。この店はダメなので、別の店に行きます。まだダメなら、さらに別の店に行きます。
正規表現の構造#
構造と演算子#
JavaScript の正規表現は、文字リテラル、文字クラス、量詞、アンカー、グループ、選択分岐、逆参照 などの構造で構成されています。
構造 | 説明 |
---|---|
文字リテラル | 特定の文字をマッチさせる、エスケープと非エスケープを含む |
文字クラス | 複数の可能な文字をマッチさせる |
量詞 | 連続して出現する文字をマッチさせる |
アンカー | 位置をマッチさせる |
グループ | 括弧全体をマッチさせる |
選択分岐 | 複数のサブ式のいずれかをマッチさせる |
関連する演算子には次のものがあります:
演算子の説明 | 演算子 | 優先度 | |
---|---|---|---|
エスケープ文字 | \ | 1 | |
括弧と角括弧 | \(...\)、\(?:...\)、\(?=...\)、\(?!...\)、\[...\] | 2 | |
量詞修飾子 | {m}、{m,n}、{m,}、?、\*、+ | 3 | |
位置とシーケンス | ^、\$、\ メタ文字、一般文字 | 4 | |
パイプ文字 | \` | \` | 5 |
メタ文字#
JavaScript の正規表現で使用されるメタ文字には ^、$、.、*、+、?、|、\、/、(、)、[、]、{、}、=、!、:、-
があり、上記の文字そのものをマッチさせる場合はすべてエスケープできます。
正規表現の構築#
正規表現を構築する際のバランスの法則:
- 期待される文字列をマッチさせる
- 期待されない文字列をマッチさせない
- 可読性と保守性
- 効率
ここでは、マッチング効率を改善するためのいくつかの方法について説明します:
- ワイルドカードの代わりに具体的な文字クラスを使用してバックトラッキングを排除します。
- 非キャプチャグループを使用します。キャプチャグループは、キャプチャグループと分岐内のデータを保存するためにメモリを占有する必要があります。
- 確定した文字を独立させます。例えば、
a+
をaa*
に変更することができます。後者は前者よりも文字 a を多く確定させています。 - 分岐の共通部分を抽出します。例えば、
this|that
をth(:?is|at)
に変更します。 - 分岐の数を減らします。例えば、
red|read
をrea?d
に変更します。
正規表現プログラミング#
JavaScript では、正規表現に関する一般的な API が 6 つあり、そのうち文字列インスタンスが 4 つ、正規表現インスタンスが 2 つあります:
String#search
String#split
String#match
String#replace
RegExp#test
RegExp#exec
文字列インスタンスの match
と search
メソッドは、文字列を正規表現に変換します:
const str = '2018.01.31'
console.log(str.search('.'))
// 0
//次のいずれかの形式に変更する必要があります
console.log(str.search('\\.'))
console.log(str.search(/\./))
// 4
console.log(str.match('.'))
// ["2", index: 0, input: "2018.01.31"]
//次のいずれかの形式に変更する必要があります
console.log(str.match('\\.'))
console.log(str.match(/\./))
// [".", index: 4, input: "2018.01.31"]
文字列の 4 つのメソッドは、毎回マッチする際に 0 から始まります。つまり、lastIndex プロパティは常に変わりません。一方、正規表現インスタンスの 2 つのメソッド exec と test は、正規表現がグローバルマッチの場合、毎回マッチが完了するたびに lastIndex を変更します。