Regular expressions

Patterns and flags

<https://javascript.info/regexp-introduction> のノート。

JavaScript では正規表現を RegExp オブジェクトを介して利用できるし、文字列のメソッドに統合されてもいる。

Regular Expressions

コンストラクターから正規表現オブジェクトを定義する方法:

let regexp = new RegExp("pattern", "flags");

Perl のようなリテラル正規表現による定義方法もある:

let regexp = /pattern/gmi;

どちらの場合も RegExp オブジェクトが生成する。

Flags

JavaScript の正規表現フラグは次の六種だ:

Flag

Specification

i

大文字小文字を区別しない

g

マッチすべてを対応する

m

複数行モード

s

. を改行文字にもマッチさせる

u

Unicode 完全サポート

y

厳密な位置での検索

Searching: str.match

呼び出し str.match(regexp) は、文字列 str の中で regexp にマッチするものを返す。

フラグ g を指定すると、戻り値はマッチ文字列からなる配列だ。

フラグ g が指定されていない場合、戻り値は長さゼロの配列であり、さらに配列は次のプロパティーを含む。

  • groups: おそらく括弧によるキャプチャー情報

  • index: str のどの位置からマッチしているか

  • input: str に等しい文字列

マッチするものがない場合、この呼び出しの結果は null となる。スクリプトは、配列が返る場合と null が返る場合のどちらにも対応する必要がある。

Replacing: str.replace

呼び出し str.replace(regexp, replacement) は、文字列 str の中で regexp にマッチするものを replacement で置換する。

フラグ g を指定すると、マッチ部分すべてを置換する一方で、指定しないと、せいぜい最初のマッチしか置換しない。

文字列 replacement には特別な意味を持つ文字列を含めることもある:

Pattern

Specification

$&

マッチ全体に等しい文字列

$

マッチ以前に等しい文字列

$'

マッチ以後に等しい文字列

$n

キャプチャー参照

$<name>

キャプチャー参照

$$

文字 $

Testing: regexp.test

呼び出し regexp.test(str) は、マッチがあるかどうかを Boolean 値で返す。

/LOVE/i.test("I love JavaScript"); // true

Character classes

<https://javascript.info/regexp-character-classes> のノート。

文字クラスとは、特殊な表記法であって、特定の集合から任意の記号にマッチするものだ。

よく使われる三種をまず紹介している:

Class

Specification

\d

文字 0, 1, …, 9

\s

ソフトスペース、タブ、改良文字等の空白文字各種

\w

ラテンアルファベットおよび \d およびアンダーバー

正規表現には、普通の文字と文字クラスの両方が含まれることがある。

"Is there CSS4?".match(/CSS\d/); // 'CSS4'
"I love HTML5!".match(/\s\w\w\w\w\d/); // ' HTML5'

Inverse classes

各文字クラスには、同じ文字で大文字に表記される「裏クラス」が存在する。裏クラスは、対応する表クラスの補集合だと考えられる。

const str = "+7(903)-123-45-67";
str.match(/\d/g).join(''); // 79031234567
str.replace(/\D/g, "");

A dot is “any character”

ドット . は、「改行以外の任意の文字」にマッチする特殊な文字クラスだ。

Dot as literally any character with s flag

ドット . が、改行も含めて文字通り「あらゆる文字」を意味するようにしたい場面はたくさんある。これはフラグ s が行う。正規表現がこのフラグを持っている場合、ドット . は文字通り任意の文字にマッチする。

"A\nB".match(/A.B/s); // A\nB

囲み記事が面白い。フラグ s が対応されていない JavaScript エンジン環境では [\s\S][^] でしのげとある。

Unicode: flag u and class \p{...}

<https://javascript.info/regexp-unicode> のノート。

これは見慣れないトピックなのでしっかりチェックする。

昔の名残で、String.length など、4 バイト文字を正しく扱えない機能がいまだにある。

デフォルトでは、正規表現は 4 バイトの「長い文字」を 2 バイトの文字の対として扱う。そして、それは文字列で起こるような奇妙な結果につながるかもしれない。文字列とは異なり、正規表現にはこのような問題を解決するフラグ u がある。さらに、Unicode プロパティー検索も利用できるようになる。

Unicode properties \p{...}

Unicode の各文字には多くのプロパティーがある。その文字がどのような「カテゴリー」に属しているかを述べ、その文字に関する雑多な情報を含む。

例えば、文字に Letter プロパティーがあれば、その文字は(何か言語の文字という意味で)アルファベットに属していることを意味する。 Number プロパティーは、その文字が数字であることを意味する。

あるプロパティーを持つ文字を正規表現 \p{...} で検索することができる。これにもフラグ u が必要だ。

例えば、\p{Letter} は任意の言語の文字を表す。略記 \p{L} も通じる。次の検索は「何でもいいから言語の文字を全て探す」であり、三文字それぞれがマッチする。

"Aბㄱ".match(/\p{L}/gu); // A,ბ,ㄱ

本文でメインカテゴリーとサブカテゴリーが長い一覧を形成している。それでもまだ全てではなく、参考文献が列挙されている。

例えば <https://unicode.org/cldr/utility/character.jsp> のページを操作すると、入力した一文字のプロパティーをすべて確認できる。

Example: hexadecimal numbers

/x\p{Hex_Digit}\p{Hex_Digit}/u

Example: Chinese hieroglyphs

