原文:http://www.infoq.com/cn/news/2011/02/regular-expressions-unicode

关于正则表达式的文档很多,但大部分都是英文的,即便有中文的文档,也翻译或改编自英文文档。在介绍功能时,这样做没有大问题,但真要处理文本,就可能会遇到一些英文开发或应用环境中难得见到的问题。比如中文之类多字节字符的匹配,就是如此。所以,这篇文章专门谈谈正则表达式如何处理多字节字符,更准确地说,是如何处理Unicode编码的文本(为什么只提到Unicode编码,而没有提到其它编码,理由在后面详述)。

首先介绍关于编码的基础知识:

通常来说,英文编码较为统一,往往采用ascii编码或兼容ascii的编码(即编码表的前127位与ascii编码一致,常用的各种编码,包括Unicode编码都是如此)。也就是说,英文字母、阿拉伯数字和英文的各种符号,在不同编码下的表示是一样的,比如字母A,其编码总是41,常见的编码中,英文字符和半角标点符号的编码都等于ascii编码,通常只用一个字节表示。

但是中文的情况则不同,常见的中文编码有GBK(CP936)和Unicode两种,同一个中文字符在不同编码下的值并不相同,比如“发”字,GBK编码的值为b7 a2,用两个字节表示;而Unicode编码的值(也就是代码点,Code Point)为53 d1。如果用UTF-8编码保存,需要3个字节(e5 8f 91);用UTF-16编码保存,需要4个字节(53 d1)。

正因为中文字符需要多个字节来表示,常见的正则表达式的文档就有可能无法覆盖这种情况。比如常见的资料都说,点号『.』可以匹配“除换行符\n之外的任意字符”,但这可能只适用于“单字节字符”,因为点号匹配的其实只是“除换行符\n之外的任意字节”而已。不信,我们可以来试试看(以下例子中,程序均使用UTF-8编码):

Python 2.x
>>> re.search('^.$', '发') == None # True
PHP 4.x/5.x
preg_match('/^.$/', '发') // 0
Ruby 1.8
irb(main):001:0> '发' =~ /^.$/ # nil

之所以会出现这种情况,是因为正则表达式无法正确将多个字节识别为“单个字符”,让点号『.』能正确匹配。不过在Python 3.x、Java、.NET和Ruby 1.9中,字符串默认都是采用Unicode编码,所以不存在上面的问题。如果你使用的是Python 2.x、Ruby 1.8或PHP,也可以显式指定采用Unicode模式。

Python 2.x
>>> re.search('^.$', u'发') == None #False
PHP 4.x/5.x
preg_match('/^.$/u', '发') // 1
Ruby 1.8
irb(main):001:0> '发' =~ /^.$/u # 0

如果你细心就会发现,在Python 2.x中,我们指定的字符串使用Unicode编码,而文档里说了,正则表达式也可以指定Unicode模式的;相反,在PHP和Ruby中,我们指定正则表达式使用Unicode编码,而字符串并没有指定。这到底是怎么回事呢?

我们知道,正则表达式的操作可以简要概括为“用正则表达式去匹配字符串”,它涉及两个对象:正则表达式和字符串。对字符串来说,如果没有设定Unicode模式,则多字节字符很可能会拆开为多个单字节字符对待(虽然它们并不是合法的ascii字符),Python 2.x中就是如此,“发”字在没有设定Unicode编码时,变成了3个单字节字符构成的字符串,点号『.』只能匹配其中的单个“字符”。如果显式将正则表达式设定为Unicode字符串(也就是在 u'发' ),则“发”字视为单个字符,点号可以匹配。

而且,如果你在正则表达式的字符组里使用了中文字符,表示正则表达式的字符串,也应该设定为Unicode字符串,否则正则表达式会认为字符组里不是单个字符,而是3个单字节字符:

Python 2.x
>>> re.search('^[我]$', u'我') == None # True
>>> re.search(u'^[我]$', u'我') == None # False

另一方面,在PHP和Ruby中并不存在“Unicode字符串”,所以我们无法修改字符串的属性。但是,设定正则表达式为Unicode模式,正则表达式也可以正确识别字符串中的Unicode字符。所以,如果你用PHP或Ruby的正则表达式处理Unicode字符串,一定不要忘记指定Unicode模式。

点号『.』对Unicode字符的匹配“我”(采用UTF-8编码)

字符串

正则表达式

语言

是否显式指定Unicode模式

可否匹配

^.$

Java

否(无须指定)

可以

^.$

JavaScript

否(无法指定)

由浏览器的实现决定

/^.$/

PHP

不可以

/^.$/u

PHP

可以

/^.$/

Ruby 1.8

不可以

/^.$/u

Ruby 1.8

可以

/^.$/

Ruby 1.9

可以

^.$

.NET

可以

^.$

Python 2.x

不可以

^.$

Python 3

可以

