正規表現のマッチングをカスタム化する――「フラグ」:ECMAScriptで学ぶ正規表現(6)
正規表現の基本と、ECMAScript(JavaScript)における利用方法を紹介する連載。今回は、ECMAScript 2015以降で大幅に強化された、正規表現のマッチングをカスタム化するフラグについて。
フラグとは?
フラグ(flag)とは、「旗」という意味です。旗には、掲げたり下げたりといった状態がありますが、転じてコンピュータの世界ではOn/Offの状態を表すものをフラグと称しています。正規表現におけるフラグは、マッチングにおいてある機能を働かせるか、働かせないかを表し、パターン修飾子とも呼ばれます。ECMAScriptでは表1に挙げるフラグがサポートされています(ESはECMAScriptの略)。
フラグ | 概要 | プロパティ |
---|---|---|
g | グローバル検索を有効にする | global |
i | 大文字と小文字を同一に扱う | ignoreCase |
m | 複数行マッチを有効にする | multiline |
y | Sticky検索を有効にする(ES2015以降) | sticky |
s | ドット(.)を改行文字にも一致させる(ES2018以降) | dotAll |
u | Unicodeエスケープを有効にする(ES2018以降) | unicode |
d | マッチした範囲を保持する(ES2022以降) | hasIndices |
表1 フラグの一覧 |
このうち、uフラグについては第4回で取り上げたので、今回は紹介を割愛します。uフラグは、UnicodeコードポイントエスケープとUnicodeプロパティエスケープという記法を使用する場合に必要なフラグでした。
フラグは、正規表現パターンの最後に記述します。複数のフラグを記述でき、基本的に小文字で記述します。順番は関係ありません。以下は、g、i、mという3つのフラグを記述した例です。
const regexp = /.../gim;
プロパティは、RegExpオブジェクトにおけるフラグの状態を表します。読み出し専用で、フラグの状態を変更することはできません。変更するコードを書いてもエラーにはなりませんが、フラグの状態は変更されません。以下の「flag_property.js」は、これを確かめる例です。
let regexp = /.+/g; console.log(regexp.global); // true regexp.global = false; console.log(regexp.global); // true(フラグの状態は変更されない)
グローバル検索を有効にする(g)
gフラグは、これまでの連載で何度も登場したように、頻繁に使われるフラグです。与えられた正規表現パターンに対して、一致する限り何度もマッチングを試みます。gフラグを指定しない場合には、最初のマッチで処理を終了します。これはどちらが優れているというわけではなく、目的に応じて使い分ける必要があります。
match()メソッドとgフラグ
gフラグの指定の有無で、正規表現を扱うメソッドの動きがどう変わるかを見てみましょう。以下の「global_match.js」は、gフラグの有無が異なるだけの正規表現パターン(「【○○】●●●●」にマッチする)を用いた例です。
const regexp1 = /【.+?】[^【]+/; // gフラグなし const regexp2 = /【.+?】[^【]+/g; // gフラグあり const str = '【著】山内直【監修】山田祥寛【翻訳】小野寺志乃'; console.log(str.match(regexp1)); (1) // (1) ['【著】山内直', index: 0, input: '【著】山内直【監修】山田祥寛【翻訳】小野寺志乃', groups: undefined] console.log(str.match(regexp2)); (2) // (3) ['【著】山内直', '【監修】山田祥寛', '【翻訳】小野寺志乃']
match()メソッドの戻り値はgフラグの有無によって変化します。gフラグなしの(1)では、最初に一致したオブジェクトを返します(このオブジェクトは、indexなどのプロパティを持っています)。これに対してgフラグありの(2)では、全ての一致した部分文字列の配列を返します。これはmatch()メソッドを使用する上で重要な違いなので、覚えておきましょう。
[NOTE]gフラグとreplace()/matchAll()/replaceAll()
ここではmatch()メソッドの例を紹介しましたが、同じStringオブジェクトのreplace()メソッドでも、gフラグを指定することで文字列全体の置換が可能です。matchAll()/replaceAll()メソッドは、それぞれmatch()メソッドとreplace()メソッドの文字列全体版ですが、これらはgフラグの指定が必須となっています。これらのメソッドについては第3回で紹介しています。
exec()メソッドとgフラグ
match()メソッドとは別に、exec()メソッドを使った例も紹介します。以下の「global_exec.js」は、「global_match.js」と同じくgフラグの有無が異なるだけの正規表現パターンを用いた例です。
const regexp1 = /【.+?】[^【]+/; // gフラグなし const regexp2 = /【.+?】[^【]+/g; // gフラグあり const str = '【著】山内直【監修】山田祥寛【翻訳】小野寺志乃'; console.log(regexp1.exec(str)); (1) let obj = null; while((obj = regexp2.exec(str)) != null) { (2) console.log(obj); }
こちらは、実行結果です。
['【著】山内直', index: 0, input: '【著】山内直【監修】山田祥寛【翻訳】小野寺志乃', groups: undefined] ['【著】山内直', index: 0, input: '【著】山内直【監修】山田祥寛【翻訳】小野寺志乃', groups: undefined] ['【監修】山田祥寛', index: 6, input: '【著】山内直【監修】山田祥寛【翻訳】小野寺志乃', groups: undefined] ['【翻訳】小野寺志乃', index: 14, input: '【著】山内直【監修】山田祥寛【翻訳】小野寺志乃', groups: undefined]
(1)の、gフラグなしのときの振る舞いはmatch()メソッドと同じです。最初に一致したものが単一のオブジェクトとして返されます。(2)では、exec()メソッドの戻り値をループを使って処理しています。exec()メソッドでは、match()メソッドのように一致した部分文字列の配列を取得するのではなく、戻り値がnullになるまで何度も実行できます。このときの戻り値は、gフラグの有無にかかわらずオブジェクトです。
[NOTE]lastIndexプロパティ
gフラグを指定したとき、exec()メソッドは最後のマッチング位置をlastIndexプロパティ(初期値は0)に記録し、次の呼び出しではその位置から検索を再開します。これによって、exec()メソッドの呼び出しの都度、新しい部分文字列を含むオブジェクトが返されるというわけです。マッチングする部分がなくなってexec()メソッドがnullを返すと、lastIndexプロパティは0に戻ります。
大文字と小文字を同一に扱う(i)
iフラグを指定すると、正規表現パターンの中の半角英文字について、大文字と小文字を区別しなくなります。半角の英文字、いわゆるASCII文字のみが対象です。iフラグを使うのは、大文字と小文字が混在する文字列や、英文字の大文字/小文字の違いに意味がない文字列などを検索したい場合です。
以下の「ignorecase.js」は、HTMLのタグが大文字と小文字を区別しないという前提で、マッチングを試みる例です。
const regexp = /<\/?[a-z]+>/ig; const str = '<strong>タイトル</STRONG>'; console.log(str.match(regexp)); // (2) ['<strong>', '</STRONG>']
この例のように、iフラグは英文字を中心とするパターンを使用して検索する場合に多用されます。「[a-zA-Z]+」でも同様の意味になりますが、iフラグを指定することでパターンを簡略化できます。
複数行マッチを有効にする(m)
mフラグを指定すると、複数行マッチが有効になります。複数行マッチでは、境界アサーション(^、$)が、それぞれ行頭と行末にマッチします。複数行マッチが無効な場合、これらはそれぞれ文字列先頭と末尾だけにマッチします。
複数行マッチが指定されているかどうかにかかわらず、改行文字を含んだパターンによるマッチングは可能です。以下の「multi_line.js」は、@で始まるユーザーIDを想定し、それが改行を挟んで並んだときの振る舞いを確かめる例です。
const regexp1 = /^@[\w]+$/g; // mフラグなし const regexp2 = /^@[\w]+$/mg; // mフラグあり const str = '@user_id\n@invalid'; console.log(str.match(regexp1)); // null (1) console.log(str.match(regexp2)); // (2) ['@user_id', '@invalid'] (2)
(1)では、「^、$」が文字列の先頭と末尾なので、文字列全体が「@[\w]+」にマッチするかどうかを検索します。しかし、与えられた文字列は「@user_id\n@invalid」と\w以外の文字を含むので、マッチしません。
(2)では、mフラグが指定されているので複数行マッチとなり、「@user_id」と「@invalid」の、それぞれの行に対してマッチングを試みます。結果も、両者に問題なくマッチしたことを示しています。
このように、ECMAScriptではデフォルトは複数行マッチが無効なので、改行文字を含んだユーザーIDを与えられても「マッチしない」として、はじくことができます。複数行マッチを有効にすると、改行文字を挟んで複数のユーザーIDが与えられても、それはマッチしてしまいます。意図した動作ならよいですが、マッチしない、マッチしたということだけを判定していると、不正なデータが紛れ込む原因ともなりかねません。複数行マッチを使うときには、このようなリスクがあることを知っておきましょう。
[NOTE]メタ文字\A、\z
ECMAScriptでは、mフラグにより複数行マッチの指定を切り替えるようになっていますが、プログラミング言語によってはこのような切り替えが存在しないものもあります(Rubyなど)。このようなプログラミング言語では、文字列の先頭/末尾と、行の先頭/末尾というものが明確に区別されています。後者では今回も使ってきた境界アサーション(^、$)が同じく使用できますが、これに加えて前者のための境界アサーション\Aと\zが使用できます。つまり、文字列の先頭(\A)と末尾(\z)、行の先頭(^)と末尾($)というようにそれぞれが異なる境界を意味するので、用途に応じて使い分けることができます。
なお、ECMAScriptではデフォルトが複数行マッチでなく、必要に応じて切り替えられるためか、メタ文字\Aと\zは使用できません。
Sticky検索を有効にする(y)
yフラグは、ECMAScript 2015からサポートされました。yフラグを指定すると、Sticky検索が有効になります。stickyとは「くっつく」という意味で、yフラグを指定した正規表現では、lastIndexプロパティが示す位置から常に検索を開始します。
どのようなマッチングになるのか見てみましょう。以下の「sticky.js」は、同じ正規表現パターン、同じ検索対象文字列でyフラグの指定の有無だけを変えてみた例です。なお、例ではregexp2にgフラグも指定していますが、これはyフラグを指定したことで、実際には無視されます。
const regexp1 = /\d{2}:?/g; // gフラグのみ指定 const regexp2 = /\d{2}:?/gy; // yフラグも指定 const str = '16:14::34'; // 正しくない時刻文字列(コロンが2個続いている) let obj = null; while((obj = regexp1.exec(str)) != null) { (1) console.log(`global:${obj}`); } while((obj = regexp2.exec(str)) != null) { (2) console.log(`sticky:${obj}`); }
実行結果は、以下のようになります。
global:16: global:14: global:34 sticky:16: sticky:14:
(1)(2)とも、時刻文字列(分と秒の間が「::」となっています)を時、分、秒に分けて取り出すパターンを指定して繰り返し実行しています。この場合、yフラグを指定しない(1)では全て出力して繰り返しを終了しました。一方、yフラグを指定した(2)では、2回目の一致を出力したら、次はnullになって繰り返しを終了してしまいます。これはyフラグにより、常にlastIndexプロパティの位置でマッチングを試みますが、そこには境界アサーションのハット(^)が指定されていると暗黙に見なすからです。つまり、gフラグではlastIndexプロパティの位置から先で一致すればよいとするのに対し、yフラグではその位置で一致しなければならないのです。これが、(2)で繰り返しが2回で終了した理由です。
yフラグの使いどころはなかなか難しいのですが、例えばパーサーのように指定したパターンが正しい位置に存在しているかどうかを調べたいときに利用できます。gフラグでは、検索開始位置から前方にマッチするところがあればよいので、例のように末尾までマッチングを試みます。これに対してyフラグでは、検索開始位置でマッチしないと、例のようにそこでマッチング失敗となります。余計な文字が入っていたら、そこでマッチングをやめてしまうので、正しく高速に文字列を走査できます。
ドット(.)を改行文字にも一致させる(s)
sフラグは、ECMAScript 2018からサポートされました。任意の1文字を表すメタ文字であるドット(.)の挙動を変化させます。デフォルト(sフラグなし)では「.」は改行文字にマッチしませんが、sフラグを付与することで改行文字にもマッチするようになります。
簡単な例で確かめてみましょう。以下の「dotall.js」は、sフラグの有無で「Hello\nworld!」がどのようにマッチするかを確かめる例です。
const regexp1 = /^.+$/s; // sフラグあり const regexp2 = /^.+$/; // sフラグなし const str = 'Hello\nworld!'; console.log(str.match(regexp1)); (1) // (1) ['Hello // world!', index: 0, input: 'Hello // world!', groups: undefined] console.log(str.match(regexp2)); (2) // null
いずれも、境界アサーションを用いて文字列先頭と末尾を明示しています。改行文字の部分で出力が改行されているので少し見にくいですが、sフラグが有効な(1)では「Hello\nworld!」全体がマッチしているのに対し、sフラグが無効な(2)ではマッチする部分がないことが分かります。(2)では、文字列の先頭と末尾の間に改行文字がありますが、ドット(.)がそれにマッチしないので、全体としてマッチしないと見なされるのです。
マッチした範囲を保持する(d)
冒頭で紹介したように、match()メソッドなどでは、マッチング結果をオブジェクトとして返します(gフラグがない場合)。このオブジェクトのプロパティを図2に示します。
プロパティ | 概要 |
---|---|
groups | 名前付きキャプチャグループのオブジェクト |
index | マッチした位置 |
input | 検索対象の文字列そのもの |
indices | マッチした部分文字列の範囲の配列(dフラグ指定時のみ) |
表2 match()メソッドの返すオブジェクトのプロパティ |
dフラグは、マッチング結果のオブジェクトに、マッチした部分文字列の位置(indicesプロパティ)を追加します。ECMAScript 2022からサポートされました。元の文字列から部分文字列を切り出すなどの後処理に利用できますが、配列を作成して保持するという処理が加わるため、知らないうちにパフォーマンスが低下するのを嫌って、必要な場合にのみ作成するようにdフラグが設けられました。
以下の「hasindices.js」は、match()メソッドをdフラグを指定して呼び出した例です。
const str = 'Date: 2022-05-20'; const regexp = /(\d+)-(\d+)-(\d+)/d; console.log(str.match(regexp));
こちらは、実行結果です。
(4) ['2022-05-20', '2022', '05', '20', index: 6, input: 'Date: 2022-05-20', groups: undefined, indices: Array(4)] 0: "2022-05-20" ★ 1: "2022" 2: "05" 3: "20" groups: undefined index: 6 indices: Array(4) ☆ 0: (2) [6, 16] 1: (2) [6, 10] 2: (2) [11, 13] 3: (2) [14, 16] groups: undefined length: 4 [[Prototype]]: Array(0) input: "Date: 2022-05-20" length: 4
☆にindicesプロパティがあることに注目してください。★の部分文字列の配列に対応する形で、部分文字列の範囲(開始インデックス、終了インデックス+1)がさらに配列で保持されていることが分かります。dフラグなしだと、☆の部分が配列の内容も含めて表示されません。
ここで、indicesプロパティの下にgroupsプロパティが存在することに注意してください。match()メソッドの戻り値直下にあるgroupsプロパティが、各キャプチャグループの部分文字列をグループ名で参照できるのと同様に、部分文字列の範囲をグループ名で参照できます。以下の「hasindices_named.js」は、これを確かめる例です。
const str = 'Date: 2022-05-20'; const regexp = /(?<year>\d+)-(?<month>\d+)-(?<day>\d+)/d; const result = str.match(regexp); console.table(result.indices.groups);
こちらは、実行結果です。
配列を見やすくするために、console.table()メソッドを使用しています。indicesプロパティのgroupsプロパティで、名前付きキャプチャグループを参照できます。最終的に範囲のインデックスを取得するコードは、例えば以下のようになります。
const start = result.indices.groups.year[0]; const stop = result.indices.groups.year[1];
まとめ
今回は、正規表現におけるマッチングをカスタム化するフラグについて、ES2015以降でサポートされたものも含めて紹介しました。
次回は、パターンを境界とできる先読みアサーション、そしてES2018でサポートされた後読みアサーションをはじめとした、境界アサーションを紹介します。
筆者紹介
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.