Unicode のプロパティーに Script がある。これは値を取ることができる。キリル文字、ギリシャ文字、アラビア文字、漢字など、さまざまな文字がある。例えば、キリル文字には \p{sc=Cyrillic}, 漢字には \p{sc=Han}, など。

`Hello Привет 你好 123_456`.match(/\p{sc=Han}/gu); // 你,好

Example: currency

通貨記号であることを示す Unicode プロパティーは \p{Currency_Symbol}, \p{Sc} が対応する。

/\p{Sc}\d/gu

Anchors: string start ^ and end $

メタキャラクター ^$ はアンカーの一種だ。それぞれ文字ではなく、テキストの先頭位置とテキストの末尾位置にそれぞれマッチする。

/^Mary/.test("Mary had a little lamb"); // true
/snow$/.test("its fleece was white as snow"); // true

Testing for a full match

両方を合わせた ^...$ は、文字列がパターンに完全にマッチするかどうかを調べるのによく使われる。ユーザー入力を検証する場合などに有用だ。

フラグ m がある場合、アンカーは異なる動作をする。

Tasks

Regexp ^$

正規表現 ^$ は空文字列にしかマッチしない。

Multiline mode of anchors ^$, flag m

<https://javascript.info/regexp-multiline-mode> のノート。

フラグ m で有効になる複数行モードだが、これは ^$ の動作にしか影響しない。複数行モードでは文字列の先頭と末尾だけでなく、行頭と行末でもマッチする。

Searching at line start ^

次の文字列に対する match(/^\d/gm)match(/^\d/g) の結果は異なる。前者は長さ 3 の配列を返すが、後者は長さ 1 の配列を返す。

1st place: Winnie
2nd place: Piglet
3rd place: Eeyore

Searching at line end $

次の文字列に対する match(/\d$/gm)match(/\d$/g) の結果は前項と同様の違いがある。

Winnie: 1
Piglet: 2
Eeyore: 3

Searching for \n instead of ^$

フラグ m なしで、改行文字 \n を直接指定してマッチさせようとするのとどう違うのかを見る。例えば前項のテキストに対して match(/\d\n/g) を考える。

  1. テキストの最後が改行文字で終わっていない場合、テキスト末端近傍のマッチが異なる。

  2. マッチ結果に改行文字が含まれるようになる。

Word boundary: \b

<https://javascript.info/regexp-boundary> のノート。

単語境界位置にマッチする \b を学ぶ。単語境界位置は次の三種類だ:

  1. 文字列の先頭の文字が \w にマッチする場合、その先頭。

  2. 文字列内の二文字の間で、一方が \w にマッチし、もう一方が \W にマッチする場合。

  3. 文字列の末尾の文字が \w にマッチする場合、その末尾。

"Hello, Java!".match(/\bJava\b/); // "Java"
"Hello, JavaScript!".match(/\bJava\b/); // null
"Hello, Java!".match(/\bHello\b/); // "Hello"
"Hello, Java!".match(/\bJava\b/);  // "Java"
"Hello, Java!".match(/\bHell\b/);  // null
"Hello, Java!".match(/\bJava!\b/); // null

\d\w の部分集合であるので、次もマッチする:

"1 23 456 78".match(/\b\d\d\b/g); // ["23", "78"]
"12,34,56".match(/\b\d\d\b/g); // ["12", "34", "56"]

\b の急所は \w と深い関係があるということだろう。

Tasks

Find the time

そうか。この問題には \b の指定が必要なのだ。

Escaping, special characters

<https://javascript.info/regexp-escaping> のノート。

バックスラッシュ \ は、例えば \d のように、文字クラスを表すのに使われることを見てきた。つまり、これは正規表現における特殊文字だと言える。

他にも [ ] { } ( ) \ ^ $ . | ? * + のように、正規表現で特別な意味を持つ文字がある。これらは、より強力な検索を行うために用いられる。

Escaping

特別な意味を持つ文字を、見てくれどおりの文字そのものをマッチさせたいとする。特殊文字を通常の文字として表現するには、その文字の直前にバックスラッシュ \ を付ける。このような行為を「文字をエスケープする」と言う。

"Chapter 5.1".match(/\d\.\d/); // "5.1"
"Chapter 511".match(/\d\.\d/); // null

"function g()".match(/g\(\)/); // "g()"

"1\\2".match(/\\/); // '\'

最後の例で、文字列のバックスラッシュも正規表現のバックスラッシュもどちらもエスケープが必要であることに注意。

A slash

スラッシュ / は特別な意味のある文字ではないが、リテラル正規表現を書くときにはエスケープが必要となる。RegExp コンストラクターで文字列から正規表現を生成するときにはこの限りでない。

new RegExp

RegExp コンストラクターで文字列から正規表現を生成する場合には別の注意を要する。リテラル文字列ではバックスラッシュが「食われる」ので、これをエスケープせねばならない。

let regexp = new RegExp("\\d\\.\\d");

"Chapter 5.1".match(regexp); // "5.1"

Sets and ranges [...]

<https://javascript.info/regexp-character-sets-and-ranges> のノート。

複数の文字または文字クラスを含む角括弧 [ ] 全体からなるパターンは、この中にあるどれかの一文字にマッチする文字にマッチする。

Sets

このようなパターンを集合と言う。通常の文字と混在して正規表現を形成することができる。

