TES Blog

株式会社テクニカルエンジニアリングサポートに所属する社員が、自身が携わるテクノロジーやイベントに関する情報を発信しています。

Unicodeの文字プロパティを指定した正規表現をみてみる(ECMAScript2018)

はじめに

Unicodeは、世界で使われる文字を利用できるようにすることを目的としています。 そのため、ラテン文字はもちろん、漢字、ハングル、キリル文字、タイ文字、(なんと!)絵文字までもがコード化されています。

一方、JavaScript(ECMAScript2018)ではUnicodeの文字プロパティにマッチングする正規表現の書き方が取り入れられます。
 関連:https://github.com/tc39/proposal-regexp-unicode-property-escapes
Unicodeの文字プロパティとは、Unicodeの規格で定められた各コードポイントの属性のことです。

まず簡単に見せると、以下のようなことができるようになります。

// 絵文字が含まれているかチェックする
/\p{Emoji}/u.test("😃");    // → true
/\p{Emoji}/u.test("絵文字"); // → false

// 平仮名が含まれているかチェックする
/\p{sc=Hiragana}/u.test("ひらがな");         // → true
/\p{sc=Hiragana}/u.test("漢字カタカナLatin"); // → false

夢が広がりますね!
本記事では、解説をしたのち、いくつかのサンプルを書いていきます。

※Unicode 10.0を前提にしています。
※本記事にあるサンプルコードは参考程度に留め、自己責任で使用してください。
※Chrome、Safari(記事投稿時点)で使えます。対応状況は、こちらでも確認できます。
http://kangax.github.io/compat-table/es2016plus/

目次

Unicodeの文字プロパティ

繰り返しになりますが、Unicodeの文字プロパティとは、Unicodeの規格で定められた各コードポイントの属性のことです。 コードポイントとは、Unicode上で割り振られているIDだと思ってください。

コードポイント 表示
U+0041 A
U+3042
U+1F361 🍡

Unicodeの文字プロパティには、いくつかの種類があります(今回関わるものだけ挙げます)。

  • General Category
  • Script
  • Script Extensions
  • 値がY/Nのどちらかであるプロパティ(たくさんあるので、ひとまとめにしました。以降Binary Propertyと呼びます)

General Category

General Categoryとは

「文字」「数字」「記号」のような、文字の大まかな分類です。 大分類と小分類があります。 大分類は7つあります。

大分類 説明
Letter 表音文字・表意文字。
一般的に言う「文字」のことです。
Mark 文字に付く記号。
例えば「Ã」の上についている記号。
よくヨーロッパの言語で見ますね。
Number 数字。
「1(半角文字)」「1(全角文字)」もここに含まれます。
ローマ数字やタイ文字の数字も該当します。
Punctuation 句読点、カッコ、ハイフンなどの記号。
次のSymbolに含まれない記号が、ここに入ります。
Symbol 数学記号、通貨記号、音声記号、絵文字など。
「!」「/」のような数学記号として使われるものであってもPunctuationに入っているものもあるので注意。
他に「㌔」「㍻」もあります。
Separator スペース。
ただし、Tab空白やCR、LFは含まれません(Otherに含まれます)。
Other 上記以外。
改行コードなどが含まれます。
未使用領域のコードポイントも対象です。

PunctuationとSymbolの使い分けはよく分かりませんでした…。
それぞれの大分類の中に小分類があります。ここではLetterだけ見ていきます。
なお、JavaScriptで指定できるScriptの一覧は、こちらでご覧になれます。
https://tc39.github.io/proposal-regexp-unicode-property-escapes/#table-unicode-general-category-values

小分類 説明 名前
uppercase 大文字。
「A(半角文字)」「A(全角文字)」など。
Uppercase_Letter
lowercase 小文字。
「a(半角文字)」「a(全角文字)」
Lowercase_Letter
titlecase 大文字→小文字で構成される合字。「Dž」など Titlecase_Letter
modifier 装飾文字。
日本語だと踊り字(「々」「ゝなど」)が該当します。
Modifier_Letter
other 上記以外。
ほとんどがここに含まれます。ひらがな、カタカナ、漢字も、ここです。
Other_Letter

