正则表达式技巧-利用 Unicode 属性
在前端开发中,正则表达式是一个强大而复杂的工具。随着 ECMAScript 2018 (ES9) 的推出,我们获得了一种新的、更简单的方式来处理复杂的文本模式:Unicode 属性转义。
什么是 Unicode 属性转义?
Unicode 属性转义是 ES9 引入的一种新的正则表达式语法,用于匹配具有特定 Unicode 属性的字符。
常规方法 vs Unicode 属性转义
让我们以匹配数字为例来说明这个改进:
// 常规方法
const regularExpression = /\d/g
// Unicode 属性转义
const unicodeEscape = /\p{Number}/gu
const testString = '1234567890Ⅴ'
// 输出: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]
console.log(testString.match(regularExpression))
// 输出: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "Ⅴ"]
console.log(testString.match(unicodeEscape))
这个 Ⅴ 是罗马数字 5,所以常规方法无法匹配到,在一般场景中,可能没有问题,但是当涉及到国际化时,可能就需要考虑到不同的数字表示法。
上面的场景,可能很少遇到,但是下面这个匹配汉字的场景,就很常见了:
// 仅匹配汉字(不带标点)
const regularExpression = /[\u4E00-\u9FCC\u3400-\u4DB5\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|[\uD86A-\uD86C][\uDC00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D]/g
const unicodeEscape = /\p{Script=Han}/gu
const testString = '这是一个测试'
// 输出: ["这", "是", "一", "个", "测", "试"]
console.log(testString.match(regularExpression))
// 输出: ["这", "是", "一", "个", "测", "试"]
console.log(testString.match(unicodeEscape))
可以看到,使用 Unicode 属性转义,不仅更简洁,而且更全面,因为它匹配所有的汉字,包括那些超出传统 Unicode 范围的字符。
常用的 Unicode 属性转义
- 匹配表情符号(Emojis)
const emojiRegex = /\p{Emoji}/u
- 匹配所有字母(包括非拉丁字母)
const letterRegex = /\p{Letter}/u
- 匹配数字字符(包括其他语言的数字)
const numberRegex = /\p{Number}/u
- 匹配标点符号
const punctuationRegex = /\p{Punctuation}/u
- 匹配空白字符
const whitespaceRegex = /\p{White_Space}/u
- 匹配特定语言的字符(例如西里尔字母)
const cyrillicRegex = /\p{Script=Cyrillic}/u
- 匹配组合字符(例如重音符号)
const combiningMarkRegex = /\p{Mark}/u
- 匹配货币符号
const currencyRegex = /\p{Currency_Symbol}/u
改进的 Unicode 属性转义 v flag
前面使用的/u flag 是 ES2015 引入的,在 今年的ES2024 中,又正式引入了新的 flag v,用于改进 Unicode 属性转义。 v flag 又有什么不同?
支持字符串属性
Unicode字符属性会扩展为一组字符编号,因此可以被转译为包含它们各自匹配的字符编号的字符类。例如,\p{ASCII_Hex_Digit}
等同于[0-9A-Fa-f]
:它每次只匹配单个Unicode字符编号,这并不能满足所有场景的需求。
例如以下场景:
// 匹配所有的表情符号
const re = /^\p{Emoji}$/u
// 匹配一个由单个字符编号组成的表情符号
re.test('⚽') // '\u26BD'
// → true ✅
// 匹配一个由多个字符编号组成的表情符号
re.test('👨🏾⚕️') // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F'
// → false ❌
在上述示例中,正则表达式不匹配 👨🏾⚕️ 表情符号,因为它恰好由多个Unicode 字符编号组成,而 Emoji 是匹配一个Unicode 字符编号。
解决方法就是,利用 Unicode 的字符串属性,而 /v flag 可以支持字符串属性。
Unicode标准还定义了一些字符串属性。这些属性扩展为一组字符串,每个字符串包含一个或多个字符编号。在正则表达式中,字符串属性转换为一组替代选项。为了说明这一点,假设有一个Unicode属性适用于字符串'a'、'b'、'c'、'W'、'xy'和'xyz'。这个属性可以转换为以下任一正则表达式模式(使用交替):xyz|xy|a|b|c|W 或 xyz|xy|a-cW。(最长的字符串放在前面,这样前缀如'xy'就不会隐藏较长的字符串如'xyz'。)与现有的Unicode属性转义不同,这种模式可以匹配多字符字符串。
const re = /^\p{RGI_Emoji}$/v;
// Match an emoji that consists of just 1 code point:
re.test('⚽'); // '\u26BD'
// → true ✅
// Match an emoji that consists of multiple code points:
re.test('👨🏾⚕️'); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F'
// → true ✅
这段代码引用了 Unicode 定义的字符串属性 RGI_Emoji,它表示"适合日常使用的表情符号的子集"。有了这个属性,我们现在可以匹配表情符号,而不用考虑它们在底层由多少个字符编号组成!
v flag 从一开始就支持以下 Unicode 字符串属性:
- Basic_Emoji(基本表情符号)
- Emoji_Keycap_Sequence(键帽序列表情符号)
- RGI_Emoji_Modifier_Sequence(RGI 表情符号修饰符序列)
- RGI_Emoji_Flag_Sequence(RGI 表情符号国旗序列)
- RGI_Emoji_Tag_Sequence(RGI 表情符号标签序列)
- RGI_Emoji_ZWJ_Sequence(RGI 表情符号零宽连接符序列)
- RGI_Emoji(所有 RGI 表情符号)
随着 Unicode 标准定义更多的字符串属性,这个支持的属性列表可能会在未来增长。虽然目前所有的字符串属性都与表情符号相关,但未来的字符串属性可能会服务于完全不同的用例。
对于 Web 开发者来说,这意味着:
- 更简单的表情符号处理:你可以使用一个简单的正则表达式来匹配所有类型的表情符号,不管它们的内部结构如何复杂。
- 更强大的文本分析能力:这为开发聊天应用、社交媒体分析工具等提供了更好的支持。
- 国际化支持的改进:更容易处理不同语言和文化中的特殊字符和符号。
- 未来扩展性:随着 Unicode 标准的发展,你的应用将能够更容易地适应新的文本处理需求。
- 性能优化:使用这些内置属性可能比手动实现复杂的正则表达式更高效。
集合表示法 + 字符串字面量语法
集合操作
v flag 引入了一些强大的集合操作,让正则表达式的匹配更加灵活和精确。这些操作对于 Web 开发者来说特别有用,可以大大简化复杂的文本处理任务。
差集操作 (--)
使用 A--B
语法可以匹配在 A 中但不在 B 中的字符串。这在需要排除特定字符或字符串时非常有用。
例如,匹配除了 π 以外的所有希腊字符:
/[\p{Script_Extensions=Greek}--π]/v.test('π'); // → false ❌
/[\p{Script_Extensions=Greek}--π]/v.test('α'); // → true ✅
甚至可以排除多个字符:
/[\p{Script_Extensions=Greek}--[αβγ]]/v.test('α'); // → false ❌
/[\p{Script_Extensions=Greek}--[αβγ]]/v.test('δ'); // → true ✅
交集操作 (&&)
A&&B
语法匹配同时在 A 和 B 中的字符串。这对于创建更精确的匹配模式非常有用。
例如,匹配希腊字母:
const re = /[\p{Script_Extensions=Greek}&&\p{Letter}]/v;
re.test('π'); // → true ✅
re.test('𐆊'); // → false ❌ (希腊数字符号,不是字母)
并集操作
v flag 还增强了并集操作的功能,现在可以结合字符串属性、字符属性和字符串字面量:
const re = /^[\p{Emoji_Keycap_Sequence}\p{ASCII}\q{🇧🇪|abc}xyz0-9]$/v;
re.test('4️⃣'); // → true ✅
re.test('_'); // → true ✅
re.test('🇧🇪'); // → true ✅
re.test('abc'); // → true ✅
这种语法特别适合处理复杂的文本模式,如匹配所有常用的旗帜表情符号:
const reFlag = /[\p{RGI_Emoji_Flag_Sequence}\p{RGI_Emoji_Tag_Sequence}]/v;
reFlag.test('🇧🇪'); // → true ✅
reFlag.test('🏴'); // → true ✅
对 Web 开发者的意义
- 更精确的文本处理:这些新特性使得处理复杂的国际化文本、表情符号和特殊字符变得更加简单。
- 提高代码可读性:使用这些新语法可以使正则表达式更加清晰和易于理解,减少了使用复杂 Unicode 范围的需求。
- 性能优化:这些操作由正则表达式引擎直接处理,可能比手动实现更高效。
- 增强的国际化支持:更容易处理不同语言和文化中的特殊字符和符号。
- 简化复杂匹配逻辑:通过组合使用差集、交集和并集操作,可以创建非常精确的匹配模式。
改进的大小写不敏感匹配
ES2015 引入的 u flag在大小写不敏感匹配方面存在一些令人困惑的行为。让我们来看一个例子:
const re1 = /\p{Lowercase_Letter}/giu
const re2 = /[^\P{Lowercase_Letter}]/giu
第一个模式匹配所有小写字母。第二个模式使用 \P 而不是 \p 来匹配除小写字母以外的所有字符,然后被包裹在一个否定字符类 (^…) 中。两个正则表达式都通过设置 i flag(ignoreCase)来实现大小写不敏感。
直觉上,你可能会认为这两个正则表达式的行为应该是相同的。但实际上,它们的行为大不相同:
const re1 = /\p{Lowercase_Letter}/giu
const re2 = /[^\P{Lowercase_Letter}]/giu
const string = 'aAbBcC4#'
string.replaceAll(re1, 'X')
// → 'XXXXXX4#'
string.replaceAll(re2, 'X')
// → 'aAbBcC4#'
新引入的 v flag解决了这个问题,使行为更加符合预期。使用 v flag替代 u flag后,两种模式的行为变得一致:
const re1 = /\p{Lowercase_Letter}/giv;
const re2 = /[^\P{Lowercase_Letter}]/giv;
const string = 'aAbBcC4#';
string.replaceAll(re1, 'X');
// → 'XXXXXX4#'
string.replaceAll(re2, 'X');
// → 'XXXXXX4#'
- ^\p{X} 等价于 \P{X},也等价于 \P{X}
- ^\P{X} 等价于 \p{X},也等价于 \p{X}
对 Web 开发者的意义
- 更一致的行为:v flag使得正则表达式的行为更加一致和可预测,特别是在处理 Unicode 属性和大小写不敏感匹配时。
- 减少错误:这种一致性可以帮助开发者避免因正则表达式行为不一致而导致的潜在错误。
- 简化复杂匹配:开发者可以更自信地使用复杂的 Unicode 属性匹配,而不必担心意外的行为。
- 提高代码可读性:由于行为更加一致,使用这些模式的代码变得更容易理解和维护。
- 国际化支持:这个改进对于处理多语言文本特别有用,可以更可靠地进行大小写不敏感的匹配。
总结
v flag 是 Unicode 属性转义的重大改进,它不仅简化了正则表达式的编写,还提高了匹配的准确性和效率。几乎可以说是u flag的改进版,需要注意的是,由于这是 ES2024 的新特性,在使用时要考虑兼容性。