// find [t or m], and then "op"
"Mop top".match(/[tm]op/gi); // ["Mop", "top"]

// find "V", then [o or i], then "la"
"Voila".match(/V[oi]la/); // null

Ranges

角括弧は、文字範囲を含むこともできる。例えば [a-z]a から z までの範囲にある文字一文字に、[0-5]0 から 5 までの数字一文字にそれぞれマッチする。

"Exception 0xAF".match(/x[0-9A-F][0-9A-F]/g); // "xAF"
  • 小文字も探したい場合は、角括弧内に範囲 a-f を追加するか、正規表現にフラグ i を追加する。

  • [ ] の中に文字クラスを使用することもできる。

  • 複数のクラスを組み合わせることも可能だ。

文字クラスは文字範囲の略記法だと考えられる:

Class

Character

Set

\d

[0-9]

\w

[a-zA-Z0-9_]

\s

[\t\n\v\f\r ] に Unicode の珍しい空白文字を加えたもの

Example: multi-language \w

文字クラス \w だと漢字、キリル文字その他にマッチしない。マッチするようなものを自作する。以前やった Unicode プロパティーを角括弧内に列挙することで、それを達成する。

/[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]/gu;

文字範囲がわかっていれば、始点文字と終点文字とをマイナス文字で連結した集合で指定してもよい。

Excluding ranges

補集合を指定するには、同じ要素列を [^ ] で囲む。

/[^ ]/;

Escaping in [...]

角括弧内ではほとんどの文字をエスケープせずに置くことができる。

  • 文字 . + ( ) はエスケープを要しない。

  • マイナス文字 - は、角括弧の最初の要素でも最後の要素でもない限りはエスケープされない。

  • キャレット文字 ^ は、角括弧の最初に書きたい場合にしかエスケープされない。

  • 文字としての角括弧 ] はいつでもエスケープされる。

角括弧の中のドット . は、文字としてのドットを意味する。

エスケープを要しない、されない、というのは、してもしなくても動くということだ:

"1 + 2 - 3".match(/[-().^+]/g) // ["+", "-"]
"1 + 2 - 3".match(/[\-\(\)\.\^\+]/g); // ["+", "-"]

Ranges and flag u

角括弧内に surrogate pairs がある場合には、正規表現にフラグ u を指定すること。まともな文字が出力されなかったり、悪い場合にはエラーが生じる。

フラグ u を与えないと、角括弧内の surrogate pair それぞれは正規表現エンジンに二文字として認識される。コードポイント値二つに分割されるということだろう。その結果、surrogate pair で構成される文字二つで文字範囲を指定しようとすると、意図に反した不正な範囲を形成してしまう可能性がある。

Tasks

Java[^script]

もちろんフラグなしで考える。”Java” にはマッチしない。”JavaScript” にはマッチする。

Find the time as hh:mm or hh-mm

角括弧内にコロンとマイナスを置く方法が問われている。

Quantifiers +, *, ? and {n}

<https://javascript.info/regexp-quantifiers> のノート。

例えば、+7(903)-123-45-67 のような文字列があり、その中のすべての数字を探したい。今回は一桁の数字ではなく、完全な数字に興味がある。7, 903, 123, 45, 67 だ。数は一桁以上の数字が並んだものだ。何個必要かを示すには量指定子をつける。

Quantity {n}

量指定子は文字、文字クラス、[ ] 集合などに付加し、それがいくつ必要かを指定する。

最も単純な量指定子は中括弧で囲まれた数字 {n} だ。

  • \d{5} は厳密に五桁の数字を表す。\d\d\d\d\d と等しい。

  • \d{3,5} は三桁から五桁までの数を表す。

  • \d{3,} は三桁以上の数を表す。

冒頭の問題に戻ると、求める正規表現は \d{1,} であることがわかる。

Shorthands

頻繁に使われる量指定子には速記形が用意されている。

Symbol

Description

Example

+

{1,} と等しい

\d+

?

{0,1} と等しい

https?

*

{0,} と等しい

\d0*

More examples

  • 小数 \d+\.\d+

  • 属性なしの開始 HTML タグ

    • /<[a-z]+>/i: 簡易版。

    • /<[a-z][a-z0-9]*>/i: h1 などが欲しいときはきっちりと書く。

  • 属性なしの開始 HTML タグまたは終了タグ /<\/?[a-z][a-z0-9]*>/i

Tasks

How to find an ellipsis ...?

ドットはメタキャラクターなので、リテラル正規表現で指定する場合にはエスケープする。

Regexp for HTML colors

#ABCDEF のように書かれた HTML 色を検索する正規表現。ここでは最初に文字 # が来て、次に十六進数がちょうど六文字くるものだけを扱えばいい。

「ちょうど何文字」というのを表現するのに、単語境界指定などが要求される。この小問は教育効果が意外に高い?

Greedy and lazy quantifiers

<https://javascript.info/regexp-greedy-and-lazy> のノート。

次の例を考える。このテキストから二重引用符で囲まれている部分文字列をすべて得たい:

a "witch" and her "broom" is one

単純に /".+"/g とすると、狙い通りにマッチしない。

'a "witch" and her "broom" is one'.match(/".+"/g); // "witch" and her "broom"

Lazy mode

量指定子の不精モードは、貪欲モードの反対に「最小限の回数を繰り返す」というモードだ。このモードを有効にするには、元となる量指定子に ? を付ける(単独の ? とは異なる意味であることに注意)。

