JavaScript 正则表达式匹配汉字
一个可能有 20 年历史的正则表达式
在谷歌搜索「JavaScript 正则表达式匹配汉字」的时候,前几条结果全都是/[\u4e00-\u9fa5]/
。没有人怀疑这个正则表达式有什么问题,那么在 2018 年的今天,让我们站在 Chrome 64 的肩膀上,放飞一下自我。
汉文(Han Script)是汉语、日本语、朝鲜语、韩国语的书写系统中的一种文字(Script),越南语在早期也曾在书写系统中使用汉文1。汉字(CJK Ideograph)是汉文的基本单元。汉字文化圈中的许多国家或地区都对汉字提出了自己的编码标准,Unicode 将这些标准加总在一起进行统一编码,力求实现原标准与 Unicode 编码之间的无损转换。Unicode 从语义(semantic)、抽象字形(abstract shape),具体字形(typeface)三个维度2出发,把不同编码标准里「起源相同、本义相同、形状一样或稍异」的汉字赋予相同编码,这些被编码的字符称为中日韩统一表意文字(下文我们提到的「汉字」,如果不加说明,均指代中日韩统一表意文字)。如果把它们全部列举出来写成正则表达式,那么就是技术上完整的匹配汉字的正则表达式了。
正则表达式/[\u4e00-\u9fa5]/
的意思是匹配所有从 U+4E00, cjk unified ideograph-4e00 到 U+9FA5, cjk unified ideograph-9fa5 的字符。这一段区域对应的是 Unicode 1.0.1 就收录进来的中日韩统一表意文字(CJK Unified Ideographs)区块,在 Unicode 3.0 加入表意文字扩展 A 区块以前,这个正则表达式确实给出了所有汉字的编码。换言之,从1992年到1999年,这个正则表达式确实是正确的。这么看来,想必这个表达式已经有20年历史了。
匹配所有统一表意文字
然而时光飞逝,Unicode 在2017年6月发布了10.0.0版本。在这20年间,Unicode 添加了许多汉字。比如 Unicode 8.0 添加的 109 号化学元素名称「鿏(⿰⻐麦)」,其码点是 9FCF,就不在这个正则表达式范围中。如果我们期望程序里的/[\u4e00-\u9fa5]/
可以与时俱进匹配最新的 Unicode 标准,显然是不现实的事情。因此,我们需要换一个思路,写一个无需维护的正则表达式:
1 | /\p{Unified_Ideograph}/u |
其中\u
是 ECMAScript 2015 定义的正则表达式标志,意味着将表达式作为 Unicode 码点序列。\p{}
是 ECMAScript 2018 定义的正则表达式 Unicode 属性转义,它向开发者提供了根据 Unicode 字符的属性数据3构造正则表达式的能力。Unified_Ideograph
是 Unicode
字符的一个二值属性,对于汉字,其取值为 Yes,否则为 No。因此\p{Unified_Ideograph}
匹配所有满足Unified_Ideograph=yes
的 Unicode 字符,而它的底层实现由运行时所依赖的 Unicode 版本决定,开发者不需要知道汉字的具体 Unicode 码点范围。
容易混淆的其他 Unicode 属性转义表达式
/\p{Ideographic}/u
这个表达式匹配所有满足Ideographic=yes
的 Unicode 字符。它是Unified_Ideograph=yes
的超集。我们先看一下 UAX #44 对这个属性的解释4:
Characters considered to be CJKV (Chinese, Japanese, Korean, and Vietnamese) or other siniform (Chinese writing-related) ideographs. This property roughly defines the class of “Chinese characters” and does not include characters of other logographic scripts such as Cuneiform or Egyptian Hieroglyphs.
这个属性表明该字符属于 CJKV 表意文字或者与汉语书写相关的其他表意文字(如西夏文、女书),这个属性粗略地定义了「中文字符」的分类。我们查看Unicode 10.0.0 字符属性列表可以知道,在 Unicode 10.0.0 中,
Ideographic 属性为 yes 的字符
3006 ; Ideographic # Lo IDEOGRAPHIC CLOSING MARK 3007 ; Ideographic # Nl IDEOGRAPHIC NUMBER ZERO 3021..3029 ; Ideographic # Nl [9] HANGZHOU NUMERAL ONE..HANGZHOU NUMERAL NINE 3038..303A ; Ideographic # Nl [3] HANGZHOU NUMERAL TEN..HANGZHOU NUMERAL THIRTY 3400..4DB5 ; Ideographic # Lo [6582] CJK UNIFIED IDEOGRAPH-3400..CJK UNIFIED IDEOGRAPH-4DB5 4E00..9FEA ; Ideographic # Lo [20971] CJK UNIFIED IDEOGRAPH-4E00..CJK UNIFIED IDEOGRAPH-9FEA F900..FA6D ; Ideographic # Lo [366] CJK COMPATIBILITY IDEOGRAPH-F900..CJK COMPATIBILITY IDEOGRAPH-FA6D FA70..FAD9 ; Ideographic # Lo [106] CJK COMPATIBILITY IDEOGRAPH-FA70..CJK COMPATIBILITY IDEOGRAPH-FAD9 17000..187EC ; Ideographic # Lo [6125] TANGUT IDEOGRAPH-17000..TANGUT IDEOGRAPH-187EC 18800..18AF2 ; Ideographic # Lo [755] TANGUT COMPONENT-001..TANGUT COMPONENT-755 1B170..1B2FB ; Ideographic # Lo [396] NUSHU CHARACTER-1B170..NUSHU CHARACTER-1B2FB 20000..2A6D6 ; Ideographic # Lo [42711] CJK UNIFIED IDEOGRAPH-20000..CJK UNIFIED IDEOGRAPH-2A6D6 2A700..2B734 ; Ideographic # Lo [4149] CJK UNIFIED IDEOGRAPH-2A700..CJK UNIFIED IDEOGRAPH-2B734 2B740..2B81D ; Ideographic # Lo [222] CJK UNIFIED IDEOGRAPH-2B740..CJK UNIFIED IDEOGRAPH-2B81D 2B820..2CEA1 ; Ideographic # Lo [5762] CJK UNIFIED IDEOGRAPH-2B820..CJK UNIFIED IDEOGRAPH-2CEA1 2CEB0..2EBE0 ; Ideographic # Lo [7473] CJK UNIFIED IDEOGRAPH-2CEB0..CJK UNIFIED IDEOGRAPH-2EBE0 2F800..2FA1D ; Ideographic # Lo [542] CJK COMPATIBILITY IDEOGRAPH-2F800..CJK COMPATIBILITY IDEOGRAPH-2FA1D # Total code points: 96174
囊括了所有统一表意文字、西夏文及其组件、女书、中日韩兼容性字符、苏州码子、「〇」以及日本语中的书信结尾标志「〆」。使用/\p{Ideographic}/u
来匹配汉字会过于宽泛。一是包含了西夏文、女书,二是只用于编码转换用的兼容字符也纳入其中。
/\p{Script=Han}/u
Script
属性5用来筛选满足下面条件的一组字符:
- 字符的书写形式具有共同的图像特征与文字流变
- 该组字符全部用来表达某个书写系统内的文本信息(textual information)
我们查看Unicode 10.0.0 Scripts可以知道,
满足Script=Han的字符
2E80..2E99 ; Han # So [26] CJK RADICAL REPEAT..CJK RADICAL RAP 2E9B..2EF3 ; Han # So [89] CJK RADICAL CHOKE..CJK RADICAL C-SIMPLIFIED TURTLE 2F00..2FD5 ; Han # So [214] KANGXI RADICAL ONE..KANGXI RADICAL FLUTE 3005 ; Han # Lm IDEOGRAPHIC ITERATION MARK 3007 ; Han # Nl IDEOGRAPHIC NUMBER ZERO 3021..3029 ; Han # Nl [9] HANGZHOU NUMERAL ONE..HANGZHOU NUMERAL NINE 3038..303A ; Han # Nl [3] HANGZHOU NUMERAL TEN..HANGZHOU NUMERAL THIRTY 303B ; Han # Lm VERTICAL IDEOGRAPHIC ITERATION MARK 3400..4DB5 ; Han # Lo [6582] CJK UNIFIED IDEOGRAPH-3400..CJK UNIFIED IDEOGRAPH-4DB5 4E00..9FEA ; Han # Lo [20971] CJK UNIFIED IDEOGRAPH-4E00..CJK UNIFIED IDEOGRAPH-9FEA F900..FA6D ; Han # Lo [366] CJK COMPATIBILITY IDEOGRAPH-F900..CJK COMPATIBILITY IDEOGRAPH-FA6D FA70..FAD9 ; Han # Lo [106] CJK COMPATIBILITY IDEOGRAPH-FA70..CJK COMPATIBILITY IDEOGRAPH-FAD9 20000..2A6D6 ; Han # Lo [42711] CJK UNIFIED IDEOGRAPH-20000..CJK UNIFIED IDEOGRAPH-2A6D6 2A700..2B734 ; Han # Lo [4149] CJK UNIFIED IDEOGRAPH-2A700..CJK UNIFIED IDEOGRAPH-2B734 2B740..2B81D ; Han # Lo [222] CJK UNIFIED IDEOGRAPH-2B740..CJK UNIFIED IDEOGRAPH-2B81D 2B820..2CEA1 ; Han # Lo [5762] CJK UNIFIED IDEOGRAPH-2B820..CJK UNIFIED IDEOGRAPH-2CEA1 2CEB0..2EBE0 ; Han # Lo [7473] CJK UNIFIED IDEOGRAPH-2CEB0..CJK UNIFIED IDEOGRAPH-2EBE0 2F800..2FA1D ; Han # Lo [542] CJK COMPATIBILITY IDEOGRAPH-2F800..CJK COMPATIBILITY IDEOGRAPH-2FA1D# Total code points: 89228
囊括了所有统一表意文字、中日韩兼容性字符、苏州码子、「〇」、「〆」、「々」以及字典常用的部首。从前面汉文(Han Script)与汉字(CJK Ideograph)的关系我们可以知道,/\p{Script=Han}/u
匹配的是汉文作为一个字符集里面的所有字符,因此它包括了部首、「々」等字符,这些字符要么当它们独立存在的时候没有语言意义(部首独立存在是一个符号),要么无法独立存在(「々」依赖于所修饰的汉字)。所以汉字是汉文的一个单元,汉文除了包含汉字以外,还包括这些部首符号、数字、修饰符。因此使用/\p{Script=Han}/u
来匹配汉字混淆了汉文与汉字的概念范围。
浏览器兼容性支持
JavaScript
截至2019年10月,Chrome 64 以上, Safari 11.1 以上都支持 正则表达式 Unicode 属性转义。对于其他浏览器,我们推荐使用 7.7 版本的@babel/env
转译配置将带有属性转义的正则表达式转为 Unicode 码点正则表达式。
1 | { |
就可以根据browserslist
指定的浏览器版本自动决定是否进行此特性的转译。注意 Babel 7.3 - 7.6 的版本对此特性支持产生了问题,该问题已经在PR 10447得到解决,将随 Babel 7.7.0 一同发布。
如果您还在使用更老一点的 Babel 7.x 版本,需要用babel
转译插件@babel/plugin-proposal-unicode-property-regex。@babel/env
背后也使用了这个插件。
上述转译结果的在线演示可以在这里查看,开发者可以自己在上面转译其他的 Unicode 属性转义正则表达式。我们在这里列举/\p{Unified_Ideograph}/u
转译成Unicode 码点正则表达式的结果:
1 | const regex = /\p{Unified_Ideograph}/u; |
从上面这个正则表达式可以知道,转译的结果跟
Unicode 10.0.0 中 Unified_Ideograph 属性为 yes 的字符
3400..4DB5 ; Unified_Ideograph # Lo [6582] CJK UNIFIED IDEOGRAPH-3400..CJK UNIFIED IDEOGRAPH-4DB5 4E00..9FEA ; Unified_Ideograph # Lo [20971] CJK UNIFIED IDEOGRAPH-4E00..CJK UNIFIED IDEOGRAPH-9FEA FA0E..FA0F ; Unified_Ideograph # Lo [2] CJK COMPATIBILITY IDEOGRAPH-FA0E..CJK COMPATIBILITY IDEOGRAPH-FA0F FA11 ; Unified_Ideograph # Lo CJK COMPATIBILITY IDEOGRAPH-FA11 FA13..FA14 ; Unified_Ideograph # Lo [2] CJK COMPATIBILITY IDEOGRAPH-FA13..CJK COMPATIBILITY IDEOGRAPH-FA14 FA1F ; Unified_Ideograph # Lo CJK COMPATIBILITY IDEOGRAPH-FA1F FA21 ; Unified_Ideograph # Lo CJK COMPATIBILITY IDEOGRAPH-FA21 FA23..FA24 ; Unified_Ideograph # Lo [2] CJK COMPATIBILITY IDEOGRAPH-FA23..CJK COMPATIBILITY IDEOGRAPH-FA24 FA27..FA29 ; Unified_Ideograph # Lo [3] CJK COMPATIBILITY IDEOGRAPH-FA27..CJK COMPATIBILITY IDEOGRAPH-FA29 20000..2A6D6 ; Unified_Ideograph # Lo [42711] CJK UNIFIED IDEOGRAPH-20000..CJK UNIFIED IDEOGRAPH-2A6D6 2A700..2B734 ; Unified_Ideograph # Lo [4149] CJK UNIFIED IDEOGRAPH-2A700..CJK UNIFIED IDEOGRAPH-2B734 2B740..2B81D ; Unified_Ideograph # Lo [222] CJK UNIFIED IDEOGRAPH-2B740..CJK UNIFIED IDEOGRAPH-2B81D 2B820..2CEA1 ; Unified_Ideograph # Lo [5762] CJK UNIFIED IDEOGRAPH-2B820..CJK UNIFIED IDEOGRAPH-2CEA1 2CEB0..2EBE0 ; Unified_Ideograph # Lo [7473] CJK UNIFIED IDEOGRAPH-2CEB0..CJK UNIFIED IDEOGRAPH-2EBE0 # Total code points: 87882
使用
1 | { |
配置将表达式转成 ES5 支持的以字符的 UTF16 表示为序列的字符串,这里不再赘述。
input
元素的 pattern
属性
在前端技术中,除了JavaScript会用到正则表达式,HTML 里<input>
元素的pattern
属性也会用到正则表达式。与 JavaScript 相比,pattern
不支持设置正则表达式的标志位,因此 HTML 标准中强制规定了 input
元素的 pattern
属性需要施加unicode
标志6。目前除了 IE 和 Edge 18 以外的主流浏览器都已经实现了这一标准。
在 React/Angular/Vue.js 三大前端框架中,Angular 提供了近似于 pattern
的指令 ngPattern
。目前ngPattern
尚未施加unicode
标志7。AngularJS 的 ngPattern
directive 仍未施加。
在大部分情况,是否施加unicode
标志不会对正则表达式产生语义区别。主要的差别在于:
- 在使用
\u{10000}
表示 Unicode 码点字符情形,正则表达式/\u{10000}/
代表匹配u
一万次,/\u{10000}/u
匹配字符\u{10000}
一次 /./
只匹配 BMP 平面的字符,/./u
匹配所有平面的字符。
由于 Unicode 属性转义正则表达式需要 Chrome 64 以上,Safari 11.1 以上,因此下面的用法目前只能在这些浏览器中使用:
1 | <input type="text" pattern="\p{Unified_Ideograph}"> |
因此,如果需要兼容其他浏览器,可以使用转译插件的底层库regexpu-core在 JavaScript 中转换正则表达式,再把转换结果通过 HTML 模版输送过去中,但无疑这是一个很麻烦的做法。
1 | const rewritePattern = require("regexpu-core"); |
总结
/[\u4e00-\u9fa5]/
已经过时了,请不要再使用二十年前的正则表达式了。/\p{Unified_Ideograph}/u
不需要维护,匹配所有汉字。这里\p
是 Unicode 属性转义正则表达式。/\p{Ideographic}/u
和/\p{Script=Han}/u
都是/\p{Unified_Ideograph}/u
的超集,匹配了除了汉字以外的其他一些字符,在「汉字匹配正则表达式」这个需求下,是错的。- 目前 Chrome 和 Safari 支持 Unicode 属性转义正则表达式。对其他环境,使用 7.7 版本的
@babel/env
就可以自动根据浏览器规定打开支持。
2: Unicode 10.0.0 第十八章第一节,东亚 ↩
3: Unicode 10.0.0 字符属性列表 ↩
4: UAX #44 第 20 版的属性说明 ↩
5: UAX #24 第 27 版 ↩
6: HTML 标准中
input
元素的pattern
属性 ↩
7: 给
ngPattern
施加unicode
标志 ↩