
Unicode1、基础知识2、关于编码3、尽量使用Unicode编码4、Unicode与字符组简记法5、规范化问题6、单词边界7、码值转义序列8、Unicode属性8.1、Unicode Property8.2、Unicode Block8.3、Unicode Script9、Unicode属性列表9.1、Unicode Property9.2、Unicode Block9.3、Unicode Script10、POSIX字符组11、Emoji1、基础知识在Unicode出现之前各种系统的文字编码是各不相同的比如中国大陆使用的是GB2312中国台湾使用的是Big5美国使用的是ASCIIISO-8859-1日本使用的是Shift JIS等。这些编码系统的作用都是给每个字符分配一个数字码值作为编码然后进行码值-字符和字符-码值的双向映射操作。比如在ASCII编码中字母C的编码是十进制的67又比如在GB2312编码中汉字“正”的编码是十进制的54781。这样的标准在本地区使用当然没有问题但如果要跨地区交流就会遇到困难因为同样的码值在不同的编码体系下对应的字是不同的。如果同一篇文档里要显示不同编码的文字那更是不可能完成的任务。Unicode的发明就是为了解决这些问题。Unicode由两大部分组成一部分是UCS也就是Universal Character Set它定义了一个广阔的空间能把各种不同语言中的字符全部装进去为每个字符分配唯一的码值Unicode中的术语是Code Point。另一部分是UTF也就是Unicode Transformation Format它定义了UCS中的每个码值以什么样的方式来传输和存储。我们通常谈论的“Unicode编码”严格来说是指UCS也可以叫“字符集”它关心每个字符对应的码值是多少比如中文字符“正”的码值是十进制的27491十六进制的表示是6B63。讨论Unicode时常用的表示法是U6B63。在本章讨论十六进制表示的码值都会采用U6B63这样的写法讨论十进制表示的码值时使用27491这样的写法。码值只是概念要变成字节才能够读取、存储、传输所以要用到UTF。如果采用如今常用的UTF-8编码它需要用3字节E6 AD A3Web相关开发里经常会需要对参数进行URL Encode如果你留意观察会发现“正”字进行URL Encode之后就是%E6%AD%A3。其他UTF还有UTF-16、UTF-32、UCS-2、UCS-4等它们之间的关系如下图所示。为了能够在全世界通行UCS空间设计得很大码值从0到1114111一共可以容纳超过100万个字符。按照2017年6月最新发布的Unicode 10.0标准28001625%已经分配完毕其中13675512%分配给了字符13746812.3%分配给了私有用途2048分配给了代用码surrogates66分配给了非字符用途。剩下83409675%尚未分配。整个UCS又可以分为17个“平面”Planes每个平面包含65536个码值二者的乘积正好是1114112。这17个平面的编号从0到16其中编号为0的平面称为“基本多语言平面Basic Multilingual Plane,BMP”对应的码值从0到65535日常用到的绝大多数文字字符都包含在这个平面里。除了BMP之外其他平面称为“补充平面Supplementary Plane”。因为日常用到的字符基本都包含在BMP中而BMP的空间大小是65536是2的16次方正好可以用2字节对应所以早年的许多设计就以此为基础后来产生了很多麻烦。因为假设“所有的字符都可以用2字节对应”所以早期出现的一种Unicode编码格式是UCS-2它用2字节表示所有字符。要注意的是它的名字里虽然有UCS其实和UTF-8一样是一种传输编码。最开始UCS-2确实工作得不错但随着技术的发展大家很快发现BMP不够用了需要启用新的平面而UCS-2因为先天限制不可能有空间表示更多的字符了。于是UTF-16诞生了。UTF-16的设计思想非常巧妙。如果要表示的字符在BMP内直接照搬UCS-2就可以。如果不在BMP内也就是码值超过了65536则利用BMP中预留的UD800到UDFFF这2048个代用码本身是没有意义的来解决。这2048个代用码分为两段UD800到UDBFF和UDC00到UDFFF。对任何一个码值只要其第一个字节在D8到DF之间它一定不是BMP中原有的字符所以可以放心用来当替代品表示其他字符。对于BMP之外的字符UTF-16定义的转换规则如下第一步将码值减去0x010000得到一个20位的数值。第二步将这个数值的高10位加上0xD800得到的码值作为高位代用码High Surrogate。第三步将这个数值的低10位加上0xDC00得到的码值作为低位代用码Low Surrogate。第四步将高低两段代用码组合起来得到4字节序列就是该字符的UTF-16编码值。UTF-16转换举例见下表。也就是说UCS-2是UTF-16的子集在UTF-16编码格式下用于表示一个字符的字节数可能是变化的或者是2个字符或者是4个字符。看起来UTF-16确实解决了空间不够的问题但它也有问题本来在ASCII编码中英文字符只占用1字节使用UTF-16之后则必须占用2字节所消耗的空间瞬间增长了一倍。在存储空间还很宝贵的时代这无疑是不小的负担。所以UTF-8编码才能够大行其道。与UTF-16类似UTF-8编码也是一种变长传输编码。与UTF-16不同的是它的编码单位不是2字节而是1字节也就是说UTF-8编码的字符最少只用1字节。这样不但兼容原本就是单字节的ASCII编码存储英文资料时也节省了大量的空间。UTF-8的转换规则及举例见下表。因为大部分中文字符都落在0800到FFFF这个区间所以在UTF-8编码格式下一个中文字符要占用3字节。而在UTF-16编码格式下一个中文字符只占用2字节看起来节省了1字节但UTF-8凭借在传输英文资料时节省大量空间、直接与ASCII兼容等优势赢得了越来越多的开发者。如今除了少数系统内部使用UTF-16大部分场合UTF-8都已经成了默认传输标准。需要再次强调无论UTF-8还是UTF-16都只是传输编码。如果我们需要在代码中处理Unicode字符一定是指定它原始的码值而不是传输编码。比如“正”字在各种编程语言中我们都是通过U6B63这个码值来指定它而不需要考虑它在UTF-8或者UTF-16下的实际编码值。可以做一个不那么严谨的类比码值就像身份证号UTF-8、UTF-16、UCS-2等传输编码就像指纹识别、声纹识别、人脸识别等方式无论采用什么识别方式最终定位的人身份证号码是不变的。所以无论采用什么传输编码无论一个中文字符要占用几字节我们总可以用[\u4E00-\u9FFF]匹配中文字符。刚制订Unicode规范的时候大家乐观地认为BMP的空间已经足够所以诞生了许多乐观的约定。比如在许多语言中都可以用\uxxxx的方式来指定Unicode字符其中xxxx是4位十六进制数。当时这样规定确实没有问题但随着Unicode分配的字符不断增加4位十六进制数的表示法就不够用了。如果直接增加位数可能会产生二义性问题比如\u10437到底是表示码值为10437的字符呢还是码值为1043的字符再加7这个字符呢所以JavaScript的ECMAScript 2015引入了新的转义序列用\u{xxxx}取代了\uxxxx这样\u20AC可以写作\u{20AC}\u10437则写作\u{10437}不会产生歧义。PHP也是如此在PHP 7里引入了\u{xxxx}的转义序列。而Java程序员就没有那么好的运气了不能直接在String里面用\uxxxx的转义序列只能调用Character.toChars(10437)来指定字符。C#程序员稍微幸运一些以前的\xn[n][n][n]和\uxxxx转义序列都只能支持4位但\U转义序列支持8位所以可以写作\U00010437虽然长了一些毕竟不算太麻烦。在Objective-C中也是这样处理。2、关于编码通常英文编码较为统一都采用ASCII编码或可以兼容ASCII编码即编码表的前127位与ASCII编码一致常见的各种编码包括Unicode编码都是如此。也就是说英文字母、阿拉伯数字、英文的各种符号在不同编码下的码值基本是一样的比如字母A其码值总是41中文的情况则不同常见的中文编码有GB18030也就是CP54936主要是在Windows平台下使用。早期是GBK也就是CP936如今采用的GB18030与GBK是兼容的考虑到大家习惯说“GBK编码”下文也沿用“GBK编码”的说法和Unicode主要用于Linux/UNIX、Mac OS两种同一个中文字符在两种编码下的码值并不相同。比如“正”在GBK编码下的码值为D5FD而在Unicode下的码值为6B63。为方便下面的讲解这里先约定两种提法ASCII字符即ASCII编码表中的字符也就是码值在0127之间的字符不包括扩展ASCII字符每个字符用1字节表示。常见的英文字符和半角标点符号都属于ASCII字符。非ASCII字符即ASCII编码表之外的字符在本书中指多字节字符。中文字符属于“非ASCII字符”它们在GBK编码中一般占用2字节在UTF-8编码中占用3字节。当然“非ASCII字符”包含的字符很多不只有中文。3、尽量使用Unicode编码常见的正则表达式的文档都是关于英文ASCII字符的英文开发者通常也只需要处理ASCII字符不需要处理中文这类多字节的字符。不过依照处理ASCII字符的方式处理中文字符就有可能出错。GBK实际是GB18030是Windows环境下默认的中文编码环境也可以笼统地说Windows下的默认编码就是GBK在大多数场合使用也一切正常。如果我们希望匹配“正”GBK码值D5FD或者“则”GBK码值是D4F2很自然会想到使用字符组[正则]。这个思路没有问题但结果是否和我们想象的一样呢假设字符串是“遭遇”这个正则表达式应当是不能匹配的。那么来看看下例的运行结果吧。GBK编码环境下的字符组会出现错误匹配为什么能够匹配结果是单个字节原因如下虽然采用了GBK编码每个中文字符用2字节表示但是正则表达式处理时识别不出这是“2个多字节字符构成的字符组”而将其视为“4个单字节字符构成的字符组”4字节正好是“正”和“则”对应的D5 FD和D4 F2。上面代码中的213、253、212、242正是用十进制标识的这4个字节。如果不按字符来进行匹配而是按照字节来进行恰好都有D4这个字节所以匹配结果就是D4这个字节。下图说明了其中的道理。如果在Unicode环境下运行是否解决了这个问题呢再看看下例的运行结果吧。Unicode编码环境下的字符组会出现错误匹配问题出在哪里呢原来在Python 2下只要没有显式指定字符串为Unicode字符串即便代码文件使用的是Unicode格式比如UTF-8仍然会按照单个字节的方式来处理如下图所示。如果我们把正则表达式和字符串都显式指定为Unicode模式就可以彻底解决问题了。在Python中显式指定编码的做法是在字符串开头的引号之前写上u字符。请注意如果同时用r指定了原生字符串那么一定要写作ur而不是ru否则会报错。示例参见下例图示参见下图。彻底消灭错误匹配如果因为某些限制只能使用GBK编码有一个“凑合能用”的办法能准确保证[正则]的匹配就是把字符组[正则]改成多选结构(正|则)。从下例可以看到此时如果要匹配成功只能是两个连续的字节D5 FD或者D4 F2这样就避免了错误匹配。GBK编码的字符组应改为多选结构避免错误匹配使用Unicode编码的好处还有很多比如避免点号的错误匹配。许多文档说点号.可以匹配“除换行符\n之外的任意字符”但这可能只适用于单字节字符因为点号匹配的其实只是“除换行符\n之外的任意字节”而已。之所以会出现这种情况是因为正则表达式没有理解这几个字节表示的是字符“正”仍然把它们当作互相独立的字节既然点号.只能匹配1个字符结果当然不能正确匹配。要解决这个问题必须显式指定编码让正则表达式处理程序正确识别多字节字符。总的来说如果使用的是Python 2、Ruby 1.8或PHP都需要显式指定Unicode。而在Python 3、Java、.NET和Ruby 1.9中字符串默认采用Unicode编码所以不存在上面的问题。PHP和Ruby 1.8中并不存在“Unicode字符串”所以无法修改字符串的属性来指定Unicode编码。不过如果将正则表达式指定为Unicode模式正则表达式处理程序就可以正确识别字符串中的Unicode字符。所以如果用PHP或Ruby的正则表达式处理Unicode字符串一定不要忘记指定Unicode模式。下表总结了常用语言中点号对多字节字符的匹配情况。现在来解释虽然GBK编码也是多字节编码为什么不推荐使用GBK编码作为常见的中文编码使用确实很多尤其是在Windows平台上但在GBK编码环境下使用正则表达式却可能遇到非常奇怪的现象因为正则表达式处理程序一般只能准确识别Unicode字符。总的来说为避免出现错误匹配只有一条原则如果使用正则表达式处理含有多字节字符的文本能使用Unicode编码环境就尽量使用。不过即便你使用了支持Unicode编码的字符串还可能遇到各种诡异的问题。常见的例子是量词的作用问题。比如正则表达式ab{4}我们非常清楚量词{4}作用的对象是字符b。如果把正则表达式改为正{4}问题就出现了。这时候量词作用的对象到底是3个字节组合而成的“正”呢还是“正”的最后一个字节呢很长的时间里JavaScript就大受这种问题困扰所以在ES2015也有人称为ES6中专门为正则表达式提供了Unicode模式解决了这个问题。另一个常见的问题是字符串内部的偏移值的计算单位。假如你面对的字符串是“零壹贰34伍陆柒”正则表达式是[0-9]匹配的结果当然是子字符串“34”但是它在原来字符串中的起始偏移值是多少单从字符来看应当是3因为前面有三个字符“零壹贰”。然而正确的答案是它取决于具体的编程语言。在Java和C#这样字符串原本就支持Unicode的语言中起始偏移值确实是3在Python 2等语言中如果字符串没有被指定为Unicode字符串则起始偏移值是9因为前面三个都是中文字符每个中文字符在UTF-8编码格式中需要用3个字符。或许会让你费解的是Golang虽然它的字符串也是原本就支持Unicode的但偏移值却是按照字节来算的所以起始偏移值也是9。虽然大多数时候我们并不会关心偏移值的具体数字只会用代码算出偏移值再用代码来根据偏移值进行处理但这个细节还是有必要知道的。4、Unicode与字符组简记法上一节提到Python文档说明除了指定字符串为Unicode字符串还可以指定正则表达式使用Unicode模式这又是怎么回事呢不妨回头仔细想想读过的文档正则表达式中的\d和\w都是如何解释的许多人的第一反应是\d等价于[0-9]\w等价于[0-9a-zA-Z_]因为有些文档说明了这种等价关系。不过也有些文档说\d匹配数字字符\w匹配单词字符\s匹配空白字符但是没有说明数字字符、单词字符、空白字符到底是哪些字符。一般来说数字字符就是[0-9]单词字符就是[0-9a-zA-Z_]空白字符则包括空格、回车等字符但这只是ASCII编码中的情况在Unicode编码中并非如此。因为涵盖了多种语言和字符所以在Unicode编码中全角数字0、1、2之类也算作“数字字符”可以由\d匹配中文字符也可以算作“单词字符”由\w匹配同样的道理中文的全角空格码值为30FF也可以算作“空白字符”由\s匹配。所以如果在Python 2中指定了正则表达式使用Unicode模式最简单的方式就是在正则表达式的开头指定模式修饰符(?u)\d、\w、\s就能匹配全角数字、中文字符、全角空格。对于这种情况本书中称为Unicode匹配规则相应的之前ASCII编码中的匹配称为ASCII匹配规则。下例所示的是排除型字符组的错误匹配。排除型字符组的错误匹配下表详细介绍了两种匹配规则下\d、\w、\s的匹配。相应的此时\D匹配\d不能匹配的字符\W匹配\w不能匹配的字符\s匹配\S不能匹配的字符。\p{L}表示任意语言中的字母字符包括英文字母和汉字。\p{M}表示用来与其他字符结合的字符声调、元音变化符等。\p{Nd}表示任何书写系统中的09的字符汉字全角字符1、2等也算。\p{Nl}表示形如字符的数字比如罗马数字。\p{Pc}表示类似下画线之类的标点字符。\p{InEnclosedAlphanumerics}表示被包围的数字或字符。\p{So}表示数字符号、货币符号、组合字符之外的符号字符。有时候这样的规定确实让人抓狂假设你希望用正则表达式\d{6,12}来验证一个长度在6到12之间的数字字符串却没留意\d能匹配全角数字验证就可能出错所以一定要注意此类问题下表总结了常用语言中的匹配规则。5、规范化问题Unicode规范中专门辟出篇幅来谈“规范化等价关系”Canonical Equivalence这是一个重要的概念也会影响正则表达式的匹配所以值得单独谈一谈。所谓“规范化”按照维基百科的定义指的是这样一个过程将有多种表现形式的数据转换到“标准”的表现形式。如果这个解释不好理解不妨来看一个例子K。它既可以是ASCII字符集中的字母K码值U0075也可以是绝对温度单位开尔文Kelvin的标志码值U212A。单纯从码值上来看两者完全不相同但如果在文本里按形状来辨识是很难准确区分的。无论我们阅读还是写作如果要用到开尔文这个单位我们的第一反应还是K。字符仍然是这个形状只是在不同的语境中它被赋予了不同的含义。“规范化”为开尔文标志和字母K定义了统一的表现形式两者是“规范化等价关系”。有了规范化等价关系处理Unicode文本时就可以直接按照“看起来像”的样子来编写和使用正则表达式而不必费心去猜测某个字符到底是什么码值。在正则表达式中一般默认不会开启对规范化等价关系的支持需要显式指定见例7-6。比如在JavaScript中在同时指定了Unicode匹配规则和不区分大小写匹配模式的情况下会开启对规范化等价关系的支持见下例。JavaScript中的规范化等价关系而在Java中是通过指定常量Pattern.CANON_EQ来开启的虽然按照JDK的文档这么做会严重降低性能见下例。Java中的规范化等价关系6、单词边界单词边界之前已经介绍过它的准确解释是一端必须出现\w能匹配的字符另一端不出现\w能匹配的字符。[8]在JavaScript、PHP、Python 2、Ruby中\w只能匹配[0-9a-zA-Z_]。所以在这些语言中\b\w\b能用来匹配几乎所有的英文单词在下例中用它来提取一段文本中的所有单词。用单词边界提取单词之所以说“几乎所有”的英文单词是因为有些情况更加复杂假设字符串中包含中文之间用空格字符分隔比如“他们 我们去见他们的时候……”。如果希望匹配单独出现的“他们”最自然的想法似乎是使用正则表达式\b他们\b结果却是不对的如下例所示。用单词边界提取中文单词会出错原因在于默认情况下\w是按照ASCII匹配规则的。“他”字之前的位置左侧没有字符不能由\w匹配右侧的“他”字也不能由\w匹配“们”字右侧的情况也是如此。所以\b他们\b无法匹配“他们”。要解决这个问题可以采用Unicode匹配规则此时\w就不只能匹配[0-9a-zA-Z_]还能匹配其他语言中的单词字符包括中文字符程序代码如下例所示。在Unicode模式下提取中文单词但是这样又出现了新的问题。比如字符串“学习regex”其中的regex在非Unicode匹配规则下是可以由\bregex\b匹配的在Unicode匹配规则下却行不通因为此时\w既能匹配中文字符又能匹配英文字符\bregex\b反而无法匹配regex了代码如下例所示。在Unicode模式下提取单词仍然有问题为了形象说明在ASCII匹配规则和Unicode匹配规则下\b能够匹配的“单词边界”因为通常处理的文本包含中文和英文所以只是简要列出了各分类的常用字符下面用图分别说明。其中深色区域是必须出现的而浅色的区域是可以出现也可以不出现的比如文本首尾位置的空字符串。ASCII匹配规则下的单词边界Unicode匹配规则下的单词边界下表举列列出了在Unicode匹配模式下\b匹配的几种典型的场景。总的来说如果使用Unicode匹配规则尽量不要在处理中英文混排文本时使用\b。如果使用ASCII匹配规则则可以在处理英文文本时放心地使用\b。也有更复杂的情况比如Java就是如此。在Java中虽然\w只能匹配[0-9a-zA-Z_]\b对“单词字符”的判断却是按照Unicode匹配规则的。7、码值转义序列Unicode字符多种多样除去ASCII中的字母、数字、标点和中文字符还包括其他多种语言和多种符号有些符号甚至很难打出来比如表示商标注册的™这时候该如何表示呢再说远一点如果我们想用一个字符组匹配所有的中文字符能不能像[a-z]那样呢使用正则表达式解决这类问题必须依赖码值。前面讲过每一个Unicode字符都有一个Unicode码值所以在正则表达式中的Unicode字符往往采用Unicode码值来指定。一般来说指定码值的形式有两种\uxxxx和\u{xxxx}其中的xxxx为编码的值\u之后必须有4位十六进制数字。.NET、Java、JavaScript、Objective-C和Python使用前一种形式而PHP和Ruby使用后一种形式PHP使用的字母是x而不是u\x{xxxx}。比如“正则”的“正”字对应的Unicode编码是U6B63所以可以在.NET、Java、JavaScript的正则表达式中用\u6B63表示它Python稍有不同必须使用u\u6B63在Ruby中必须写作\u{6B63}在PHP中则写作\x{6B63}。下表总结了常用语言中用码值表示Unicode字符的方法。通过前面的介绍我们知道我们用到的Unicode字符不限于BMP所以码值可能大于UFFFF也就是说4位十六进制数是不够的。如果采用\uxxxx表示法通常就需要另想办法来表示此字符否则会有二义性问题如果采用\u{xxxx}表示法就没有这种问题。既然可以这样指定Unicode字符自然也可以在字符组中用范围表示法指定Unicode编码范围。这个功能最常见的应用就是匹配任意一个中文字符。查询Unicode编码表可知中文字符的码值大多位于4E00到9FFF之间所以可以用字符组匹配中文字符Unicode编码4E009FFF归类为CJK 统一表意符号CJK UnifiedIdeographs涵盖了绝大多数中文字符。下表列出了常用语言中匹配中文字符的字符组。如果实在没有办法使用Unicode编码环境而只能采用GBK编码在Python、Ruby 1.8和PHP中也有一个办法匹配所有中文字符查阅GBK编码表可知中文字符的GBK码值从B0 00开始到FE A0结束但是在通常情 况下正则引擎无法正确识别非Unicode编码字符串的字符边界所以不能写作[\xb000-\xfea0]只能写作[\xb0-\xfe][\x00-\xff]它匹配两个字节第一个字节的码值范围从B0到FE第二个字节的码值范围从00到FF如果要匹配多个中文字符必须添加括号将这两个字节分为一组再使用量词比如([\xb0-\xfe][\x00-\xff])。因为此时完全是将字符串作为单字节序列来对待的所以也不应该指定任何与Unicode有关的设置无论是正则表达式还是字符串都是如此。8、Unicode属性每一个Unicode字符除了有Code Point与之对应外还具有其他属性在正则表达式中常用到3种Unicode属性Unicode Property、Unicode Block、UnicodeScript分别对应字符的功能、所属代码区段、书写系 统它们的表现形式类似\p{property}。下图比较形象地说明了这3种属性下面详细进行介绍。8.1、Unicode PropertyUnicode Property的记法类似\p{L}、\p{P}。它按照字符的功能分类Unicode字符每个Unicode字符只能属于一个Unicode Property。可以这样理解Unicode Property它并不关心字符所属的语言只关心字符的功能比如\p{Z}表示任意的空白字符或不可见的分隔符\p{P}表示任何标点字符等等。遇到中英文混排、全角、半角字符同时出现的情况就可以用\p{Z}匹配所有的空白字符而不用关心空格到底是全角空格还是半角空格用\p{P}匹配所有的标点字符而不用关心逗号到底是中文逗号还是英文逗号。可以说有了Unicode Property各种字符“第一次”可以按功能或者意义来分类不再是一堆零散的码值。正因为如此大多数语言中的正则表达式如果不是支持所有的Unicode属性最起码也会支持Unicode Property。如果把Unicode Property理解为一个“字符组”那么一定还有对应的排除型字符组此排除型字符组的通行记法是将\p{property}中的小写p改为大写P写作\P{property}。这样\P{Z}对应\p{Z}无法匹配的字符\P{P}对应\p{P}无法匹配的字符。Unicode Block和Unicode Script对应的排除型字符组也是这样标记下面不再赘述。支持Unicode Property的语言有.NET、Java、Objective-C、JavaScriptES2017、PHP、Ruby限1.9以上版本、Golang在PHP和Ruby中使用Unicode Property时必须要开启Unicode模式。下面的代码展示了在几种常用语言中\p{P}如何匹配一个全角逗号。8.2、Unicode BlockUnicode Block不同于Unicode Property它按照编码区间划分Unicode字符各个区间彼此联系但互不相交所以每个Unicode 编码中的每个字符都有唯一归属的Unicode Block。在Unicode编码表中同一种语言的字符通常是落在同一区间的所以Unicode Block也可以粗略表示某类语言的字符比如\p{InHebrew}表示希伯来语字符\p{InCJK_Unified_Ideographs}表示兼容CJK中文、日文、韩文统一表意字符。如果读者细心阅读文档会发现Unicode Block的名字虽然类似某种语言的名字但都有InJava风格或者Is.NET风格前缀所以它对应的还是“落在某个区间的Unicode字符”。在本书介绍的语言中Java、.NET、Golang支持Unicode Block但是记法不相同下面以CJK统一表意字符为例虽然记法不同但功能是相同的都可以匹配任意一个中文字符。另外值得一用的是\p{InCJK_Symbols_and_Punctuation}从命名上看它覆盖了中文、日文、韩文中的大部分常见标点但事实并非如此比如全角逗号就不在这个Block中。不过所有标点都在\p{P}这个Unicode Property中代码如下例所示。全角逗号不属于\p{InCJK_Symbols_and_Punctuation}8.3、Unicode ScriptUnicode Script按照字符所属的书写系统来划分Unicode字符比如\p{Greek}表示希腊语字符\p{Han}表示汉语中文字符。它的写法类似Unicode Block只是名字的开头没有Is或者In。PHP、Ruby限1.9以上版本、Objective-C[11]支持Unicode ScriptPHP在使用Unicode Script时必须开启Unicode模式详见PHP的章节。在这几种语言中我们可以很方便地用\p{Han}来匹配中文字符。9、Unicode属性列表9.1、Unicode Property每个Unicode字符都只能属于一个Unicode Property。BMP中所有的Unicode Property共分为7大类30小类。大类的名字只有单个字母小类的名字则包含多个字母开头字母与所在大类的名字相同小类包含的字符都属于它所在的大类。下表列出了所有的UnicodeProperty。9.2、Unicode Block每个Unicode Block都对应一个连续的Unicode码值区间BMP中的字符一共划分为105个Block。使用时应该注意Java使用的Unicode Block是\p{In…}形式的[12]比如InCJK_Unified_Ideographs而.NET使用的Unicode Block是\p{Is…}形式的同时不会包含下画线[13]比如IsCJKUnifiedIdeographs。下表列出了所有的Unicode Block但没有包含Is或者In的前缀使用时请自行添加。9.3、Unicode Script除去没有赋值的Unicode码值之外每个Unicode码值都归属某个Unicode Script它一般对应某种语言。下表列出了所有的Unicode Script。10、POSIX字符组我们已经介绍过POSIX字符组并且提到根据locale的不同这些字符组简记法能匹配的字符也不相同下表介绍了Unicode编码中这些简记法能匹配的字符。Java、PHP、Ruby支持使用POSIX字符组。但是只有Ruby 1.9的POSIX字符组是完全支持Unicode字符的并且不需要显式指定Unicode模式。在Java和PHP中POSIX字符组只能匹配ASCII字符。11、EmojiEmoji“绘文字”已经不再局限在日本流行而是获得世界各国人民广泛使用。Unicode 6.0及以上版本已经收录了Emoji表情并且随着Unicode规范的升级Emoji表情也在不断更新。到本章写作时为止最新的版本是2017年5月18日定稿发布的Emoji 5.0其中收录了2666个Emoji。要注意的是Unicode规范中定义的Emoji并不都是新添加的一些老的字符也会被算作Emoji比如ASCII编码中的#也在官方的Emoji列表中。如果希望获得最全的Emoji列表可以访问这个地址http://www.unicode.org/Public/emoji/5.0/emoji-data.txt。与常见字符不同的是关于EmojiUnicode规范中只定义了Emoji的意义没有规定它的具体形态所以“同样”的Emoji在不同的平台iOS、Android、微信等上的表现会有细微的差别。Emoji与常见字符相区别的另一点是许多Emoji都不在BMP内码值超过FFFF所以\uxxxx的表示法无能为力\u{xxxx}则没有这个问题。同样因为其码值超过FFFF所以在UTF-8编码格式下需要用4字节—既不同于ASCII字符的1字节也不同于中文的3字节。处理Emoji的思路和处理传统文本的思路有很大区别。第一Emoji是不可读的没有读音可言第二Emoji是表意的只能从图像的角度来理解第三许多时候Emoji对程序代码而言是不可见的只能用\u{xxxx}的方式来表达仅仅阅读源代码并不知道这个字符到底是什么第四传统的Unicode系统未必能存储Emoji典型的例子是MySQL必须将编码设定为UTF8MB4才可以接收包含Emoji的文本“传统上”用的UTF-8会报错。那么正则表达式处理Emoji有什么好办法吗网络上有一些开源的工具其原理大都是找出Emoji所在的码值区间然后用正则表达式的字符组来匹配。普通用途应当没有问题但这些工具的作者对Emoji的理解深浅不一Emoji也在不断增加用起来还是应该谨慎。正则表达式擅长的是处理文本而不是图像虽然[\u{1f600}-\u{1f64f}]这样的正则表达式是能运行的可以匹配主要的Emoji表情但这种正则表达式基本不可读。而想用一个正则表达式找到全部Emoji的办法也不可行Emoji分散在BMP和各个补充平面中也不能归一到某个Unicode Block或是Unicode Property。虽然有Emoticons这个Unicode Block但它只包含了80个Emoji而Emoji的总数有2666个覆盖面太低。实际上不只是正则表达式处理Emoji很麻烦通用的文本处理对它们也束手无策。正因为如此许多语言都提供了与Emoji相关的专门的工具有效降低了处理Emoji的难度。下面的代码来自Python的Emoji包其中的:thumbs_up_sign:就表示“挑大拇指”的Emoji。