热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

正则表达式(二):Unicode诸问题(上)

关于正则表达式的文档很多,但大部分都是英文的,即便有中文的文档,也翻译或改编自英文文档。在介绍功能时,这样做没有大问题&#x

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

\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

\u0026#xD;\nPython 2.x \u0026#xD;\n\u0026gt;\u0026gt;\u0026gt; re.search('^.$', '发') == None # True \u0026#xD;\nPHP 4.x/5.x \u0026#xD;\npreg_match('/^.$/', '发') // 0 \u0026#xD;\nRuby 1.8 \u0026#xD;\nirb(main):001:0\u0026gt; '发' =~ /^.$/ # nil \u0026#xD;\n

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

\u0026#xD;\n

\u0026#xD;\nPython 2.x \u0026#xD;\n\u0026gt;\u0026gt;\u0026gt; re.search('^.$', u'发') == None #False \u0026#xD;\nPHP 4.x/5.x \u0026#xD;\npreg_match('/^.$/u', '发') // 1 \u0026#xD;\nRuby 1.8 \u0026#xD;\nirb(main):001:0\u0026gt; '发' =~ /^.$/u # 0\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

\u0026#xD;\nPython 2.x \u0026#xD;\n\u0026gt;\u0026gt;\u0026gt; re.search('^[我]$', u'我') == None # True \u0026#xD;\n\u0026gt;\u0026gt;\u0026gt; re.search(u'^[我]$', u'我') == None # False \u0026#xD;\n\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

字符串

\u0026#xD;\n
\u0026#xD;\n

正则表达式

\u0026#xD;\n
\u0026#xD;\n

语言

\u0026#xD;\n
\u0026#xD;\n

是否显式指定Unicode模式

\u0026#xD;\n
\u0026#xD;\n

可否匹配

\u0026#xD;\n
\u0026#xD;\n

\u0026#xD;\n
\u0026#xD;\n

^.$

\u0026#xD;\n
\u0026#xD;\n

Java

\u0026#xD;\n
\u0026#xD;\n

否(无须指定)

\u0026#xD;\n
\u0026#xD;\n

可以

\u0026#xD;\n
\u0026#xD;\n

^.$

\u0026#xD;\n
\u0026#xD;\n

Javascript

\u0026#xD;\n
\u0026#xD;\n

否(无法指定)

\u0026#xD;\n
\u0026#xD;\n

由浏览器的实现决定

\u0026#xD;\n
\u0026#xD;\n

/^.$/

\u0026#xD;\n
\u0026#xD;\n

PHP

\u0026#xD;\n
\u0026#xD;\n

\u0026#xD;\n
\u0026#xD;\n

不可以

\u0026#xD;\n
\u0026#xD;\n

/^.$/u

\u0026#xD;\n
\u0026#xD;\n

PHP

\u0026#xD;\n
\u0026#xD;\n

\u0026#xD;\n
\u0026#xD;\n

可以

\u0026#xD;\n
\u0026#xD;\n

/^.$/

\u0026#xD;\n
\u0026#xD;\n

Ruby 1.8

\u0026#xD;\n
\u0026#xD;\n

\u0026#xD;\n
\u0026#xD;\n

不可以

\u0026#xD;\n
\u0026#xD;\n

/^.$/u

\u0026#xD;\n
\u0026#xD;\n

Ruby 1.8

\u0026#xD;\n
\u0026#xD;\n

\u0026#xD;\n
\u0026#xD;\n

可以

\u0026#xD;\n
\u0026#xD;\n

/^.$/

\u0026#xD;\n
\u0026#xD;\n

Ruby 1.9

\u0026#xD;\n
\u0026#xD;\n

\u0026#xD;\n
\u0026#xD;\n

可以

\u0026#xD;\n
\u0026#xD;\n

^.$

\u0026#xD;\n
\u0026#xD;\n

.NET

\u0026#xD;\n
\u0026#xD;\n

\u0026#xD;\n
\u0026#xD;\n

可以

\u0026#xD;\n
\u0026#xD;\n

^.$

\u0026#xD;\n
\u0026#xD;\n

Python 2.x

\u0026#xD;\n
\u0026#xD;\n

\u0026#xD;\n
\u0026#xD;\n

不可以

\u0026#xD;\n
\u0026#xD;\n

^.$

\u0026#xD;\n
\u0026#xD;\n

Python 3

\u0026#xD;\n
\u0026#xD;\n

\u0026#xD;\n
\u0026#xD;\n

可以

\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

\u0026#xD;\nPython 2.x(字符均为全角) \u0026#xD;\n\u0026gt;\u0026gt;\u0026gt; re.search('(?u)^\\d$', u'1') == None # True \u0026#xD;\n\u0026gt;\u0026gt;\u0026gt; re.search('(?u)^\\w$', u'发') == None # True \u0026#xD;\n\u0026gt;\u0026gt;\u0026gt; re.search('(?u)^\\s', u' ') == None # True \u0026#xD;\n\u0026#xD;\n

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

\u0026#xD;\n

下面的表格列出了常见语言中的匹配规定\u0026#xD;\n

\u0026#xD;\n

语言

\u0026#xD;\n
\u0026#xD;\n

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

\u0026#xD;\n
\u0026#xD;\n

Java

\u0026#xD;\n
\u0026#xD;\n

均只能匹配ascii字符

\u0026#xD;\n
\u0026#xD;\n

Javascript

\u0026#xD;\n
\u0026#xD;\n

均只能匹配ascii字符

\u0026#xD;\n
\u0026#xD;\n

PHP

\u0026#xD;\n
\u0026#xD;\n

均只能匹配ascii字符

\u0026#xD;\n
\u0026#xD;\n

Ruby 1.8

\u0026#xD;\n
\u0026#xD;\n

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

\u0026#xD;\n
\u0026#xD;\n

Ruby 1.9

\u0026#xD;\n
\u0026#xD;\n

均可以识别Unicode字符

\u0026#xD;\n
\u0026#xD;\n

.NET

\u0026#xD;\n
\u0026#xD;\n

均可以识别Unicode字符

\u0026#xD;\n
\u0026#xD;\n

Python 2.x

\u0026#xD;\n
\u0026#xD;\n

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

\u0026#xD;\n
\u0026#xD;\n

Python 3

\u0026#xD;\n
\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

GBK编码的情况

\u0026#xD;\n

罚 b7 a3

\u0026#xD;\n

[收发] ca d5 b7 a2

\u0026#xD;\n

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

\u0026#xD;\n

罚 7f5a

\u0026#xD;\n

[收发] 6536 53d1

\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

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

\u0026#xD;\n

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


推荐阅读
  • Python爬虫中使用正则表达式的方法和注意事项
    本文介绍了在Python爬虫中使用正则表达式的方法和注意事项。首先解释了爬虫的四个主要步骤,并强调了正则表达式在数据处理中的重要性。然后详细介绍了正则表达式的概念和用法,包括检索、替换和过滤文本的功能。同时提到了re模块是Python内置的用于处理正则表达式的模块,并给出了使用正则表达式时需要注意的特殊字符转义和原始字符串的用法。通过本文的学习,读者可以掌握在Python爬虫中使用正则表达式的技巧和方法。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 本文介绍了在Python3中如何使用选择文件对话框的格式打开和保存图片的方法。通过使用tkinter库中的filedialog模块的asksaveasfilename和askopenfilename函数,可以方便地选择要打开或保存的图片文件,并进行相关操作。具体的代码示例和操作步骤也被提供。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 本文介绍了在处理不规则数据时如何使用Python自动提取文本中的时间日期,包括使用dateutil.parser模块统一日期字符串格式和使用datefinder模块提取日期。同时,还介绍了一段使用正则表达式的代码,可以支持中文日期和一些特殊的时间识别,例如'2012年12月12日'、'3小时前'、'在2012/12/13哈哈'等。 ... [详细]
  • PHP图片截取方法及应用实例
    本文介绍了使用PHP动态切割JPEG图片的方法,并提供了应用实例,包括截取视频图、提取文章内容中的图片地址、裁切图片等问题。详细介绍了相关的PHP函数和参数的使用,以及图片切割的具体步骤。同时,还提供了一些注意事项和优化建议。通过本文的学习,读者可以掌握PHP图片截取的技巧,实现自己的需求。 ... [详细]
  • Java实战之电影在线观看系统的实现
    本文介绍了Java实战之电影在线观看系统的实现过程。首先对项目进行了简述,然后展示了系统的效果图。接着介绍了系统的核心代码,包括后台用户管理控制器、电影管理控制器和前台电影控制器。最后对项目的环境配置和使用的技术进行了说明,包括JSP、Spring、SpringMVC、MyBatis、html、css、JavaScript、JQuery、Ajax、layui和maven等。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • Python如何调用类里面的方法
    本文介绍了在Python中调用同一个类中的方法需要加上self参数,并且规范写法要求每个函数的第一个参数都为self。同时还介绍了如何调用另一个类中的方法。详细内容请阅读剩余部分。 ... [详细]
  • Python语法上的区别及注意事项
    本文介绍了Python2x和Python3x在语法上的区别,包括print语句的变化、除法运算结果的不同、raw_input函数的替代、class写法的变化等。同时还介绍了Python脚本的解释程序的指定方法,以及在不同版本的Python中如何执行脚本。对于想要学习Python的人来说,本文提供了一些注意事项和技巧。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 延迟注入工具(python)的SQL脚本
    本文介绍了一个延迟注入工具(python)的SQL脚本,包括使用urllib2、time、socket、threading、requests等模块实现延迟注入的方法。该工具可以通过构造特定的URL来进行注入测试,并通过延迟时间来判断注入是否成功。 ... [详细]
author-avatar
曹lister_638
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有