'a "witch" and her "broom" is one'.match(/".+?"/g); // ["witch", "broom"]

正規表現 ".+?" はどのように照合されるのか:

  1. 最初の文字 " についてはさっきと同じ処理となる。

  2. 次の文字 . についてもさっきと同じだ。

  3. 正規表現エンジンは次にある +? を見て、ドットの照合を一つきりで打ち切る。代わりに、残りパターンである " の照合処理をそこから開始する。" が見つかればそこで終了となるが、今回はそうではないので続行する。

  4. それから、正規表現エンジンはドットの繰り返し回数を増やし、もう一回テストする。文字 i, t, c, … と続ける。

  5. そうこうしていると "witch" が得られる。

  6. 次の検索は現在のマッチの終わりから始まり、さらに "broom" を得る。

不精モードは +? の他に *?, ?? も有効だ。上のアルゴリズムに準じる。

不精モードは必要のないことを繰り返さない。

現代的な正規表現エンジンは最適化がよく働くので、上記のアルゴリズムよりも効率良い処理をする可能性がある。

Alternative approach

同じことをする正規表現が複数あることはよくある。

'a "witch" and her "broom" is one'.match(/"[^"]+"/g); // ["witch", "broom"]

不精モードではダメで、この集合版が必要な場合もある。例えば、<a href="..." class="doc"> の形式で、何でもいいから href を持つリンクを見つけたいとする。まず最初に思いつく正規表現は /<a href=".*" class="doc">/g だ(貪欲モードが先に思いつく)。

しかし、この正規表現では同一行にこのような A タグが複数ある場合には狙いどおりにマッチしない。さっきの魔女のほうきと同じことが起こる。

そこで正規表現を不精にする:

/<a href=".*?" class="doc">/g

しかし、次のようなテキストに対してはまた狙いを外れる:

...<a href="link1" class="wrong">... <p style="" class="doc">...

今回は /<a href="[^"]*" class="doc">/g とするのが妥当だ。

Tasks

A match for /d+? d+?/

せっかくだから /\d+ \d+?/g, etc. なども試すといい。

Find HTML comments

他の言語のコメントにも応用できる、つぶしの効く正規表現を習得できる。

  • 複数行にまたがることが考えられる場合には正規表現フラグ s を指定する。

  • 用いる不精モードはここでは *? だ。

Find HTML tags

キャレットあり集合を使うパターンと不精モードは、正規表現一つの中では本質的には共存しない気がする。

Capturing groups

正規表現の意味を変えずに、パターンの一部を丸括弧で囲むことができる。これを捕捉グループと呼ぶ。これには効果が二つある:

  1. マッチした部分を別の項目として結果配列に取り込むことができる。

  2. ( ) の後に量指定子を置くと、それは括弧全体に適用される。

Examples

Example: gogogo

'Gogogo now!'.match(/(go)+/ig) ); // "Gogogo"

Example: domain

"site.com my.site.com".match(/(\w+\.)+\w+/g); // ["site.com", "my.site.com"]

Example: email

ドメインのパターンが構築できたので、メールアドレスのパターンも行ける。名前部分は -. も使用可能であるから、正規表現では [-.\w]+ あたりになる。ドメインも若干手直しする。

"my@mail.com @ his@site.com.uk".match(/[-.\w]+@([\w-]+\.)+[\w-]+/g); // ["my@mail.com", "his@site.com.uk"]

Parentheses contents in the match

パターン内の丸括弧は左から右へ番号が割り当てられている。正規表現エンジンはそれぞれにマッチした内容を記憶し、結果に示すことができる。

メソッド str.match(regexp)regexp にフラグ g がない場合、最初のマッチを探し、配列として返す。中身は次のような具合だ:

  • result[0]: 完全マッチ

  • result[1]: 最初の丸括弧の中身

  • result[2]: 二番目の丸括弧の中身

例えば、HTML タグ <.*?> を見つけて処理したい。タグの内容を別の変数に格納する。

let str = '<h1>Hello, world!</h1>';
let tag = str.match(/<(.*?)>/);

tag[0]; // "<h1>"
tag[1]; // "h1"

Nested groups

捕捉グループを入れ子にすることもできる。番号はやはり左から右へと割り当てられる。

let result = '<span class="my">'.match(/<(([a-z]+)\s*([^>]*))>/);
result[0]; // '<span class="my">'
result[1]; // 'span class="my"'
result[2]; // 'span'
result[3]; // 'class="my"'

Optional groups

グループがオプショナルであって、マッチに存在しない場合がある。( )? とか ( )* のようなものがある場合だ。それでも、対応する結果配列の項目は存在し、値は undefined に等しい。

let match = 'a'.match(/a(z)?(c)?/);

match.length; // 3
match[0]; // "a"
match[1]; // undefined
match[2]; // undefined

マッチするグループとしないグループがある例:

let match = 'ac'.match(/a(z)?(c)?/)

match.length; // 3
match[0]; // "ac"
match[1]; // undefined
match[2]; // "c"

Searching for all matches with groups: matchAll

フラグ g でマッチ全てを検索する場合、メソッド match はグループに対する内容を返さない。捕捉グループが無視されて、たんにマッチを全て含む配列が返る。

メソッド matchAll は捕捉グループに対応した全検索機能だ。

  1. 配列ではなく、反復可能なオブジェクトを返す。

  2. フラグ g がある場合、すべてのマッチをグループを含む配列として返す。

  3. マッチがない場合、空の反復可能なオブジェクトを返す。

