天天看点

《正则表达式经典实例(第2版)》——2.7 Unicode码位、类别、区块和字母表

本节书摘来自异步社区《正则表达式经典实例(第2版)》一书中的第2章,第2.7节,作者: 【美】jan goyvaerts , steven levithan著,更多章节内容可以访问云栖社区“异步社区”公众号查看

问题描述

使用一个正则表达式来查找商标符号(™),要求通过指定其unicode码位,而不是复制并粘贴一个实际上的商标符号。如果你选择复制并粘贴,那么商标符号可以被看作另外一个字面字符,即使并不能从键盘上直接输入它。字面字符已经在实例2.1中进行了讨论。

创建一个正则表达式来匹配unicode“货币符号”(currency symbol)类别的任意字符。

创建一个正则表达式来匹配在unicode“希腊扩展”区块中的任意字符。

创建一个正则表达式来匹配根据unicode标准属于希腊字母表一部分的任意字符。

创建一个正则表达式来匹配一个字形(grapheme),它通常也被当作同一个字符:一个基本字符加上它所有的组合标记。

解决方案

unicode码位

在python 2.x中,这些正则表达式需要作为一个unicode字符串u"u2122"或u"u00002122"引用。

pcre必须使用utf-8支持进行编译,在php中,需要使用/u模式修饰符来打开utf-8支持。

ruby 1.8不支持unicode正则表达式。

unicode类别

pcre必须使用utf-8支持进行编译,在php中,需要使用/u模式修饰符来打开utf-8支持。javascript和python不支持unicode属性。可以使用xregexp为javascript添加unicode属性支持。ruby 1.8不支持unicode正则表达式。

unicode 区块

javascript、pcre、python和ruby 1.9不支持unicode区块。但它们支持unicode码位,可以用本例“变体”一节演示的方法匹配区块。xregexp可以为javascript添加unicode区块支持。

unicode字母表

unicode字母表(script)支持要求pcre 6.5或者更新版本,而且pcre必须使用utf-8支持进行编译。在php中,需要使用/u模式修饰符来打开utf-8支持。.net、javascript和python不支持unicode属性。xregexp可以为javascript添加unicode属性支持。ruby 1.8不支持unicode正则表达式。

unicode字形

pcre和perl都包含一个专门的记号来匹配字形pcre。必须使用utf-8支持进行编译;在php中,需要使用/u模式修饰符来打开utf-8支持。

.net、java、xregexp和ruby 1.9没有匹配字形的记号。但是它们支持unicode类别,可以用来模拟匹配字形。

javascript(不使用xregexp时)和python不支持unicode字形。ruby 1.8不支持unicode正则表达式。

讨论

unicode 码位

码位(code point)是unicode字符数据库中的一个条目。码位与字符是不一样的,当然这还要基于你给“字符”什么样的含义。在unicode中,在屏幕上作为字符出现的符号被称作是一个字形(grapheme)。

unicode码位u+2122表示的是“商标符号”字符。根据所使用的正则流派的不同,你可以使用‹u2122›、‹u{2122}›或‹x{2122}›来匹配这个字符。

‹u›的语法要求必须使用四位十六进制数字。这意味着你只能用它来表示u+0000~u+ffff的unicode码位。

‹u{⋯}›和‹x{⋯}›语法则允许花括号间出现1~6位十六进制数字,这可以支持从u+000000~u+10ffff的所有码位。你可以使用‹x{e0}›或‹x{00e0}›来匹配u+00e0。u+100000之后码位是很少使用的,字体和操作系统也没有对它们提供很好的支持。

python的正则表达式引擎不支持unicode码位。python 2.x中字面unicode字符串和python 3.x中字面文本字符串需要转义unicode码位。u0000 ~uffff表示u+0000到u+ffff的unicode码位,u00000000~u0010ffff表示全部unicode码位。在u后必须使用8位十六进制数字,无论是否使用了u+10ffff以后的unicode码位。

在python代码中使用字面字符串硬编码正则表达式时,可以直接使用‹u2122›和‹u00002122›。从文件或用户输入中读取正则式时,如果把这些读取或接收到的unicode转义直接传递给re.compile()将不会正常工作。在python 2.x中,可以调用string.decode ('unicode-escape')解码unicode转义。在python 3.x中,可以调用string.encode('utf-8'). decode('unicode-escape')。

码位可以在字符类之内和之外进行使用。

每个unicode码位都属于一个单独的unicode类别(category)。一共存在30个由两个字母代表的unicode类别,它们被组织为7个单一字母代表的超类。

‹p{l}› 任意语言的任意字母

‹p{m}› 用于与另外一个字符组合使用的字符(重音符号、变音符号、包围框等)

‹p{z}› 任何种类的空白或不可见的分隔符

‹p{s}› 数学符号、货币符号、装饰标志(dingbat)、制表符(box-drawing)等

‹p{n}› 任意字母表中的任意种类的数字字符