注:PHP和Ruby的正则表达式本身是不包含分隔符(分隔符可以有很多种,常见的是反斜线/)的,但PHP指定Unicode模式必须在后一个分隔符之后写u,所以在这里将分隔符也写出来。

不过,如果你熟悉Python语言,会发现Python也可以指定正则表达式使用Unicode模式,这又是怎么回事呢?

不妨回头仔细想想你读过的文档,正则表达式中的『\d』和『\w』,都是如何解释的?或许你的第一反应是:『\d』等价于『[0-9]』,『\w』等价于『[0-9a-zA-Z_]』。因为有些文档说明了这种等价关系,有些文档却说:『\d』匹配数字字符,『\w』匹配单词字符。然而这只是针对ascii编码的规定,在Unicode编码中,全角数字0、1、2之类,应该也可以算“数字字符”,由『\d』匹配;中文的字符,应该也可以算“单词字符”,由『\w』匹配;同样的道理,中文的全角空格,应该也可以算作“空白字符”,由『\s』匹配。所以,如果你在Python中指定了正则表达式使用,『\d』、『\w』、『\s』就能匹配全角数字、中文字符、全角空格。

Python 2.x(字符均为全角)
>>> re.search('(?u)^\d$', u'1') == None # True
>>> re.search('(?u)^\w$', u'发') == None # True
>>> re.search('(?u)^\s', u' ') == None # True

老实说,这样的规定有时候确实让人抓狂,假设你希望用正则表达式『\d{6,12}』来验证一个长度在6到12之间的数字字符串,却没留意『\d』能匹配全角数字,验证就不够严密了。

下面的表格列出了常见语言中的匹配规定

语言

『\w』『\d』『\s』的匹配规则

Java

均只能匹配ascii字符

JavaScript

均只能匹配ascii字符

PHP

均只能匹配ascii字符

Ruby 1.8

默认情况下只能匹配ascii字符,Unicode模式只影响『\w』的匹配

Ruby 1.9

均可以识别Unicode字符

.NET

均可以识别Unicode字符

Python 2.x

默认情况下只能匹配ascii字符,Unicode模式下均可以识别Unicode字符

Python 3

默认情况下均可以识别Unicode字符,但可以显式指定ascii

注1:一般来说,单词边界『\b』能匹配的位置是:一端是『\w』,一端不是『\w』(也可以什么都没有),其中『\w』的规定与『\w』一样,但Java中则不是这样,细节比较复杂,这里不展开,有兴趣的读者可以自己试验。

注2:在Python 3中可以在表达式之前添加『(?a)』指定ascii模式。

虽然常见的中文字符编码有GBK和Unicode两种,但如果需要使用正则表达式处理中文,我强烈推荐使用Unicode字符,不仅是因为正则表达式提供了对Unicode的现成支持,而且因为GBK编码可能会有其它问题。比如:我们要求匹配“收”字或者“发”字,很自然会想到使用字符组『[收发]』,这思路是对的,但如果采用GBK编码,正则引擎见到的很可能不是“两个字符构成的字符组”,而是“四个字节构成的字符组”。

使用GBK编码,[收发]的解释『ca d5 b7 a2』

如果我们用『[收发]』来匹配字符“罚”(它的GBK编码是b7 a3),就会产生错误——虽然“罚”字既不等于“收”也不等于“发”,但“罚”和『[收发]』却可以匹配一个字节

GBK编码的情况

罚 b7 a3

[收发] ca d5 b7 a2

Unicode编码的情况(因为Unicode编码能正确识别,无论采用UTF-8还是UTF-16,Unicode字符都会正确转化为Unicode编码点)

罚 7f5a

[收发] 6536 53d1

“罚”的Unicode编码是7f5a,无论如何也不会发生错误匹配。

如果出于某些限制,只能使用GBK编码,也有一个偏方准确保证『[收发]』的匹配,就是把字符组『[收发]』改成多选分支『(收|发)』。此时如果要匹配成功,只能是两个连续的字节ca d5或者b7a2,而“罚”字两个字节为b7 a3,无法匹配。

但这样也会有问题,因为在GBK编码下字符串被当作“字节序列”来对待。比如字符串 “账珍”对应四个字节,d5 ca d5 e4,其中正好出现了“收”字对应的两个字节ca d5,正则表达式就可能在此处匹配成功。

更重要的问题在于排除型字符组的匹配,仍然使用上面的例子,假如我们希望匹配一个“收”和“罚”之外的字符,自然的思路就是使用排除型字符组『[^收发]』。但是通过上面的讲解,我们已经知道,这样“排除”的并不是2个字符,而是4个字节:ca d5 b7 a2。但“罚”字的GBK编码为b7 a3,b7这个字节被“排除”了,所以正则表达式会显示“罚”字不能由『[^收发]』匹配,这完全违背了我们的本意。

