'abc'
で始まらない文字列に一致させる'.mjs'
を含まない部分文字列に一致させるこの章では、例を使用して正規表現の先読み・後読みアサーションについて説明します。先読み・後読みアサーションはキャプチャを行わず、入力文字列の現在位置の前後にある(または無い)ものに一致する(または一致しない)必要があります。
パターン | 名称 | |
---|---|---|
(?=«pattern») |
肯定先読み | ES3 |
(?!«pattern») |
否定先読み | ES3 |
(?<=«pattern») |
肯定後読み | ES2018 |
(?<!«pattern») |
否定後読み | ES2018 |
4つの先読み・後読みアサーションがあります(表 4)
(?=«pattern»)
は、pattern
が入力文字列の現在位置の後にあるものと一致する場合に一致します。(?!«pattern»)
は、pattern
が入力文字列の現在位置の後にあるものと一致しない場合に一致します。(?<=«pattern»)
は、pattern
が入力文字列の現在位置の前にあるものと一致する場合に一致します。(?<!«pattern»)
は、pattern
が入力文字列の現在位置の前にあるものと一致しない場合に一致します。例では、先読み・後読みアサーションで何が実現できるかを示しています。ただし、正規表現は常に最良の解決策とは限りません。適切な構文解析などの別の手法の方が適している場合があります。
後読みアサーションは比較的新しい機能であり、ターゲットとするすべての JavaScript エンジンでサポートされているとは限りません。
先読み・後読みアサーションは、特にパターンが長い文字列と一致する場合、パフォーマンスに悪影響を与える可能性があります。
以下のインタラクションでは、引用符で囲まれた単語を抽出します
ここでは、2つの先読み・後読みアサーションが役立ちます
(?<=")
「引用符が先行する必要がある」(?=")
「引用符が後続する必要がある」先読み・後読みアサーションは、一致全体(キャプチャグループ 0)を返す /g
モードの .match()
に特に便利です。先読み・後読みアサーションのパターンが一致するものはキャプチャされません。先読み・後読みアサーションがないと、引用符が結果に表示されます
前のセクションで行ったことの逆を行い、文字列からすべての引用符で囲まれていない単語を抽出するにはどうすればよいでしょうか?
'how "are" "you" doing'
['how', 'doing']
最初の試みは、単に肯定先読み・後読みアサーションを否定先読み・後読みアサーションに変換することです。残念ながら、これは失敗します
問題は、引用符で囲まれていない文字シーケンスを抽出することです。つまり、文字列 '"are"'
では、途中の「r」は引用符で囲まれていないと見なされます。これは、「a」が先行し、「e」が後続するためです。
接頭辞と接尾辞が引用符でも文字でもないことを指定することで、これを修正できます
別の解決策は、\b
を使用して、文字シーケンス [a-z]+
が単語境界で開始および終了することを要求することです
否定後読みと否定先読みの良いところは、例で示されているように、文字列の先頭または末尾でもそれぞれ機能することです。
否定先読み・後読みアサーションは強力なツールであり、通常、他の正規表現手段でエミュレートすることはできません。
それらを使用しない場合は、通常、完全に異なるアプローチをとる必要があります。たとえば、この場合、文字列を(引用符で囲まれた単語と引用符で囲まれていない単語に)分割してから、それらをフィルタリングできます
const str = 'how "are" "you" doing';
const allWords = str.match(/"?[a-z]+"?/g);
const unquotedWords = allWords.filter(
w => !w.startsWith('"') || !w.endsWith('"'));
assert.deepEqual(unquotedWords, ['how', 'doing']);
このアプローチの利点
これまでに見てきたすべての例に共通しているのは、先読み・後読みアサーションが、一致の前後に何が来るべきかを指示する一方で、それらの文字を一致に含めないことです。
この章の残りの部分で示されている正規表現は異なります。それらの先読み・後読みアサーションは内側を向き、一致の内容を制限します。
'abc'
で始まらない文字列に一致させる'abc'
で始まらないすべての文字列に一致させたいとしましょう。最初の試みは、正規表現 /^(?!abc)/
である可能性があります。
これは .test()
ではうまく機能します
ただし、.exec()
は空の文字列を返します
問題は、先読み・後読みアサーションなどのアサーションは、一致したテキストを拡張しないことです。つまり、入力文字をキャプチャせず、入力の現在位置に関する要求を行うだけです。
したがって、解決策は、入力文字をキャプチャするパターンを追加することです
意図通り、この新しい正規表現は 'abc'
で始まる文字列を拒否します
そして、完全な接頭辞を持たない文字列を受け入れます
'.mjs'
を含まない部分文字列に一致させる次の例では、以下を見つけたいと思います
import ··· from '«module-specifier»';
ここで、module-specifier
は '.mjs'
で終わりません。
const code = `
import {transform} from './util';
import {Person} from './person.mjs';
import {zip} from 'lodash';
`.trim();
assert.deepEqual(
code.match(/^import .*? from '[^']+(?<!\.mjs)';$/umg),
[
"import {transform} from './util';",
"import {zip} from 'lodash';",
]);
ここで、後読みアサーション (?<!\.mjs)
は *ガード* として機能し、正規表現がこの場所で '.mjs
' を含む文字列と一致するのを防ぎます。
シナリオ:コメントをスキップしながら、設定行を解析したいとします。例えば
const RE_SETTING = /^(?!#)([^:]*):(.*)$/
const lines = [
'indent: 2', // setting
'# Trim trailing whitespace:', // comment
'whitespace: trim', // setting
];
for (const line of lines) {
const match = RE_SETTING.exec(line);
if (match) {
const key = JSON.stringify(match[1]);
const value = JSON.stringify(match[2]);
console.log(`KEY: ${key} VALUE: ${value}`);
}
}
// Output:
// 'KEY: "indent" VALUE: " 2"'
// 'KEY: "whitespace" VALUE: " trim"'
正規表現 RE_SETTING
はどのようにして導き出されたのでしょうか?
設定の次の正規表現から始めました
直感的には、これは次の部分のシーケンスです
この正規表現は、*いくつか* のコメントを拒否します
しかし、他のコメント(コロンが含まれているもの)は受け入れます
ガードとして (?!#)
を前に付けることで、これを修正できます。直感的には、「入力文字列の現在位置の後に文字 #
があってはならない」という意味です。
新しい正規表現は意図通りに機能します
直線の二重引用符のペアを中かっこ引用符に変換したいとしましょう
`"yes" and "no"`
`“yes” and “no”`
これは私たちの最初の試みです
最初と最後の引用符のみが中かっこになっています。ここでの問題は、*
量指定子が *貪欲に* (できるだけ多く)一致することです。
*
の後に疑問符を付けると、*非貪欲に* 一致します
バックスラッシュによる引用符のエスケープを許可したい場合はどうでしょうか?引用符の前にガード (?<!\\)
を使用することで、これを行うことができます
> const regExp = /(?<!\\)"(.*?)(?<!\\)"/g;
> String.raw`\"straight\" and "curly"`.replace(regExp, '“$1”')
'\\"straight\\" and “curly”'
後処理ステップとして、次の処理を行う必要があります
ただし、バックスラッシュでエスケープされたバックスラッシュがある場合、この正規表現は失敗する可能性があります
2番目のバックスラッシュにより、引用符が中かっこになりませんでした。
ガードをより洗練されたものにすることで、これを修正できます(?:
はグループを非キャプチャにします)
新しいガードは、引用符の前にバックスラッシュのペアを許可します
> const regExp = /(?<=[^\\](?:\\\\)*)"(.*?)(?<=[^\\](?:\\\\)*)"/g;
> String.raw`Backslash: "\\"`.replace(regExp, '“$1”')
'Backslash: “\\\\”'
1つの問題が残っています。このガードは、最初の引用符が文字列の先頭にある場合に一致するのを防ぎます
> const regExp = /(?<=[^\\](?:\\\\)*)"(.*?)(?<=[^\\](?:\\\\)*)"/g;
> `"abc"`.replace(regExp, '“$1”')
'"abc"'
最初のガードを (?<=[^\\](?:\\\\)*|^)
に変更することで、これを修正できます
> const regExp = /(?<=[^\\](?:\\\\)*|^)"(.*?)(?<=[^\\](?:\\\\)*)"/g;
> `"abc"`.replace(regExp, '“$1”')
'“abc”'
@jonasraoni
によって提案されました。RegExp
)」の章