‹p{p}› 任意种类的标点字符

‹p{c}› 不可见的控制字符和未使用的码位

‹p{cs}› 在utf-16编码中一个替代对的一半

‹p{cn}› 没有赋予任何字符的码位

‹p{ll}›匹配属于“小写字母”类别的单个码位。‹p{l}›可以被用作‹[p{ll} p{lu}p{lt}p{lm}p{lo}]›的简写形式,用来匹配在任意“字母”类别中的单个码位。

‹p›是‹p›的否定版本。‹p{ll}›匹配不属于ll类别的单个码位。‹p{l}›匹配不拥有任何“字母”属性的单个码位。这与‹[p{ll}p{lu}p{lt}p{lm}p{lo}]›是不一样的,后者会匹配所有的码位。‹p{ll}›匹配属于lu类别(以及除了ll之外的所有其他类别)的码位,而‹p{lu}›会包含ll码位。把这两个组合到一个码位组中就可以把所有可能的码位都包括进来。

《正则表达式经典实例(第2版)》——2.7 Unicode码位、类别、区块和字母表

在perl和pcre 6.5及以后版本中,‹p{l&}›可视为‹[p{ll}p{lu]} p{lt}]›的简写,匹配字母表中所有拥有大小写变体的字母。

unicode区块

unicode字符数据库把所有码位划分为不同的区块。每个区块由一个连续范围内的码位组成。在unicode 6.1版本的标准中码位u+0000~u+ffff被划分为156个区块:

unicode区块是一个连续范围之内的码位。虽然许多区块都拥有unicode字母表和unicode类别的名称,但是它们并不是百分之百相对应的。一个区块的名称只是用来说明它的主要用途。

currency(货币)区块中并不包含美元和日元符号。由于历史的原因,这些符号在basiclatin和latin-1supplement区块中才能找到。但是二者都属于currency symbol(货币符号)类别。如果要匹配任何货币符号,那么应该使用p{sc},而不是p{incurrency}。

大多数区块中都包含没有分配的码位,这些都被包括在了类别‹p{cn}›中。其他unicode类别,以及所有的unicode字母表中,都不会包含未分配的码位。

‹p{inblockname}›的语法可以在.net、xregexp和perl中使用,而java使用的则是‹p{isblockname}›的语法。

perl同样支持is变体形式,但是我们推荐你坚持使用in的语法,这是为了不与unicode字母表发生混淆。对于字母表来说,perl支持‹p{script}›和‹p{isscript}›,但是不支持‹p{inscript}›。

unicode标准规定区块名称不区分大小写,并且忽略空格、连字符和下划线。遗憾的是,绝大多数正则流派没有这么灵活。.net所有版本和java 4都要求符合上表所示大小写格式。perl 5.8及以后版本和java 5及以后版本允许混用大小写格式。perl、java和.net均支持使用连字符和不含空格的格式。推荐使用这种形式。本书讨论的正则流派中,只有xregexp和perl 5.12及以后版本可以灵活处理unicode区块名称中的空格、连字符和下划线。

除了未分配的码位之外,每个unicode码位都是刚好属于一个unicode字母表。未分配的码位不属于任意字母表。在unicode 6.1版本的标准中,到u+ffff之前的已经分配的所有码位被分配到了如下72个字母表中:

字母表是由某种人类特定语言书写系统使用的一组码位组成。一些字母表,如thai(泰语),对应于单个的人类语言。其他字母表,如latin(拉丁),则会涉及多种语言。有些语言是由多种字母表来组成的。例如,其中并不存在一种日语unicode字母表;事实上,unicode提供了日语文档中通常会使用到的hiragana(平假名)、katakana(片假名)、han(汉字)和latin(拉丁)字母表。

在上面的列表中列在第一个的common(常见)字母表没有按照字母顺序排列。这种字母表包含了对于许多字母表相同的各种字符,如标点、空白符号以及其他各色符号。

java要求字母表名称前使用is,如‹p{isyi}›。perl允许is前缀,但不强制要求。xregexp、pcre和ruby则不允许is前缀。

unicode标准要求字母表名称不区分大小写,并且忽略空格、连字符和下划线。遗憾的是,绝大多数正则流派没有这么灵活。本书中介绍的所有支持字母表的流派都支持符合上表大小写与下划线格式的字母表名称。

当使用到组合标志(combining marks)的时候,码位与字符的区别就展现出来了。unicode码位u+0061是“拉丁小写字母a”,而u+00e0是“加了重音符号的拉丁小写字母a”。通常来说大多数人把二者都称作字符。

u+0300是“重音组合符号”的组合标志。它只有在一个字母之后使用才有意义。一个包含unicode码位u+0061 u+0300的字符串会被显示为à,这同u+00e0是完全一样的。组合标志u+0300会被显示到字符u+0061的顶上。

