在正则表达式中使用 Unicode 属性转义

在前端开发中,产品经理有时候会给我们提一些“奇怪”的需求,比如过滤掉用户输入的 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 修饰符基本字符,本身是 Emoji
  • Emoji_Modifier 表示 Emoji 修饰符,必须和 Emoji_Modifier_Base 搭配使用
  • Emoji_Presentation 表示默认展示为 Emoji 的字符
  • Emoji 表示符号字符,不一定是 Emoji,必须搭配变体选择器(Variation Selector)VS16 U+FE0F 才能匹配 Emoji

除此之外,Unicode 标准中还包含 Emoji_Component 属性表示可组成 Emoji 的字符。

部分 Emoji 可以通过 VS16 U+FE0F 显示成 Emoji 风格,也可以通过 VS15 U+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: textfont-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 进行转译处理。

相关链接