マッチを forof ループで得たり、次のように変数に代入したりする。

let [tag1, tag2] = '<h1> <h2>'.matchAll(/<(.*?)>/gi);

tag1[0]; // "<h1>"
tag1[1]; // "h1"
tag1.index; // 0
tag1.input; // "<h1> <h2>"

Named groups

Python のように、捕捉グループに名前を付けることもできる。(?<name> ) の形式も同じだ。メソッド match の戻り値のプロパティー groups から、指定した name でマッチそれぞれを参照する。

let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;

let groups = "2019-04-30".match(dateRegexp).groups;
groups.year; // "2019"
groups.month; // "04"
groups.day; // "30"

メソッド matchAll の場合には、個々のマッチにプロパティー groups がある。

let results = "2019-10-30 2020-01-01".matchAll(dateRegexp);
for(let result of results) {
    let {year, month, day} = result.groups;
    // ...
}

Capturing groups in replacement

置換メソッド str.replace(regexp, replacement) では、置換文字列に捕捉グループの内容を使用することができる。

その参照には、ドルマークと番号を組み合わせて指定する。

"John Bull".replace(/(\w+) (\w+)/, '$2, $1'); // "Bull, John"

名前付きグループを使った場合には、ドルマークと名前を組み合わせて指定する。

let regexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;
let str = "2019-10-30, 2020-01-01";
str.replace(regexp, '$<day>.$<month>.$<year>'); // ["30.10.2019", "01.01.2020"]

Non-capturing groups with ?:

量指定子は使いたいが、照合結果としては要しないこともある。そういう場合には (?: ) を指定することで捕捉グループを除外する。

Tasks

Check MAC-address

量指定子を含むグループに対して量指定子を付けることができることに注意。

「文字列が~にマッチするか」という問いに対しては ^$ でメインの正規表現を挟むこと。

Find color in the format #abc or #abcdef

この解答例だと RGB 成分が個別に取れない。

最後に \b を付けるのを忘れないようにする。そうしないと 4 桁にも 5 桁にもマッチする。

Find all numbers

整数、浮動小数点、負の数を含む、すべての十進数を検索する正規表現。すべて 0 の場合にはその部分を省略しても許されるバージョンも考えられる。

Parse an expression

算術二項演算を表す文字列 expr を入力とし、第一オペランド、第二オペランド、演算子からなる配列を出力とする関数 parse(expr) を実装する。

  • オペランドに対する正規表現は直前の問いの結果を利用する。

  • 演算子の集合は [-+*/] のように、マイナスを先頭に持ってくるのがコツとなる。

  • 演算子の前後には空白文字がいくつあってもよいから \s* を入れる。

メソッド match の結果をそのまま返すことはたぶんできない。マッチを選り抜いて新しく配列を作って返す。

Backreferences in pattern: \N and \k<name>

<https://javascript.info/regexp-backreferences> のノート。

捕捉グループ ( ) の内容は、結果や置換文字列だけでなく、パターン自体にも利用することができる。

Backreference by number: \N

番号 1, 2, 3, … の捕捉グループの内容を \1, \2, \3, … で参照できる。

番号が振られていない (?: ) は参照されない。