之所以会出现两种不同方式来表示一个加重音符号的字符,是因为在许多历史上的字符集中,把“带有重音符号的a”编码为了单个字符。unicode的设计者认为有必要与这些常用的遗留字符集保持一对一的映射,另外unicode新增了把标志和基本字母分开的表示方式,这样可以使遗留字符集无法支持的任意组合成为可能。

对于一个正则表达式用户来说,重要的是本书中介绍的所有正则流派操作的都是码位而不是图形化的字符。当我们说正则表达式‹.›匹配单个字符的时候,实际上的含义是它匹配单个的码位。如果你的目标文本中包含了两个码位u+0061 u+0300,在像java这样的编程语言中它可以使用字符串常量"u0061u0300"来表示,那么一个点号只能匹配码位u+0061(也就是a),而不会匹配重音符号u+0300。使用正则表达式‹..›才可以同时匹配二者。

perl和pcre提供了一个特殊的正则表达式记号‹x›,用来匹配任意单个的unicode字形。本质上说,它是unicode版本的特殊点号。无论采取哪种编码方式,‹x›都会在文本àà中找到两个匹配。如果它的编码是u00e0u0061u0300,那么第一个匹配是u00e0,第二个匹配是u0061u0300。匹配任何单一unicode码位的点号,则会分三次分别匹配u00e0、u0061和u0300。

将unicode码位组合视为字形的规则相当复杂1ff。通常来说,要匹配一个字形我们需要匹配不是组合标志的任意unicode码位,以及紧跟其后的所有组合标志(如果有的话)。我们也可以在支持unicode的正则流派中使用正则‹(?>p{m}p{m})›代替‹x›记号匹配字形。‹p{m}›匹配所有不属于mark(标志)类别的符号。‹p{m}›匹配其后所有的标志(如果存在的话)。

我们将两个正则表达式记号放置在同一个固化分组(atomic group)中,确保‹p{m}›后面的正则表达式记号匹配失败时不会回溯。‹x{2}.›不会匹配àà,因为在‹x{2}›匹配两个重音字母后没有可供点号匹配的字符。同理‹(?>p{m}p{m}){2}.›也不会匹配àà。但是如果编码方式为u00e0u0061u0300,则非捕获型分组‹(?:p{m} p{m}){2}.›可以匹配àà。在第二次尝试匹配时,‹p{m}›会匹配u0300,而点号则会匹配失败。因此正则式会回溯,使‹p{m}*›交回它所匹配的字符,从而使点号成功匹配u0300。

javascript的正则引擎不支持固化分组。xregexp也无法实现此特性,因为xregexp仍然依赖javascript的正则引擎实现其模式匹配。所以在使用xregexp时,‹(?>p{m}p{m})›是我们最接近的模拟‹x›的实现。没有固化分组时,需要牢记正则式中‹(?:p{m}p{m})›后面任何可以匹配标志(mark)类别字符的记号都可能使‹p{m}*›回溯。

变体

否定变体

大写形式的‹p›是小写形式‹p›的否定变体。例如,‹p{sc}›会匹配不拥有“currency symbol”(货币符号)unicode属性的任意字符。所有支持‹p›的流派都会在其所支持的属性、区块和字母表中支持‹p›。

字符组

所有流派都允许把它们所支持的所有的‹u›、‹x›、‹p›和‹p›记号用在字符组之内。这样会把码位所表示的字符,或者是在该类别、区块或者字母表中的字符添加到字符组中。例如,你可以用如下的正则式来匹配一个左引号(初始标点属性)、一个右引号(终止标点属性)或商标符号(u+2122):

列出所有字符

如果你的正则表达式流派不支持unicode类别、区块或字母表的话,那么你可以把属于该类别、区块或字母表的字符枚举到一个字符组中。对于区块来说,这会比较容易:因为每个区块其实就是两个码位之间的一个范围。例如,希腊语扩展(greek extended)区块包括u+1f00~u+1fff的字符:

然而对于大多数类别和许多字母表来说,与之等价的字符组是单个码位和较短范围的一张冗长列表。构成每个类别和许多字母表的字符是散布在unicode表中的。下面表示的是希腊语字母表:

在构造这个正则表达式的过程中,我们用unicodeset web应用(<code>http://unicode.org/cldr/utility/list-unicodeset.jsp</code>)生成了希腊语的字母表。在文本框输入p{greek},勾选“abbreviate”(缩写)和“escape”(转义)复选框,再单击“show set”(显示码位集)按钮。

如本实例先前“unicode码位”一节的解释,只有python支持这种unicode码位语法。要使其他正则流派使用此正则表达式,需要做一些修改。

如果从字符组中删除u+ffff以后的码位,则很多流派可以正常使用此表达式:

对于unicode码位,perl和pcre使用不同的语法。在上述正则式中,需要把‹uffff›替换为‹x{ffff}›、‹u0010ffff›替换为‹x{10ffff}›。java 7同样支持这个正则式: