正規表現における文字の指定方法を深める――「文字クラス」と「Unicodeプロパティエスケープ」:ECMAScriptで学ぶ正規表現(4)
正規表現の基本と、ECMAScript(JavaScript)における利用方法を紹介する連載。今回は、文字クラスによる文字の指定方法と、便利なUnicodeプロパティエスケープについて。
正規表現を使うポイントに、「検索するパターンをどのように指定するか?」ということがあります。ここで重要なのが、文字の種類に基づく文字クラスです。一般的な正規表現では大ざっぱな指定しかできませんでしたが、Unicodeプロパティエスケープを使うことで日本語でも緻密な指定が可能になりました。今回は、文字クラスについておさらいし、本命のUnicodeプロパティエスケープを紹介していきます。
文字クラス
文字クラスについては、第2回で代表的なメタ文字であると紹介しました。文字クラスを使うと、大ざっぱな指定で特定の文字種を表現できます。表1に、文字クラスの一覧を示します(第2回の表1を詳細にして再掲)。
メタ文字 | 機能 |
---|---|
. | あらゆる1文字とマッチする |
\d | 数字とマッチする |
\D | 数字以外とマッチする |
\w | 英数字とマッチする |
\W | 英数字以外とマッチする |
\s | ホワイトスペース文字とマッチする |
\S | ホワイトスペース文字以外とマッチする |
\t | 水平タブ文字(0x09、U+0009)とマッチする |
\v | 垂直タブ文字(0x0b、U+000B)とマッチする |
\r | 復帰文字(0x0d、U+000D)とマッチする |
\n | 改行文字(0x0a、U+000A)とマッチする |
\f | 改ページ文字(0x0c、U+000C)とマッチする |
[\b] | バックスペース文字(0x08、U+0008)とマッチする。単独で使うと単語境界を表すメタ文字\bと被るので文字集合([ ])の内部でのみ使う |
\0 | ヌル文字(0x00、U+0000)とマッチする。0〜7の数字を続けると8進数と解釈されてしまうので注意 |
\cX | 制御文字とマッチする。XはA〜Zの英大文字。いわゆるCtrl+Zなら\cZというように表記する |
\xhh | 2桁の16進数(hh)からなる文字とマッチする(\x20以上) |
\uhhhh | 4桁の16進数(hhhh)からなるUnicodeコードポイントとマッチする(\u0020以上) |
\u{hhhh}〜\u{hhhhhh} | 4〜6桁の16進数からなるUnicodeコードポイントとマッチする(使用にはuフラグが必要) |
表1 文字クラス |
このうち、ドット(.)から\Sまでは第2回で紹介しました。今回は、制御文字やコード指定について紹介します。
[NOTE]Unicodeコードポイント
表1において、タブ文字や復帰文字のコードを「U+0009」などと示しましたが、これはUnicodeにおけるコードポイントを表します。コードポイントとは、Unicodeにおける文字コードのようなもので、各文字に与えられた識別子です。Unicodeであることを表すUの文字とともに、プラス記号(+)に続けて16進数でコードを表記します。Unicodeが制定された初期の頃は4桁の16進数でコードポイントを指定していましたが、その後の文字の拡張で現時点では最大6桁の16進数で指定します。
制御文字にマッチする\t、\nなど
\tや\nなどは、制御文字にマッチします。制御文字とは、目に見える文字と異なり、テキスト(文字列)の画面表示などを制御するために用いられる文字です。最もよく使われるのが、水平タブ文字(以降、タブ文字)と改行文字でしょう。タブ文字は、文字の表示位置をそろえるのに用いられますし、改行は文字通り改行に用いられます。
本来は、画面表示などの制御に用いられるのが制御文字なのですが、タブ文字は文字列中で区切り文字として用いられたり、改行文字は1個の文字列で見た目が複数行からなる文字列を表現するための区切り文字として用いられたりします。つまり、文字列を扱う際に制御文字がそこに含まれていることがあるので、これらにマッチさせるために\tなどのメタ文字を用います。
以下の「control_char.js」は、異なる制御文字で区切られた文字列を分割します。ここではタブ文字以外も区切りに用いましたが、タブ文字を区切りに用いた文字列は、TSV(Tab Separated Value)などと呼ばれます。
const regexp = /[\t\f\n]/g; // 複数の制御文字を指定 const str = '1\t山内直\f1980\n男'; // (1)複数の制御文字で区切られた文字列 console.log(str.split(regexp)); // (2)正規表現で分割 // (4) ['1', '山内直', '1980', '男']
ここで(2)に、split()メソッドが登場しました。split()は第1回で紹介していますが、区切り文字列を指定して文字列を分割するStringクラスのメソッドです。区切り文字列に正規表現を指定した場合には、正規表現にマッチする部分で文字列が分割されます。
また、(1)では文字列中に制御文字を記述しています。これは正規表現におけるメタ文字とは異なりますが、同じくタブ文字などを表現しています。これらの文字がマッチし、split()メソッドによって分割され、結果として4つの部分文字列が返ってくる、というわけです。
文字コードにマッチする\xhh、\uhhhh
\xhhと\uhhhhは、指定された文字コードを持つ文字にマッチします。\xhhは2桁の16進数を指定するので、主にASCII文字の指定に用いられます。近年は、Unicodeでテキストデータを取り扱うことが多くなっているので、\uhhhhを使ってUnicodeにおけるコードポイントを指定する機会も増えてくるでしょう。なお、この\uhhhhは、Unicodeエスケープシーケンスとも呼ばれることがあります。
以下の「char_code.js」は、カンマ(,)あるいはタブ文字を含む文字列を分割します。カンマとタブ文字のいずれかで区切られた文字列に対応できます。なお、カンマで区切られた文字列は、CSV(Comma Separated Value)などと呼ばれます。正規表現パターンを16進数とUnicodeエスケープシーケンスなどさまざまなパターンで行っていて、結果は全て同じになります。
const regexp1 = /[\x2c\cI]/g; // カンマとタブ文字を指定 const str1 = '1,山内直,1980,男'; // カンマで区切られた文字列 const str2 = '1\t山内直\t1980\t男'; // タブ文字で区切られた文字列 console.log(str1.split(regexp1)); // (4) ['1', '山内直', '1980', '男'] console.log(str2.split(regexp1)); // (4) ['1', '山内直', '1980', '男'] const regexp2 = /[\u002c\t]/g; // Unicodeでカンマ、それにタブ文字を指定 console.log(str1.split(regexp2)); // (4) ['1', '山内直', '1980', '男'] console.log(str2.split(regexp2)); // (4) ['1', '山内直', '1980', '男']
[NOTE]\xhhにおける制限
\xhhと\uhhhhの指定において、\x20(\u0020)未満の値(制御文字に相当)はエラーにこそなりませんが非推奨です。代わりに\t、\n、\0、\cなどを使ってください。\x1aなら\cZとなります。上記サンプルはこれに準じています。
Unicodeコードポイントエスケープでマッチする\u{hhhh}など
\uにブレース({ })を続けると、それはUnicodeコードポイントエスケープとなり、\uhhhhと同様にUnicodeコードポイントを指定することができます。この記法を使用するには、ECMAScript 2015から使用できるようになったuフラグが必要です。
\u{}では、コードポイントは6桁まで(正確には0x10FFFFまで)指定できます。つまり、\u{hhhhhh}といった指定が可能ということです。Unicodeでは、16進数で4桁を超えるコードポイントを扱うことができますが、\u{}ではこれを直接指定できるというわけです(\uhhhhでは4桁までです)。
なお、\u{}を使わずに\uhhhhを2個使って同様の目的を果たすことができます。Unicodeコードポイントエスケープを用いない方法は、サロゲートペアというものです(サロゲートとは「代理」という意味)。\uhhhhでは16進数で4桁までのコードポイントしか指定できないので、4桁を超えるコードポイントに対しては\uhhhhを2個組み合わせて指定します。これには、Unicodeで使用されていない\uD800〜\uDBFFを上位サロゲート、\uDC00〜\uDFFFを下位サロゲートとして、コードポイントから下記の計算によって得られる値を使用します。
- コードポイントから0x10000を引いて、それをXとする
- Xを0x0400で割ってその商を0xD800に足して上位サロゲートとする
- Xを0x0400で割ってその剰余を0xDC00に足すして下位サロゲートとする
下記の「unicode_codepoint.js」では、ロケットの絵文字🚀を2通りの方法(fromCodePoint()メソッドを使ってコードポイントから指定、同じくサロゲートペアから計算)で表示しています。
const rocket = 0x1F680; const str1 = String.fromCodePoint(rocket); console.log(`\\u{${rocket.toString(16)}}は${str1}`); // \u{1f680}は🚀 const x = rocket - 0x10000; const upper = Math.floor(x / 0x0400) + 0xD800; const lower = (x % 0x0400) + 0xDC00; const str2 = String.fromCodePoint(upper, lower); console.log(`\\u${upper.toString(16)}\\u${lower.toString(16)}は${str2}`); // \ud83d\ude80は🚀
Unicodeプロパティエスケープ
Unicodeプロパティエスケープとは、ECMAScript 2018からサポートされた、Unicode文字体系における文字のグループ分けをする仕組みです。正規表現には、第2回で紹介した列挙や範囲にて文字集合を指定するメタ文字([ ])がありますが、単純な英数字ならともかく、日本語を含む世界中の文字を範囲指定するには向きません。どのような文字体系でもそうですが、文字の前後関係を把握した上で正確に範囲を指定するのは骨の折れる作業ですし、可読性も良くありません。
そこで、Unicodeプロパティを利用して文字種を指定できるようにしたのがUnicodeプロパティエスケープです。Unicodeプロパティエスケープは、uフラグを指定した上で、メタ文字\p{}を使って指定します。\wと\Wの関係と同様に、大文字の\P{}とすると否定の意味になります。
Unicodeプロパティとは?
Unicodeプロパティとは、Unicodeの規格で定められている、各コードポイントに与えられる属性です。コードポイントとは、各文字に割り当てられる識別子のことと紹介しました。例えば、平仮名の「あ」のコードポイントはU+3042で、ここに属性としてプロパティ(平仮名)が割り当てられます。
Unicodeプロパティの指定は、\p{Name=Value}形式と\p{LoneNameOrValue}形式のいずれかで行います。\p{Name=Value}形式では、Nameで指定される属性の値がValueであるコードポイントにマッチします。ECMAScriptでは、Nameに指定できる属性は以下の3つのみです。
- 一般カテゴリー(General_Category、別名はgc、省略可)
- スクリプト(Script、別名はsc)
- スクリプト拡張(Script_Extensions、別名はscx)
\p{LoneNameOrValue}は、LoneNameOrValueで指定される属性を持つコードポイントにマッチします。この属性は、特にバイナリプロパティと呼ばれます。なお、一般カテゴリーのNameは省略可能なので、\p{LoneNameOrValue}形式でUnicodeプロパティエスケープが指定されたときには、まずValueが一般カテゴリーにおけるものかどうかを調べます。一致するものがあれば、それは一般カテゴリーの指定とされます。一致しなければ、それはバイナリプロパティの指定と見なされます。
以降、それぞれについて紹介していきます。
一般カテゴリー(General_Category、gc)
一般カテゴリーとは、英字、数字、記号のような、文字の大まかな分類とそれらを細分化した分類の組み合わせで、Unicodeプロパティにおいて最も基本的な分類です。アンダースコアを区切りに用いた「大まかな分類_細分化した分類」の形で表現し、「大まかな分類」のみの形式も指定できるようになっています。主な一般カテゴリーを表2に示します。
名称(別名) | 概要 |
---|---|
Letter(L) | 表音文字、表意文字。いわゆる「文字」のこと |
Uppercase_Letter(Lu) | 英大文字(半角、全角) |
Lowercase_Letter(Ll) | 英小文字(半角、全角) |
Mark(M) | 文字に付く記号。例えば「ä」の上に付く記号(ウムラウト¨など) |
Nonspacing_Mark(Mn) | Markのうち文字幅を持たないもの |
Number(N) | 数字。半角文字も全角文字も含まれる。ローマ数字も含まれる |
Decimal_Number(Nd) | 10進数で使われる数字 |
Punctuation(P) | 句読点、カッコ、ハイフンなど |
Connector_Punctuation(Pc) | タイのようなPunctuation |
Symbol(S) | 数学記号、通貨記号、音声記号、絵文字など |
Math_Symbol(Sm) | 数学で使われる記号 |
Separator(Z) | 空白文字 |
Space_Separator(Zs) | 空白文字で幅を持つもの |
Other(C) | 上記以外 |
Control(Cc) | 制御コード |
表2 Unicodeプロパティ(一般カテゴリー) |
非常に大ざっぱなくくりですが、表2の名称欄にある名前を指定して(別名も可能)、一般カテゴリーによるマッチングを行うことができます。下記の「unicode_gc.js」は、別名を含めた3つの方法でマッチングを試しています
const str = 'Hello, World!!'; const regexp1 = /\p{General_Category=Lowercase_Letter}/ug; console.log(str.match(regexp1)); // (8) ['e', 'l', 'l', 'o', 'o', 'r', 'l', 'd'] const regexp2 = /\p{General_Category=Lu}/ug; console.log(str.match(regexp2)); // (2) ['H', 'W'] const regexp3 = /\p{gc=Lo}/ug; console.log(str.match(regexp3)); // null
[NOTE]ECMAScriptで指定できる一般カテゴリー
ECMAScriptで指定できる一般カテゴリーについては、下記に一覧がありますので参考にしてください(英語)。
Value aliases and canonical values for the Unicode property General_Category
スクリプト(Script、sc)
スクリプトは、文字を「平仮名」「片仮名」「漢字」「アラビア文字」といった観点で分類した属性です。一般カテゴリーは、文字、数字、記号といった分類でしたので、より文字の性質に近い分類と言えます。スクリプトには、一般カテゴリーのような階層はありません。例えば平仮名なら、Hiraganaというスクリプトになります。主なスクリプトを表3に示します。
名称(別名) | 概要 |
---|---|
Hiragana(Hira) | 平仮名 |
Katakana(Kana) | 片仮名 |
Greek(Grek) | ギリシャ文字 |
Latin(Latn) | ラテン文字 |
Han(Hani) | 漢字 |
表3 Unicodeプロパティ(スクリプト) |
以下の「unicode_script.js」は、スクリプトに平仮名、漢字、ギリシャ文字を指定してマッチングを行っています。
const str = 'こんにちはー、世界!'; const regexp1 = /\p{Script=Hiragana}+/ug; console.log(str.match(regexp1)); // (1) ['こんにちは'] const regexp2 = /\p{Script=Han}+/ug; console.log(str.match(regexp2)); // (1) ['世界'] const regexp3 = /\p{Script=Greek}+/ug; console.log(str.match(regexp3)); // null
平仮名については問題ないと思われますが、漢字については要注意です。ここで言う漢字は、CJK文化圏すなわち中国、日本、朝鮮におけるもの全てが含まれます。ですので、中国における繁体字、簡体字なども含まれて必ずしも期待するマッチ結果にならないことがあります。
[NOTE]ECMAScriptで指定できるスクリプトとスクリプト拡張
ECMAScriptで指定できるスクリプトと、次に紹介するスクリプト拡張については、下記に一覧がありますので参考にしてください(英語)。
Value aliases and canonical values for the Unicode properties Script and Script_Extensions
スクリプト拡張(Script_Extensions、scx)
上記「unicode_script.js」の例の実行結果を見ると、音引き(ー)は平仮名にマッチしていません。これは片仮名も同様です。音引きや濁点などは、CommonあるいはInheritedといったスクリプトに分類されます。ですが「カレンダー」のように、それで一語をなす場合、「ー」だけマッチしないのでは不便です。そのようなときのために、スクリプト拡張というものを使用することができます。
スクリプト拡張は、音引き(ー)などを平仮名とも片仮名ともマッチするようにできるプロパティです。スクリプト拡張では、コードポイントが属するスクリプトを複数対応付けることによって、いずれかの属性を持っていると見なすことができます。例えば音引き(ー)には、HiraとKana(それぞれHiraganaとKatakanaの別名)が割り当てられています。
現時点で、平仮名と片仮名に関連したスクリプト拡張には14個のコードポイントが割り当てられています。それらを表4に示します。
字種 | 文字 |
---|---|
繰り返し(縦書き) | 〱 〲 〳 〴 〵 |
濁点、半濁点 | ゙ ゚ ゛ ゜ |
二重ハイフン | ゠ |
音引き | ー |
音引きと濁点、半濁点(半角) | ー ゙ ゚ |
表4 Unicodeプロパティ(スクリプト拡張) |
以下の「unicode_scriptex.js」は、スクリプト拡張を含めた平仮名と片仮名のマッチングを行っています。「〱」は「くノ字点」を意味する文字で、か行う段の「く」ではありませんので注意してください。
const str1 = 'さら〱、さら〱と葉ずれの音がして、'; // くノ字点を含む文字列 const regexp1 = /\p{Script_Extensions=Hiragana}+/ug; console.log(str1.match(regexp1)); // (3) ['さら〱、さら〱と', 'ずれの', 'がして、'] const str2 = 'Signalize!/カレンダーガール'; // 音引きを含む文字列 const regexp2 = /\p{scx=Katakana}+/ug; console.log(str2.match(regexp2)); // (1) ['カレンダーガール']
バイナリプロパティ
バイナリプロパティとは、各コードポイントが「〜かどうか?」という情報を返します。各コードポイントは、一般カテゴリーやスクリプトについては1個しか持てなかったのに対し、バイナリプロパティについては「大文字か?」「絵文字か?」というように複数のプロパティにマッチすることができます。一般的なプログラミング言語におけるisXXXX()メソッドのようなものと思えばよいでしょう。
代表的なバイナリプロパティを表5に示します。
名称 | 概要 |
---|---|
ASCII | ASCII文字かどうか |
Assigned | 使用されているコードポイントかどうか |
Emoji | 絵文字かどうか |
Lowercase | 小文字かどうか |
Uppercase | 大文字かどうか |
White_Space | 空白文字かどうか |
表5 Unicodeプロパティ(バイナリプロパティ) |
下記の「unicode_bp.js」では、バイナリプロパティの有無をテストしています。
const str = 'Hello, World!!'; const regexp1 = /[\p{White_Space}\p{Uppercase}]/ug; console.log(str.match(regexp1)); // (3) ['H', ' ', 'W'] const regexp2 = /\p{White_Space}/ug; console.log(str.match(regexp2)); // (1) [' ']
[NOTE]ECMAScriptで指定できるバイナリプロパティ
ECMAScriptで指定できるバイナリプロパティについては、下記に一覧がありますので参考にしてください(英語)。
Binary Unicode property aliases and their canonical property names
[NOTE]POSIX文字クラス
文字クラスには、POSIX文字クラスというものもあります。POSIX文字クラスとは、上記の文字クラスとは異なる形式による、POSIX(Portable Operating System Interface)で定められる文字クラスの記法です。文字集合を示すブラケット([ ])の内部でのみ使用でき、コロン(:)で挟んで文字種を指定します。例えば、英数字なら[:alnum:]、英字なら[:alpha:]、数字なら[:digit:]というように指定します。残念ながら、ECMAScriptではPOSIX文字クラスはサポートされていませんが、Unix系のツール(vi、grepなど)をはじめ、Perl、PHP、Rubyといったスクリプト言語ではサポートされています。例えばPHPでは、以下のようにPOSIX文字クラスを使って判定できます。
<?php if(ereg('[:alnum:]', 'abcdefg0123456789こんにちは')) …中略… ?>
同様の目的なら、より応用範囲の広いUnicodeプロパティエスケープをECMAScript 2018から使えますので、POSIX文字クラスが使えないからといって不便に感じることはないでしょう。
まとめ
この回では、ECMAScriptにおける文字クラスの詳細について、特にUnicode関係のものを中心に紹介しました。
次回は、第2回でも取り上げた数量詞について、さらに掘り下げて紹介します。
筆者紹介
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.