さて、ここで「名前」が出てきました。 これはGeneral Categoryを識別する名前のことです。正規表現上では、これを使います。

書き方

「\p」を使って、以下のように書きます。 なお、u修飾子が無いと上手く動かなくなりますので、必ずつけるようにしましょう。

// General Categoryを指定してマッチングする
/\p{General_Category=Uppercase_Letter}/u // 大文字にマッチングする
/\p{General_Category=Lu}/u               // 値は、エイリアスでもOK
/\p{gc=Lu}/u                             // 「General_Category」もエイリアスで書ける

また、「○○以外」という否定の書き方もできます。「\P」を使います。

// General Categoryを指定し、それに該当しないものにマッチングする
/\P{General_Category=Uppercase_Letter}/u // 大文字以外にマッチングする
/\P{General_Category=Lu}/u               // 値は、エイリアスでもOK
/\P{gc=Lu}/u                             // 「General_Category」もエイリアスで書ける

さらにキー名「General_Category」を省略して書くこともできます。

/\p{Uppercase_Letter}/u // 大文字にマッチングする
/\p{Lu}/u               // 値エイリアスもOK

Script

Scriptとは

General Categoryは、「文字」「数字」「記号」で分類しましたが、Scriptは「ラテン文字」「アラビア文字」「ひらがな」「カタカナ」「漢字」のような観点で分類します。 たくさんありますので、いくつか列挙します。

  • Hiragana
  • Katakana
  • Greek ←ギリシャ文字
  • Latin ←ラテン文字
  • Han ←漢字

JavaScriptで指定できるScriptの一覧は、こちらでご覧になれます。 https://tc39.github.io/proposal-regexp-unicode-property-escapes/#table-unicode-script-values

特筆すべき点は、複数の分類にまたがるものについてです。 例えば、日本語の濁点や半濁点は「ひらがな」「カタカナ」の両方に分類できますが、ひとつのコードポイントは、ひとつの分類としているため、濁点や半濁点を両方に分類することはできません。 この場合、CommonあるいはInheritedに分類されます。長音「ー」も同様です。 このことは、のちに出てきますので覚えておいてください。

書き方

基本的にはGeneral Categoryと一緒です。

// Scriptを指定してマッチングする
/\p{Script=Hiragana}/u // ひらがなにマッチングする
/\p{Script=Hira}/u     // 値は、エイリアスでもOK
/\p{sc=Hira}/u         // 「Script」もエイリアスで書ける

否定の書き方です。

// Scriptを指定し、それに該当しないものにマッチングする
/\P{Script=Hiragana}/u // ひらがなにマッチングする
/\P{Script=Hira}/u     // 値は、エイリアスでもOK
/\P{sc=Hira}/u         // 「Script」もエイリアスで書ける

General Categoryと違って、キー名「Script」は省略できません。

Script Extensions

前節で濁音や半濁音、長音の記号は「ひらがな」や「カタカナ」には属しておらず、CommonあるいはInheritedに属していると書きました。 正規表現でScriptが「カタカナ」属性になっているコードポイントをマッチングしようと思った時、次のような場合に困ってしまいます。

// カタカナのみを受け付けるバリデーションを設定したいが、
// これだと結果がFalseになってしまい期待通りの動きにならない。
/^\p{Script=Katakana}+$/u.test("ソースコード"); // 長音記号はKatakanaではない

これでは不便ですので、Script Extensionsの出番です。 Script Extensionsでは、長音記号をHiraganaでもありKatakanaでもあるように扱うことができます。 よって以下のように書けば、上手く動くようになります。

/^\p{Script_Extensions=Katakana}+$/u.test("ソースコード");   // →true

// こっちも動く
/^\p{Script_Extensions=Hiragana}+$/u.test("ひらがなだよー"); // →true

// エイリアスでも書けます
/^\p{scx=Hira}+$/u.test("ひらがなだよー"); // →true

Script Extensionsの一覧は、こちらにあります。Scriptとの違いが分かります。 http://www.unicode.org/Public/UCD/latest/ucd/ScriptExtensions.txt

Binary Property

Binary Propertyとは

各コードポイントは、「○○かどうか」の情報を持っています。 例えば、「A」(U+0041)はこんな情報を持っています。

分類 Y/N 説明
ASCII Y アスキー文字かどうか。
Uppercase Y 大文字かどうか。
Lowercase N 小文字かどうか。
Emoji N 絵文字かどうか。
White_Space N 空白文字かどうか。
Quotation_Mark N 引用符かどうか。
Assigned Y 使用されているコードポイントかどうか。

JavaScriptで使えるBinary Propertyの一覧です。 https://tc39.github.io/proposal-regexp-unicode-property-escapes/#table-binary-unicode-properties

書き方

他のと違って、キー名は書きません。

/\p{White_Space}+/u // 空白文字にマッチングする
/\p{space}+/u       // エイリアスもあります

サンプル

ひらがなにマッチング

ひらがなで、ふりがなの入力を許可するバリデーションに使えます。

/^\p{scx=Hiragana}+$/u

これらにマッチングします。 豆腐になっているところは変体仮名です(フォントによっては豆腐になっていないかもしれません)。 見たこと無い文字もちらほらありますね。

ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖゝゞゟ𛀁𛀂𛀃𛀄𛀅𛀆𛀇𛀈𛀉𛀊𛀋𛀌𛀍𛀎𛀏𛀐𛀑𛀒𛀓𛀔𛀕𛀖𛀗𛀘𛀙𛀚𛀛𛀜𛀝𛀞𛀟𛀠𛀡𛀢𛀣𛀤𛀥𛀦𛀧𛀨𛀩𛀪𛀫𛀬𛀭𛀮𛀯𛀰𛀱𛀲𛀳𛀴𛀵𛀶𛀷𛀸𛀹𛀺𛀻𛀼𛀽𛀾𛀿𛁀𛁁𛁂𛁃𛁄𛁅𛁆𛁇𛁈𛁉𛁊𛁋𛁌𛁍𛁎𛁏𛁐𛁑𛁒𛁓𛁔𛁕𛁖𛁗𛁘𛁙𛁚𛁛𛁜𛁝𛁞𛁟𛁠𛁡𛁢𛁣𛁤𛁥𛁦𛁧𛁨𛁩𛁪𛁫𛁬𛁭𛁮𛁯𛁰𛁱𛁲𛁳𛁴𛁵𛁶𛁷𛁸𛁹𛁺𛁻𛁼𛁽𛁾𛁿𛂀𛂁𛂂𛂃𛂄𛂅𛂆𛂇𛂈𛂉𛂊𛂋𛂌𛂍𛂎𛂏𛂐𛂑𛂒𛂓𛂔𛂕𛂖𛂗𛂘𛂙𛂚𛂛𛂜𛂝𛂞𛂟𛂠𛂡𛂢𛂣𛂤𛂥𛂦𛂧𛂨𛂩𛂪𛂫𛂬𛂭𛂮𛂯𛂰𛂱𛂲𛂳𛂴𛂵𛂶𛂷𛂸𛂹𛂺𛂻𛂼𛂽𛂾𛂿𛃀𛃁𛃂𛃃𛃄𛃅𛃆𛃇𛃈𛃉𛃊𛃋𛃌𛃍𛃎𛃏𛃐𛃑𛃒𛃓𛃔𛃕𛃖𛃗𛃘𛃙𛃚𛃛𛃜𛃝𛃞𛃟𛃠𛃡𛃢𛃣𛃤𛃥𛃦𛃧𛃨𛃩𛃪𛃫𛃬𛃭𛃮𛃯𛃰𛃱𛃲𛃳𛃴𛃵𛃶𛃷𛃸𛃹𛃺𛃻𛃼𛃽𛃾𛃿𛄀𛄁𛄂𛄃𛄄𛄅𛄆𛄇𛄈𛄉𛄊𛄋𛄌𛄍𛄎𛄏𛄐𛄑𛄒𛄓𛄔𛄕𛄖𛄗𛄘𛄙𛄚𛄛𛄜𛄝𛄞🈀〱〲〳〴〵゙゚゛゜゠ーー゙゚