总的来说,所以如果使用GBK编码(或者说非Unicode编码),对此类问题基本是无解的。因此,根本的办法还是使用Unicode编码。

正则表达式(二):Unicode诸问题上篇(转)的更多相关文章

  1. [.net 面向对象程序设计进阶] (3) 正则表达式 (二) 高级应用

    [.net 面向对象程序设计进阶] (2) 正则表达式 (二)  高级应用 上一节我们说到了C#使用正则表达式的几种方法(Replace,Match,Matches,IsMatch,Split等),还 ...

  2. 正则表达式: javascript Unicode 中文字符 编码区间:\u4e00-\u9fa5

    正则表达式: javascript Unicode 中文字符  编码区间:\u4e00-\u9fa5 RegExp 对象 javascript Unicode 中文字符的 编码区间: \u4e00-\ ...

  3. 正则表达式(三):Unicode诸问题下篇(转)

    原文:http://www.infoq.com/cn/news/2011/04/regular-expressions-4 我们使用正则表达式,熟练掌握各种功能和结构只是手段,解决实际的问题才是真正的 ...

  4. java基础---->java中正则表达式二

    跟正则表达式相关的类有:Pattern.Matcher和String.今天我们就开始Java中正则表达式的学习. Pattern和Matcher的理解 一.正则表达式的使用方法 一般推荐使用的方式如下 ...

  5. 正则表达式通过Unicode属性匹配

    原文链接:http://zochen.iteye.com/blog/690716 Unicode 编码并不只是为某个字符简单定义了一个编码,而且还将其进行了归类. \pP 其中的小写 p 是 prop ...

  6. python中关于正则表达式二

    2.2 反向引用 \1, \2... 表达式在匹配时,表达式引擎会将小括号 "( )" 包含的表达式所匹配到的字符串记录下来.在获取匹配结果的时候,小括号包含的表达式所匹配到的字符 ...

  7. [正则表达式]匹配Unicode

    一.PHP[PCRE]之Unicode PCRE支持的16进制字符编码转义符有 \x00-\xFF,或\x{num},num为任意位16进制数 但并不支持\u0000-\uFFFF这的形式 PCRE运 ...

  8. python正则表达式二[转]

    原文:http://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html 1. 正则表达式基础 1.1. 简单介绍 正则表达式并不是Python的一 ...

  9. php 正则表达式二.基本语法

    官方手册正则语法:http://php.net/manual/zh/reference.pcre.pattern.syntax.php 正则表达式在线测试工具:regexpal 正则表达式的匹配先后顺 ...

随机推荐

  1. MHL相关资源链接

    http://www.mhlconsortium.org/ 消费者网站: www.meetmhl.com采用者网站:www.mhltech.org 博客http://blog.sina.com.cn/ ...

  2. transformNative_libsWithStripDebugSymbolForDebug

    local.properties #ndk.dir=C\:\\My_other\\Android_SDK\\ndk-bundle  把NDK注释掉就ok了

  3. System.Web.Mvc.AJAX缺少程序集引用

    今天在构建新项目的时候,出现这个错误,实际上这个出现过很多次了,但是每次都忘记之前怎么解决的,之前博客也不记得写过没有...

  4. HIGHGUI ERROR: V4L/V4L2: VIDIOC_S_CROP错误解决方法

    在树莓派上运行在windows上正确的程序,  报错: HIGHGUI ERROR: V4L/V4L2: VIDIOC_S_CROP OpenCV Error: Assertion failed (s ...

  5. SNAT DNAT MASQUERADE 区别

    SNAT,DNAT,MASQUERADE都是NATMASQUERADE是SNAT的一个特例SNAT是指在数据包从网卡发送出去的时候,把数据包中的源地址部分替换为指定的IP,这样,接收方就认为数据包的来 ...

  6. Clojure 学习入门(14)—— 循环控制

    Clojure 基于函数的流程控制   repeatedly 字面意思为重复函数.一般的用法如下: #(rand-int 11)) 8 2 6 6) 重复产生5次随机数.rand-int 11表示0至 ...

  7. Delphi应用程序的调试(一)

    集成式调试器是Delphi IDE的一个重要特性.该调试器使用户能方便地设置断点.监视变量.检查对象等等.在运行程序时,使用该调试器能快速查找出程序发生了什么(或未发生什么).一个号的调试器对程序开发 ...

  8. c++ ::开头

    std::string 表示std命名空间下的 string类.直接::开始,表示顶层命名空间(全局变量)std::string -> ::std::string 这样也可以.::和 文件路径的 ...

  9. 一起脱去小程序的外套和内衣 - 微信小程序架构解析

    版权声明:本文由渠宏伟  原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/760767001484042227 来源:腾云阁 ...

  10. CSS3 渐变效果

    CSS3 渐变效果 background-image: -moz-linear-gradient(top, #8fa1ff, #3757fa); /* Firefox */ background-im ...