Archive for September, 2009
PCRE/Python 下的 re 细节(3) — Unicode
本篇是概念流,但个人认为这个 Recipe 2.7 的内容比 Mastering Regular expressions 中关于 Unicode 的部分讲得更精到,相当有看点的说;再就是对 unicode 的完美支持大多集中在 PCRE/Perl 上,所以下面例出的手法在除 Perl 外的大多数语言中都是屠龙之技。(怒了,Perl 6 杀很大,你到底准备在哪年圣诞节君临天下啊…)
代码点(code point)
一个代码点就是 Unicode 字符集中的一个条目,虽然说多数字符都唯一映射到一个对应的代码点,但一个代码点还是不能说完全等同于一个字符,因为有些语言中的单个字符是映射到 Unicode 中的多个代码点组合上的,比如字符 å 就映射到 U+0061 和 U+030A 两个代码点。
可以用 \uXXXX 和 \x{XXXX} 匹配一个代码点,使用 \u 还是 \x 取决于所使用的语言流派,前者在 Python 中使用,后者则应用在 PCRE/Perl 之中。此外,二者还有个区别是 \u 后面的 XXXX 必须是固定的4位16进制数值,范围从 U+0000 到 U+FFFF;而 \x 后面的 XXXX 则支持变长的16进制数,取值从 U+000000 到 U+FFFFFF,即 \x{0000E0} 等价于 \x{E0}。另外就是代码点在字符组(character classes)内外都可以使用。
属性(property)和分类(category)
每个代码点都有一个属性或者说属于某个分类,这个只是说法不同,二者想要表达的意思是完全一样的,这里是所有 Unicode 分类的列表,总共7大类30小类。所谓分类其实就是把所有 Unicode 代码点按其意义分组并形成多个不同的类别。比如 Sc 这个分类下面就全是跟货币符号相关的代码点,分类 Sm 下面全是跟数学符号相关的代码点。PCRE/Perl 下用 \p{Sc} 和 \p{Sm} 来匹配前面提到的两组分类,可惜的是 Python re 不支持按 Unicode 分类来匹配代码点,怒~~~
块(block)
块和分类的概念差不多,也是把代码点划分为 105 个不同的类别,但划分得更细腻,块名字也更适合人类阅读,块名字显示了这个块下面的代码点的主要用途。比如上面的 \p{Sc} 等价的块匹配就是 \p{InCurrency} 明显 InCurrency 比 Sc 可读性高上一百倍呀一百倍。然后让我们继续回到鄙视 Perl 的大路上了,块匹配更是连 PCRE 都不支持了,可挨千刀的 Perl 继续在完美的支持它,\p{InBlockName} 和 \p{IsBlockName} 这两种语法都可以工作,但推荐使用 \p{InBlockName} 这种语法,因为 Is 可能会和 \p{IsScript} 产生混淆。
Script
这个 Script 我真不知道该怎么翻译了,保持原样好了。Script 和 block/category 也都差不多,它按自然语言的不同将代码点分成了汉语(Han)、片假名(Katakana)、希腊语(Greek)等 N 多个分组。但这种划分并不是完全一一对应到自然语言的,有的 Script 下可能包括多种自然语言中的字符,比如 Latin Script;而有的自然语言下的字符所对应的代码点又会被划分到多个不同的 Script 中,比如 Hiragana Script(平假名)、Katakana Script(片假名)、Han Script(汉语)和 Latin Script 这4种 Script 下面均有日文字符的代码点。\p{Script} 和 \p{IsScript} 都用来匹配某个 Script,但推荐使用前者,以避免和 \p{IsBlockName} 发生冲突。是的,你没有猜错,这个不知道是什么东西的 Script 在 Python 下也是不支持的,它仅在 PCRE/Perl 下面生效。
字形(grapheme)
前面有提到代码点和字符并全是一一对应的关系,会有一个字符对应到多个代码点组合的情形发生,比如字符 å 就对应到 U+0061 和 U+030A 两个代码点。当使用点号去匹配 Unicode 字符时,其实只匹配得到一个代码点,也就是只匹配得到 U+0061,这明显不是期望中的结果。要解决这个问题,就需要用到 \X 模式,这里有我以前写的一个例子,现在就不多说了。
最后一句,所有 \p 都有对应的 \P 模式,\P 的意思就是匹配所有非 XXX 的情况。比如 \P{Sc} 就是匹配所有非 Currency 代码点。然后是支持 \p 的流派也都支持 \P 的,所以那些流派可以用 \P{M}\p{M}* 来模拟 PCRE/Perl 的 \X 模式,但由于 Python 连 \p 都不支持,所以建议各位用 Python 童鞋继续望 Perl 兴叹吧,Perl 6深情的凝望…
PCRE/Python 下的 re 细节(2) — 点号、^ 和 $
古时候正则表达式被用来处理一行一行读入的文件内容,那时候是不会存在“匹配一个换行符”这样的需求滴。后来神说这个可以有,于是世间就出现了单行模式。我猜可能是由于拉伯伯是学语言(自然语言,不是计算机语言)出身的缘故,才会给出 single line 这么个让普罗大众有些目眩神迷的称谓。要避免出现大海的感觉,还是得余大的点号通配模式比较靠谱一点(Python re 下面叫 dotall 模式)。关于多行模式和单行模式(点号通配模式)的区别这里就不再赘述了,有兴趣的可以猛击“single line 和 multiline 一毛钱关系都没有”查看详情。下面就直接给出两种实现点号通配模式(单行模式)的办法:
1. 在表达式外部指定点号通配模式,Python 下用 re.S 参数,这个地球人都晓得滴。
2. 在表达式内部用 (?s) 模式修饰符,这个就比较优雅了,从表达式本身就可以看出处于点号通配模式下,推荐使用。
相应的,激活多行模式也有两种手法:
1. 用 re.M 参数
2. 用 (?m) 模式修饰符
PCRE/Python 下的 re 细节(1)
O’Reilly 的 cookbook 系列差不多都是适合摆在手边准备随时查阅的工具书,期间充斥着大量的技术细节。今年七月的时候 rex 同学(一、二)还送了这本热腾腾的 Regular Expressions Cookbook 给我,可惜一直没来得及看,想当初甚至还雄心勃勃准备和 rex 一起翻译这本书的,但最后还是由于这样那样的原因导致计划搁浅,我真是愧对 rex 的信任。为了对得起 rex 同学的美貌与智慧,现在我准备花时间读一读,至少每天一个 recipe 嘛,然后把其中一些关于 PCRE 和 Python 的亮点都记录在这里,以飨诸君。:)
不废话,上 Recipe…
希望匹配目标文本中的 $()*+.?[\^{| 这12个符号时,就必须对表达式中的相应字符进行转义,意即表达式应该是这个样子滴:\$\(\)\*\+\.\?\[\\\^\{\|
除了上面 12 个特殊字符外,其余的特殊字符就无需进行转义了。不过,即使在无需进行转义的特殊字符前添加了转义符 \ 也是不会报错的,只是多余的 \ 可能会使表达式变得难以理解。再就是 PCRE 支持 \Q, \E 对,意思是 \Q 和 \E 间的特殊字符会被自动转义,也就是说表达式 \Q$()*+.?[\^{|\E 和第1个表达式是等价的。
除反斜线 \ 和 ] 外, [...] 中的特殊字符也不必手工转义,比较特殊的情况是 ^ 和 – 这两个。当 ^ 是 [...] 中第一个出现的字符时,表示不匹配后面出现的字符,而 – 出现在文数字中间时,则表示从 xxx 到 xxx 的含义。因此欲匹配这两种情形下的 ^ 和 – 时是需要转义的。
忽略大小写敏感
默认情况下表达式是大小写敏感的,但可以通过 (?i) 来忽略大小写敏感。例如表达式 (?i)ab 可以匹配的目标文本包括 ab, AB, aB 和 Ab
PCRE 同时支持 (?-i) 来关闭 (?i) 的忽略效果,使 (?-i) 后面的表达式字符重回大小写敏感的默认状态。
unpack 的三种实现
解析文件或者网络数据时,基本的操作就是 unpack 流内容,下面演示了三种不同的 unpack 手法,分别使用了 re, struct.unpack 和 list slice,最后是三种手法的性能比较。
代码:
上面代码演示了将文本 unpack 为长度分别是 5, 3(忽略), 8, 8, k 的子串,下面是对三个函数进行百万次调用后的性能分析:

其实三种手法还是有一些差异的,struct.unpack 是基于 bytes 来拆分,而 re 和 list slice 是基于字符拆分的,所以在非 ASCII 的情况下要特别注意。
反转序列的可读性和性能
老规矩,直接上图上真相…
代码:
-
#!/usr/bin/env python
-
#coding=utf-8
-
-
rev_by_index = lambda x: x[::-1]
-
rev_by_func = lambda x: [e for e in reversed(x)]
-
-
if __name__ == ‘__main__’:
-
for i in xrange(10, 20001):
-
lst = range(i)
-
rev_by_index(lst)
-
rev_by_func(lst)
-
结果:

9.7s v.s. 0.6s … 你选择可读性还是性能?