なぜか「🈀」が入ってしまっていて不自然ですね。 「🈀」だけ除外したい場合はこんな書き方で対応できます。

/^(?=[^🈀]*$)\p{scx=Hiragana}+$/u

※一部除外する正規表現を書く上で、こちらを参考にしました。 https://boukenki.info/sakuraeditor-and-kensaku/

カタカナにマッチング

今度はカタカナで、フリガナの入力を許可するバリデーションに使えます。

/^\p{scx=Katakana}+$/u

これらにマッチングします。

ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺヽヾヿㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ㋐㋑㋒㋓㋔㋕㋖㋗㋘㋙㋚㋛㋜㋝㋞㋟㋠㋡㋢㋣㋤㋥㋦㋧㋨㋩㋪㋫㋬㋭㋮㋯㋰㋱㋲㋳㋴㋵㋶㋷㋸㋹㋺㋻㋼㋽㋾㌀㌁㌂㌃㌄㌅㌆㌇㌈㌉㌊㌋㌌㌍㌎㌏㌐㌑㌒㌓㌔㌕㌖㌗㌘㌙㌚㌛㌜㌝㌞㌟㌠㌡㌢㌣㌤㌥㌦㌧㌨㌩㌪㌫㌬㌭㌮㌯㌰㌱㌲㌳㌴㌵㌶㌷㌸㌹㌺㌻㌼㌽㌾㌿㍀㍁㍂㍃㍄㍅㍆㍇㍈㍉㍊㍋㍌㍍㍎㍏㍐㍑㍒㍓㍔㍕㍖㍗ヲァィゥェォャュョッアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン𛀀〱〲〳〴〵゙゚゛゜゠ーー゙゚

↑では、一文字記号(「㌀」など)が含まれていますが、これを含めたくない場合があると思います。 その場合の正規表現です。

/^(?=[^㌀-㍗]*$)\p{scx=Katakana}+$/u

さらに一文字記号だけでなく囲み文字も除外する場合は、これです。

/^(?=[^㌀-㍗㋐-㋾]*$)\p{scx=Katakana}+$/u

漢字にマッチング

今回の中で一番使いみちがありそうな、漢字のマッチングです。
シンプルにかけます。ただし、日本語以外の漢字も含みますので、場合によっては注意が必要です。

/\p{Han}/u

これにマッチングする文字は↓で確認できます。 https://unicode.org/cldr/utility/list-unicodeset.jsp?a=%5Cp%7BHan%7D&g=&i=

スペース(改行含む)にマッチング

氏名で、姓と名との間にあるスペースを許可しないバリデーションなどに使えます。
このスペースには改行も含みます。

/^\p{White_Space}+$/u

また、「\P」を使ってスペース以外にマッチングすることで、スペース以外の文字数をカウントする処理が書けます。

const text = "検索したい文字  \t\t  ";
(text.match(/(\P{White_Space})/ug) || []).length; // →7

絵文字にマッチング

これは提案者が例示しているので、それを引用します。 https://github.com/tc39/proposal-regexp-unicode-property-escapes#matching-emoji

/\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu;

何やらいっぱいありますね。 絵文字は、絵文字そのものだけでなくモノクロ/カラーの切り替えコードポイントや肌色変更のコードポイントなどがあるため、上記のように複数のプロパティを指定する必要があります。 Unicodeの絵文字は非常に複雑ですので詳細は別の記事に譲ります。

通貨にマッチング

Webスクレイピングで、価格情報を抽出したいケースで使えます。 通貨の記号+数字にマッチングします。

/\p{Currency_Symbol}\d+/u

おわりに

ECMAScript2018で導入されるUnicodeの文字プロパティを指定した正規表現について見てきました。 英単語を使って書けるので可読性が上がったり、柔軟な処理を実装できたりと良さそうです。
私は知らなかったのですが、JavaScript以外にも他の言語(Ruby・Javaなど)でも似たようなことはできるようです。 もし他に実用的な使いみちがあれば、ぜひコメントで教えてください。

参考ページ