`He said: "She's the one!".`.match(/(['"])(.*?)\1/g); // "She's the one!"

Backreference by name: \k<name>

名前付きグループ (?<name> ) を使った場合には \k<name> で参照できる。

`He said: "She's the one!".`.match(/(?<quote>['"])(.*?)\k<quote>/g); // "She's the one!"

Alternation (OR) |

<https://javascript.info/regexp-alternation> のノート。

正規表現では、縦線文字でパターン同士を連結すると、「それらのパターンのいずれかに」マッチする表現となる。例えば、プログラミング言語を探すとする。 HTML, PHP, Java, JavaScript にマッチするかを調べるには、例えば次のように書く:

"First HTML appeared, then CSS, then JavaScript".match(
    /html|php|css|java(script)?/gi); // ['HTML', 'CSS', 'JavaScript']

gr(a|e)ygr[ae]y は同じものにマッチする。そして gra|eygra``または ``ey にマッチする。括弧で括らない場合には、いちばん外側に括弧があるかのように解釈されるらしい。

Example: regexp for time

以前やった HH:MM のような時刻の正規表現を改良する。25:99 みたいなものを無視したい。

HH の部分は次のように正規表現を組み立てる:

  • 最初の桁が 0 または 1 の場合、次の桁はどれでもかまわないから [01]\d.

  • 最初の桁が 2 であれば、次の桁は 0, 1, 2, 3 のいずれかでなければならないから 2[0-3].

  • 最初の桁が他の文字になることは認めない。

以上を縦棒で連結したもの [01]\d|2[0-3] を HH 部分の正規表現とする。

MM 部分も似たように組み立てて [0-5]\d を得る。

これらを : で連結する。ただし見えない括弧問題を避けるために HH 部分に丸括弧を付ける。

"00:00 10:10 23:59 25:99 1:2".match(
    /([01]\d|2[0-3]):[0-5]\d/g)); // ["00:00", "10:10", "23:59"]

Tasks

Find programming languages

話を単純にして要点をまとめる。文字列から JavaJavaScript を検索したいとする。そこで Java|JavaScript のように指定してしまうと、文字列に JavaScript しかない場合には狙いどおりにいかない。Java が先に見つかってしまうからだ。

そこで、次のどちらかのパターンを指定する:

  • Java(Script)?

  • JavaScript|Java

Find bb-tag pairs

過去数章の内容を総合したような演習問題。

  • まず開始タグのパターンは \[(b|url|quote)] のようになる。

  • 終了タグは、他に捕捉グループがなければ \[/\1] となる。リテラル正規表現で指定するならば /\[\/\1]/

  • タグの中身はこの場合には .*? とする(同一タグの入れ子を除外することになる)。

文字列が複数行にまたがる可能性があるので、正規表現オプションに s を加えて . に改行文字も対応させる。

Find quoted strings

二重引用符に囲まれた部分をマッチさせたい。エスケープ対応をする必要がある。特に、"AAAAAA\" のように、エスケープされた二重引用符で終わる文字列にマッチしてはいけない。

  • 開始文字は当然ながら " とする。

  • 終了文字も当然 " とする。

  • 中間にくるものは空文字を含む何かが何文字来てもいい。エスケープされているか否かで場合分けする。

    • エスケープ文字、任意の文字 .

    • エスケープでない文字

    • 二重引用符でない文字

/"(\\.|[^"\\])*"/

Find the full tag

タグ <style...> を見つける正規表現。全体にマッチする必要がある。<style> のように属性がないこともあれば、<style type="..." id="..."> のように属性が複数あることもある。

  • 開始パターンは <style でいいとする。

  • 次の文字は以下のどちらかしか認めない。

    • 文字 > で終わる。

    • 空白文字、それに続いて任意で何かの文字が任意の個数、最後に文字 > で終わる。

/<style(>|\s.*?>)/

Lookahead and lookbehind

<https://javascript.info/regexp-lookahead-lookbehind> のノート。

あるパターンの後に続いたり、前にある別のパターンとのマッチしか要らない場合がある。そのための特別な構文 lookahead, lookbehind を習う。

Lookahead

正規表現 X(?=Y) は「パターン X が欲しいが、パターン Y が続く場合のみ欲しい」ときに使う。

"1 turkey costs 30€".match(/\d+(?=€)/); // ["30"]

正規表現エンジンは X を見つけ、その直後に Y があるかどうかをチェックする。マッチしない場合には、マッチする可能性のあるものを飛ばして検索を続ける。例えばパターン X, Y, Z を含む正規表現 X(?=Y)(?=Z) を考える。これは Y でも Z でもマッチするパターンが X に続いているようなものにマッチする。

let str = "1 turkey costs 30€";

str.match(/\d+(?=\s)(?=.*30)/); // ["1"]
str.match(/\d+(?=.*30)(?=\s)/); // ["1"]

いい例ではなさそうだ。

Negative lookahead

正規表現 X(?!Y) は「パターン X が欲しいが、パターン Y が続かない場合のみ欲しい」ときに使う。

今度は価格ではなく、七面鳥の数量が欲しいとする(ユーロが付かないほうの数字)。

"2 turkeys cost 60€".match(/\d+\b(?!€)/g); // ["2"]

Lookbehind

Lookbehind は lookahead に似ているが、チェックする向きが反対だ。パターンの前に指定パターンがある場合にしか、そのパターンにマッチさせないようにできる。

(?<=Y)X: その直前にパターン Y があるときに限りパターン X にマッチする。

(?<!Y)X: その直前にパターン Y がないときに限りパターン X にマッチする。

let str = "2 turkey costs $30";

// the dollar sign is escaped \$
str.match(/(?<=\$)\d+/); // ["30"]
str.match(/(?<!\$)\b\d+/g); // ["2"]

二つ目の match では \b を欠くと 300 がマッチする。

Capturing groups

一般的には lookaround の丸括弧内のパターンはマッチ結果の一部とはならない。そのようなパターンを参照したい場合には、別途丸括弧で包み込む。

/\d+(?=(€|kr))/
/(?<=(\$|£))\d+/

Tasks

Find non-negative integers

「マイナス文字で始まらない数字の塊」というパターンを組み立てるのではダメだ。「マイナス文字でも数字でもない文字の次に来る数字の塊」が正しい考え方だ。

Insert After Head

問題の概要はこうだ。HTML ファイル全体を読み込んで得た文字列があるとする。この BODY 開始タグの直後に文字列 <h1>Hello</h1> を挿し込みたい。ただし、 BODY 開始タグの属性などがどうなっているかはわからない。

メソッド str.replace(regex, hello) を使って挿し込むのだが、二パターン紹介されている。題意に沿っているのは後者の lookbehind 採用版。位置にしかマッチしないことを利用するので $& が要らない。

HTML のテキストを正規表現で処理するときにはフラグ s, i の検討をいつでもすること。

Catastrophic backtracking

<https://javascript.info/regexp-catastrophic-backtracking> のノート。

まずい正規表現を書くと JavaScript エンジンが固まる。

Example

正規表現 ^(\w+\s?)*$ を考える。これは、行頭から行末まで、0 個以上の単語を指定するものだ。

しかし、特定の文字列については処理時間が長くなる。JavaScript エンジンが 100% の CPU 消費で固まるほど長い時間だ。

Simplified example

正規表現を単純にして要点を理解する。

/^(\d+)*$/.test("012345678901234567890123456789z");

正規表現エンジンの動きはだいたい次のようなものだ:

  1. 先頭から \d+ 部分を貪欲に取り尽くす。012...789

  2. (\d+)* 部分を処理しようとするが、これ以上やることがない。

  3. 次に $ を処理するが、文字 z があるのでマッチ失敗とする。

  4. マッチ失敗なので、貪欲モードのバックトラックを発動する。 \d+ 部分を 012...89 にする。

  5. (\d+)* 部分の * に相当する部分を 9 とする。 \d+ が二つ (*) あるからマッチ成功という判断だ。

  6. 次に $ を処理するが、文字 z があるのでマッチ失敗とする。

  7. マッチ失敗なので、貪欲モードのバックトラックを発動する。 \d+ 部分を 012...8 にする。

  8. * 部分を 89 とみなすと \d+ が二つ (*) あるからマッチ成功。

こんな感じで数字部分 \(n\) 桁の分割 \(2^{n - 1}\) 通りをすべてチェックするから CPU が固まるのだ。

(0...123456789)z
(0...12345678)(9)z
(0...1234567)(89)z
(0...1234567)(8)(9)z
(0...123456)(789)z
(0...123456)(78)(9)z
...

Back to words and strings

冒頭の正規表現 ^(\w+\s?)*$ についても同様の理由で、入力次第で CPU が固まる。

ここでは不精モードを有効にしても役に立たない。組み合わせの順序が変わるだけにすぎない。

正規表現エンジンによっては、技巧的なテストや有限自動化によって、組み合わせをすべて調べないようにしたり、より速くしたりすることができるが、大半のエンジンはそうではない。いつも役に立つとは限らない。

How to fix?

解決方法は二つある。一つは可能な組み合わせの数を減らすことだ。

正規表現 ^(\w+\s?)*$^(\w+\s)*\w*$ と書き換える。空白を省かないようにする。これでいくつかの \d に続く空白文字のあとに、オプショナルで \d 任意個数が来るパターンとなる。

例えば文字列 input を旧 (\w+\s?)* 部分において input のように分割する場合が消えた。この新パターンは、組み合わせのほとんどを試す時間を節約する。

Preventing backtracking

別のやり方として、量指定子のバックトラックを禁止することが考えられる。問題の根本は、正規表現エンジンが人間の目からは明らかに間違っている組み合わせをたくさん試そうとすることだ。

^(\w+\s?)*$ では \w+ でのバックトラックを禁じたいかもしれない。つまり \w+ は可能な限り長い単語全体とマッチする必要がある。 \w+ の繰り返し回数を減らしたり、\w+w+ などのように分割したりする必要はない。

最近の正規表現エンジンでは、そのために所有量指定子 (possessive quantifiers) をサポートしている。正規の量指定子の後に + をつけると所有格になる。例えば \d+ に対応するものは \d++ と表される。

所有量指定子は、実は普通の量指定子量詞よりも単純だ。バックトラックをせずに、マッチングできる数だけマッチングする。バックトラックのない検索処理はより単純になる。

また、JavaScript ではサポートされていないが、括弧の中のバックトラックを無効にする方法も考えられる。

Lookahead to the rescue!

バックトラックが意味をなさないことがあるので、+ のような量指定子はバックトラックしないようにしたい。

パターン \w の、バックトラックをせずに繰り返しをできるだけ多く取るパターンは (?=(\w+))\1 だ。

  • ?= は現在位置から始まる最長ワード \w+ を前方に探す。

  • (?= ) の中身は正規表現エンジンに記憶されないので、\w+ を括弧で囲んで捕捉グループを指定する。

  • そして、それを \1 としてパターン中から参照できるようにする。

つまり、前方を見て、単語 \w+ があれば、それを \1 としてマッチする。その理由は、lookahead が全体として単語 \w+ を見つけ、それを \1 を使ってパターンに取り込むからだ。つまり、本質的に所有量指定子 + を実装している。単語全体 \w+ しか捕捉しないのであって、その一部ではない。

例えば、JavaScript という単語では、Java にマッチするだけでなく、 Script を省いて残りのパターンにマッチさせることもある。

"JavaScript".match(/\w+Script/); // "JavaScript"

この場合、まず \w+JavaScript という単語全体を捕捉する。その後 + が一文字ずつバックトラックして、パターンの残りの部分にマッチしようと試みる。このバックトラックは \w+Java にマッチした時点で成功する。

"JavaScript".match(/(?=(\w+))\1Script/); // null

この場合 (?=(\w+)) は lookahead して JavaScript という単語を見つける。これは \1 によって全体としてパターンに含まれているので、その後に Script を見つける方法が残らない。

その後の + に対するバックトラックを禁止する必要があるときには、 (?=(\w+))1 の中にもっと複雑な正規表現を \w の代わりに入れることができる。


最初の例を、バックトラックを防ぐために lookahead を使って書き直す。

/^((?=(\w+))\2\s?)*$/

名前グループでわずかに見やすくする。

/^((?=(?<word>\w+))\k<word>\s?)*$/

Sticky flag "y", searching at position

<https://javascript.info/regexp-sticky> のノート。

フラグ y は、文字列の指定された位置で検索を実行する。指定された位置の何かを読み取るときに用いる。

例えばコード let varName = "value" の変数名を得たい。

メソッド regexp.exec(str) を使うやり方がある。フラグ gy がない正規表現では、このメソッドは最初にマッチするものしか探さない。フラグ g があるときに限り、プロパティー regexp.lastIndex に格納された位置から、str の検索をする。そして、マッチした場合は、マッチ直後のインデックスを regexp.lastIndex に代入する。つまり、regexp.lastIndex は検索の出発点であり、regexp.exec(str) を呼ぶたびに新しい値にリセットされる。

したがって、regexp.exec(str) を連続して呼び出すと、次々とマッチが返される。メソッド str.matchAll がない場合には代わりになる。

次のようにすると、変数名を得ることだけができる:

let str = 'let varName = "value"';
let regexp = /\w+/g;
regexp.lastIndex = 4;
regexp.exec(str); // ["value"]

ここからフラグ y の説明になる。フラグ yregexp.execlastIndex の位置から厳密に検索するようにする。上の例は実は lastIndex = 3 でも同じ結果となった。

let str = 'let varName = "value"';
let regexp = /\w+/y;
regexp.lastIndex = 3;
regexp.exec(str); // null

regexp.lastIndex = 4;
regexp.exec(str); // ["varName"]

フラグ y を使用することで性能向上がある。長いテキストがあり、その中にマッチするものが全くないとする。フラグ g を使った検索では、テキストの最後まで行っても何も見つからない。正確な位置だけをチェックするフラグ y を使う検索よりも時間がかなりかかってしまう。

Methods of RegExp and String

<https://javascript.info/regexp-methods> のノート。

str.match(regexp)

メソッド str.match(regexp) はいわばモードが三つある。

  1. 正規表現がフラグ g を持たない場合、最初のマッチだけを、捕捉グループの配列として返す。また、この配列にはプロパティー indexinput がある。

  2. 正規表現がフラグ g を持つ場合、グループやその他の詳細を捕捉せず、すべてのマッチを文字列とした配列を返す。

  3. マッチするものがなければ、フラグ g があろうがなかろうが null を返す。空の配列ではなく null であることを忘れないようにする。

いつでも配列として結果を扱いたい場合には、次のように書くといい:

let result = str.match(regexp) ?? [];

str.matchAll(regexp)

メソッド str.matchAll(regexp)str.match の上位互換バージョンのようなものだ。これはすべてのグループとのすべてのマッチを検索するために主に用いられる。元となった str.match との三つの違い:

  1. 配列ではなく、マッチからなる反復可能なオブジェクトを返す。

  2. すべてのマッチは、捕捉グループからなる配列として返される。フラグ g なし str.match 形式だ。

  3. 結果がない場合は空の反復可能オブジェクトを返す。今度は null ではない。

str.split(regexp|substr, limit)

正規表現または部分文字列で区切り方を指定して文字列を分割する。

'12-34-56'.split('-'); // ['12', '34', '56']

'12, 34, 56'.split(/,\s*/); // ['12', '34', '56']

str.search(regexp)

メソッド str.search(regexp) は最初にマッチした位置を返す。何もない場合は -1 を返す。

str.replace(str|regexp, str|func)

メソッド str.replace は汎用文字列置換機能だ。

第一引数が文字列の場合、最初にマッチしたものしか置換されない。すべてのマッチを見つけるには、文字列、フラグ g を伴う正規表現を使用する必要がある。

第二引数が文字列の場合、特別な文字列を指定することで特別な置換をする。その表はすでに示した。

賢い置換を必要とする状況では、第二引数に関数を指定することができる。この関数はマッチするたびに呼び出され、返された値が置換として挿し込まれる。

その関数の引数リストは (match, p1, p2, ..., pn, offset, input, groups) のようなものだ。正規表現に括弧がない場合は (str, offset, input) となる。

  • match: 正規表現のマッチ

  • p1, …, pn:捕捉グループの内容

  • offset: マッチの位置

  • input: replace 呼び出しの this に相当する文字列

  • groups: 名前付きグループがあるオブジェクト

str.replaceAll(str|regexp, str|func)

メソッド str.replaceAll は、基本的には str.replace と同じだ。大きな違いが二つある。

  1. 第一引数が文字列の場合、その文字列のすべての出現箇所を置換する。

  2. 第一引数が正規表現の場合、フラグ g がないとエラーになる。フラグを付けると replace と同じように動作する。

'12-34-56'.replaceAll("-", ":"); // 12:34:56

regexp.exec(str)

これはさっきやったばかり。

以前、JavaScript にメソッド str.matchAll が追加されるまでは、ループ内で regexp.exec を呼び出して、グループを持つすべてのマッチを取得していたらしい。

let str = 'More about JavaScript at https://javascript.info';
let regexp = /javascript/ig;

let result;
while (result = regexp.exec(str)) {
    `Found ${result[0]} at position ${result.index}`;
}

regexp.test(str)

メソッド regexp.test(str) は一致するものを探し、それが存在するかどうかを返す。

正規表現がフラグ g を持つ場合は regexp.exec 同様にプロパティー regexp.lastIndex から検索し、このプロパティーを更新する。

同じグローバル正規表現を異なる入力に適用すると、間違った結果になることがある。 regexp.testregexp.lastIndex を進めるので、別の文字列での検索がゼロ以外の位置から始まることがあるからだ。

let regexp = /javascript/g;
// regexp.lastIndex == 0

regexp.test("javascript"); // true
// regexp.lastIndex == 10

regexp.test("javascript"); // false