在前端开发中,产品经理有时候会给我们提一些“奇怪”的需求,比如过滤掉用户输入的 Emoji 表情,或者 只允许用户输入中文。这往往需要我们编写一长串的正则表达式,众所周知 Unicode 字符每年都在增加, 手动维护这个正则表达式有些不切实际,有没有更优雅的方式呢?答案就是 Unicode 属性转义。
注:2020年5月10日预计发布 Unicode 13.0 标准,届时会有新的 Emoji 和汉字加到 Unicode 中。
更新:Unicode 13.0 已于 2020年3月10日发布。
Unicode 属性转义
从 ES2018 起,JS 开始支持在正则表达式中使用 Unicode 属性转义,该特性可以让我们直接使用 Unicode 标准中定义的属性名称来匹配字符。
基本语法
/\p{UnicodePropertyName=UnicodePropertyValue}/u
示例(以下结果都为 true
):
/\p{General_Category=Decimal_Number}/u.test('1');
/\p{Script=Greek}/u.test('π');
/\p{Script_Extensions=Greek}/u.test('π');
/\p{Script_Extensions=Hiragana}/u.test('あ'); // 平假名;
/\p{Script_Extensions=Katakana}/u.test('ア'); // 片假名;
/\p{Script=Common}/u.test('1');
二值属性语法
/\p{LoneUnicodePropertyNameOrValue}/u
二值属性即属性的值为 Yes 或 No。
它同时作为 General_Category
属性的简写形式,\p{UnicodePropertyValue}
等同于 \p{General_Category=UnicodePropertyValue}
。
示例(以下结果都为 true
):
/\p{Decimal_Number}/u.test('1'); // 匹配数字;
/\p{ASCII}/u.test('1'); // 匹配 ASCII 字符;
/\p{Alphabetic}/u.test('a'); // 匹配字母;
/\p{Lowercase}/u.test('r'); // 匹配小写;
/\p{Uppercase}/u.test('R'); // 匹配大写;
/\p{White_Space}/u.test(' '); // 匹配空格;
否定形式
和 \w
/\W
等元字符一样,\P
是 \p
的否定形式。
/\p{Script=Greek}/u.test('π'); // true 匹配希腊文;
/\P{Script=Greek}/u.test('π'); // false 匹配希腊文以外字符;
属性别名
大部分属性名可以进行简写。
General_Category
:gc
Script
:sc
Script_Extensions
:scx
所以上面的数字匹配有三种表示方式(以下结果都为 true
):
/\p{General_Category=Decimal_Number}/u.test('1');
/\p{gc=Decimal_Number}/u.test('1');
/\p{Decimal_Number}/u.test('1');
其他示例
匹配单词(以下结果都为 true
):
/\w+/.test('hello');
/([a-zA-Z0-9_])+/.test('hello');
/([\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]+)/gu.test('hello');
匹配数字(以下结果都为 true
):
/^\d+$/.test('6');
/^\p{Decimal_Number}+$/u.test('𝟞');
/^\p{Number}+$/u.test('Ⅵ');
匹配 Emoji
RegExp Unicode Property Escapes 提案的介绍页面已经给出 Emoji 的正则表达式:
/\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu
其中
Emoji_Modifier_Base
表示 Emoji 修饰符基本字符,本身是 EmojiEmoji_Modifier
表示 Emoji 修饰符,必须和Emoji_Modifier_Base
搭配使用Emoji_Presentation
表示默认展示为 Emoji 的字符Emoji
表示符号字符,不一定是 Emoji,必须搭配变体选择器(Variation Selector)VS16U+FE0F
才能匹配 Emoji
除此之外,Unicode 标准中还包含 Emoji_Component
属性表示可组成 Emoji 的字符。
部分 Emoji 可以通过 VS16
U+FE0F
显示成 Emoji 风格,也可以通过 VS15U+FE0E
显示成文本风格。文本风格的符号算不算 Emoji?这个问题仁者见仁,智者见智。
示例:
const regexEmoji = /\p{Emoji}/u;
const regexEmojiWithModifier = /\p{Emoji}\p{Emoji_Modifier}/u;
const regexEmojiComponent = /\p{Emoji_Component}/u;
const regexEmojiFull = /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu;
regexEmoji.test('✌️'); // true 字符为 "\u270C\uFE0F"
regexEmojiWithModifier.test('✌️'); // false 字符为 "\u270C\uFE0F"
regexEmojiWithModifier.test('✌🏽'); // true 字符为 "\u{270C}\u{1F3FD}" 或 "\u270C\uD83C\uDFFD"
regexEmoji.test('1'); // true
regexEmojiComponent.test('1'); // true
regexEmoji.test('☺'); // true 字符为 "\u263A"
regexEmoji.test('☺️'); // true 字符为 "\u263A\uFE0F"
regexEmoji.test('☺︎'); // true 字符为 "\u263A\uFE0E"
regexEmojiFull.test('☺'); // false 字符为 "\u263A"
regexEmojiFull.test('☺️'); // true 字符为 "\u263A\uFE0F"
regexEmojiFull.test('☺︎'); // false 字符为 "\u263A\uFE0E"
// 示例来自 https://github.com/tc39/proposal-regexp-unicode-property-escapes
const regex = /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu;
const text = `
\u{231A}: ⌚ default emoji presentation character (Emoji_Presentation)
\u{2194}\u{FE0F}: ↔️ default text presentation character rendered as emoji
\u{1F469}: 👩 emoji modifier base (Emoji_Modifier_Base)
\u{1F469}\u{1F3FF}: 👩🏿 emoji modifier base followed by a modifier
`;
let match;
while (match = regex.exec(text)) {
const emoji = match[0];
console.log(`Matched sequence ${ emoji } — code points: ${ [...emoji].length }`);
}
注:虽然 CSS 的 unicode-range
支持范围为 U+0-10FFFF
,覆盖本文涉及到的 Emoji,但目前并不支持变体选择器,不同系统不同浏览器显示效果无法保证,受字体限制,可能全部显示为文本风格或彩色 Emoji。CSS Fonts Module Level 4 草案将会增加 font-variant-emoji
属性,到时候可以使用 font-variant-emoji: text
或 font-variant-emoji: emoji
指定文本或 Emoji 风格(对于不单独写样式,能否实现同一段文本中 \u263A\uFE0F
显示成文本风格,\u263A\uFE0E
显示成 Emoji 风格,仍然存疑)。
匹配中文
“匹配中文”是个不那么精确的概念,精确一点的需求描述是“匹配汉字”,对此网上已经有一篇文章详细分析过这个问题JavaScript 正则表达式匹配汉字。
其中涉及以下正则表达式:
/\p{Ideographic}/u
表意文字,包含汉字、西夏文、女书等表意文字。/\p{Unified_Ideograph}/u
统一表意文字,即统一汉字,包含中日韩越使用的汉字。/\p{Script=Han}/u
汉文,包含汉文书写系统中所有字符,汉字以及其他字符(如〇々〩)。可以简写成/\p{sc=Han}/u
。/\p{Script_Extensions=Han}/u
不一定是汉文书写系统字符,但书写为汉文,如 🉑🉐㊗️🈶🈚 等 Emoji。可以简写成/\p{scx=Han}/u
。
文章指出 /\p{Unified_Ideograph}/u
即是“匹配所有汉字”的正则表达式。
在实际使用时需要注意 Unified_Ideograph
包含了汉字偏旁部首(部分偏旁部首本身就是汉字单字)
和日本、韩国、越南使用的汉字,不包括全角标点符号、注音符号等中文日常使用涉及的符号字符(也不包括中文小写数字“〇”和叠字符号“々”)。
示例:
// 中日韩越汉字;
/\p{Unified_Ideograph}/u.test('汉') // true;
/\p{Unified_Ideograph}/u.test('亀') // true;
// 和制汉字(日本汉字);
/\p{Unified_Ideograph}/u.test('雫') // true;
/\p{Unified_Ideograph}/u.test('凪') // true;
// 越南喃字;
/\p{Unified_Ideograph}/u.test('𢆥') // true;
// 苏州码子: 〡、〢、〣、〤、〥、〦、〧、〨、〩、十;
/\p{Unified_Ideograph}/u.test('〩') // false;
/\p{Ideographic}/u.test('〩') // true;
/\p{Script=Han}/u.test('〩') // true;
// 汉字偏旁部首;
/\p{Unified_Ideograph}/u.test('阝') // true;
/\p{Unified_Ideograph}/u.test('扌') // true;
// 注音符号;
/\p{Unified_Ideograph}/u.test('ㄓ') // false;
/\p{Script=Han}/u.test('ㄓ') // false;
// 汉字 Emoji;
/\p{Unified_Ideograph}/u.test('🉐') // false;
/\p{Script=Han}/u.test('🉐') // false;
/\p{Script_Extensions=Han}/u.test('🉐') // true;
// 汉字小写数字零;
/\p{Unified_Ideograph}/u.test('〇') // false;
/\p{Script=Han}/u.test('〇') // true;
// 中文叠字符号;
/\p{Unified_Ideograph}/u.test('々') // false;
/\p{Script=Han}/u.test('々') // true;
总结
善用 Unicode 属性转义可以避免手动维护 Unicode 码点形式的正则表达式,对于尚未支持的浏览器可以使用 Babel 进行转译处理。