http://www.infoq.com/cn/news/2011/03/regular-expressions-unicode-2
关于正则表达式的文档很多,但大部分都是英文的,即便有中文的文档,也翻译或改编自英文文档。在介绍功能时,这样做没有大问题,但真要处理文本,就可能会遇到一些英文开发或应用环境中难得见到的问题。比如中文之类多字节字符的匹配,就是如此。所以,这篇文章专门谈谈正则表达式如何处理多字节字符,更准确地说,是如何处理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或者b7 a2,而“罚”字两个字节为b7 a3,无法匹配。
但这样也会有问题,因为在GBK编码下字符串被当作“字节序列”来对待。比如字符串 “账珍”对应四个字节,d5 ca d5 e4,其中正好出现了“收”字对应的两个字节ca d5,正则表达式就可能在此处匹配成功。
更重要的问题在于排除型字符组的匹配,仍然使用上面的例子,假如我们希望匹配一个“收”和“罚”之外的字符,自然的思路就是使用排除型字符组『[^收发]』。但是通过上面的讲解,我们已经知道,这样“排除”的并不是2个字符,而是4个字节:ca d5 b7 a2。但“罚”字的GBK编码为b7 a3,b7这个字节被“排除”了,所以正则表达式会显示“罚”字不能由『[^收发]』匹配,这完全违背了我们的本意。
总的来说,所以如果使用GBK编码(或者说非Unicode编码),对此类问题基本是无解的。因此,根本的办法还是使用Unicode编码。
上一篇文章我们介绍了和Unicode有关的匹配问题,这篇文章我们主要讲述一下Unicode编码本身的特性,以便更好地运用正则表达式解决与Unicode相关的问题。
Unicode Code Point
Unicode字符多种多样,除去ascii中的字母、数字、标点和中文字符,还包括其它多种语言和多种符号,有些符号甚至很难打出来(比如表示商标注册的?),这时候该如何表示呢?再说远一点,如果我们想用一个字符组匹配所有中文字符,能不能像『[a-z]』那样呢?
所幸,每一个Unicode字符都对应自己的Unicode编码,也就是Unicode编码表中的一个代码点(Code Point),所以在正则表达式中的Unicode字符往往采用Unicode代码点来指定。
一般来说,指定代码点的形式有3种:『\uxxxx』、『\u{xxxx}』、『\x{xxxx}』(其中的xxxx为编码的值,\u之后必须有4位16进制数字)。.NET、Java、JavaScript和Python使用第一种方式,而PHP和Ruby使用第二种方式(Ruby 1.9以上版本才支持这种表示法),PHP使用第三种方式。
比如“发表”的“发”字对应的Unicode编码是53 d1,它在不同语言中的表示法如下:所以我们可以在.NET、Java、JavaScript的正则表达式中这样表示“发”字:“\u53d1”,Python稍有不同,必须使用u”\u53d1”(之前的u表示这是一个Unicode字符串); Ruby中,“发”则必须写作”\u{53d1}”。
现在以“发”字为例,介绍不同语言中的Unicode表示法:
编码
语言
表示法
备注
53 d1
.NET
\u53d1
Java
\u53d1
JavaScript
\u53d1
Python
\u53d1
必须使用Unicode字符串,Python 2.x中,要在字符串之前加u
Ruby
\u{53d1}
限Ruby 1.9以上版本,且必须显式指定Unicode模式
PHP
\x{53d1}
必须指定Unicode模式
既然可以这样指定Unicode字符,自然也可以在字符组中用范围表示法指定一个Unicode编码范围了。比如,我们查询Unicode编码表可知,中文的编码一般在4e00到9fff之间,所以可以用这样的字符组匹配中文字符(Unicode编码4e00-9fff归类为“CJK 统一表意符号”CJK Unified Ideographs[1],涵盖了绝大多数中文字符):
语言
字符组
备注
.NET
[\u4e00-\u9fff]
Java
[\u4e00-\u9fff]
JavaScript
[\u4e00-\u9fff]
Python
[\u4e00-\u9fff]
必须使用Unicode字符串,Python 2.x中,要在字符串之前加u
Ruby
[\x{4e00}-\x{9fff}]
限Ruby 1.9以上版本,且必须显式指定Unicode模式
PHP
[\u{4e00}-\u{9fff}]
必须指定Unicode模式
根据Unicode规范,每一个Unicode字符除了有唯一代码点对应,还具有其它属性,现在详细介绍三种属性,它们是:Unicode Property、Unicode Block、Unicode Script,下面的图粗略说明了这三者的关系。
Unicode Property
Unicode Property的记法类似『\p{L}』、『\p{Lo}』,它按照字符的功能分类Unicode字符,而不关心字符属于哪种语言,每个Unicode字符只能属于唯一Unicode Property。
举例来说,『\p{Z}』表示任意的空白字符或不可见的分隔符;『\p{P}』表示任何标点字符,等等。遇到中英文混排、全角半角同时出现的情况,我们就可以用『\p{Z}』匹配所有的空白字符(而不用关心空格到底是全角空格还是半角空格),用『\p{P}』匹配所有的标点字符(而不用关心逗号到底是中文逗号还是英文逗号),而不用费心细节。
如果我们把Unicode Property理解为一个“字符组”,那么它一定能对应某个排除型字符组,此排除型字符组的通行记法是将『\p{xx}』中的小写p改为大写P,写作『\P{xx}』。这样,『\P{Z}』对应『\p{Z}』无法匹配的字符,『\p{P}』对应『\p{P}』无法匹配的字符。Unicode Block和Unicode Script对应的排除型字符组也是这样标记,下面不再赘述。
支持Unicode Property的语言有.NET、Java、PHP和Ruby(限1.9以上版本),在PHP和Ruby中使用Unicode Property时,必须要开启Unicode模式,下面以『\p{P}』的匹配为例:
.NET
Regex.IsMatch(‘,’, “\\p{P}”); //true
Java
“,”.matches(“\\p{P}”); //true
PHP
preg_match(‘/\p{P}/u’, ‘,’); //1
Ruby 1.9
‘/\p{P}/u’ =~ ‘,’ # 0
Unicode Property的完整信息,可参考http://www.regular-expressions.info/unicode.html。
Unicode Block
Unicode Block则不同于Unicode Property,它按照编码区间划分Unicode字符,每个Unicode Block中的字符编码都是落在同一个连续区间的。因为Unicode编码表中,某种语言的字符通常是落在同一区间的,所以它也可以粗略表示某类语言的字符,比如\p{InHebrew}表示希伯莱语字符,『\p{InCJK_Compatibility}』表示兼容CJK(汉语、韩语、日本语)的字符。如果你细心观察,会发现Unicod Block的名字虽然类似某种语言的名字,但都有“In”(Java风格)或者“Is”(.NET风格)前缀,这表明它其实对应的还是“落在某个区间的Unicode字符”。
本书介绍的语言中,只有Java和.NET支持Unicode Block,它们的写法不相同:
Java:『\p{ InCJK_Compatibility_Ideographs }』
.NET:『\p{ IsCJK_Compatibility_Ideographs }』
我们可以在Java中用\p{InCJK_Compatibility}或者在.NET中用\p{IsCJK_Compatibility}粗略匹配中文字符,虽然它们可能匹配日文或者韩文字符,尚不够精确,但许多情况下确实够用了。
Java
“我”.matches(“\\p{InCJK_Compatibility_Ideographs}”); //true
.NET
Regex.IsMatch(‘我’, “\\p{IsCJK_Compatibility_Ideographs}”); //true
Unicode Block的完整信息,可参考http://www.regular-expressions.info/unicode.html。
Unicode Script
Unicode Script按照字符所属的书写系统来划分Unicode字符,比如\p{Greek}表示希腊语字符,\p{Han}表示汉语(中文字符)。它的写法类似Unicode Block,只是名字的开头没有“Is”或者“In”。
在本书介绍的语言中,只有PHP和Ruby(限1.9以上版本)支持Unicode Script,PHP在使用Unicode Script时,必须开启Unicode模式(详见xx页)。在这两种语言中,我们可以很方便地用\p{Han}来匹配中文字符。
PHP
preg_match(‘/\p{Han}/u’, ‘我’); //1
Ruby 1.9
/\p{Han}/u =~ '我' #0
Unicode Script的完整信息,请参考http://www.regular-expressions.info/unicode.html。
小结
用正则表达式处理包含多字节字符(比如中文)的字符串时,最好使用Unicode编码,否则多字节字符很可能被割裂为多个字节,导致匹配错误,最好的办法是使用Unicode编码。
如果实在不能使用Unicode编码,使用字符组时要尤其小心,普通字符组可以用多选分支取代,绕过隐患,但这种办法对排除型字符组行不通。
如果设定了Unicode模式,可以指定Unicode Code Point来表示某个字符,但不同语言中的记法不同,可能是\uxxxx、\u{xxxx}、\x{xxxx}。
Unicode Property描述Unicode字符的属性,.NET、Java、PHP和Ruby(限1.9以上版本)中可以使用Unicode Property。
Unicode Block描述Unicode字符所在的区间,其特征是名称以In或Is为前缀,.NET和Java中可以使用Unicode Block。
Unicode Script描述Unicode字符所在的书写系统,名字类似Unicode Block,但没有In或Is前缀,PHP和Ruby(限1.9以上版本)中可以使用Unicode Script。
无论Unicode Property、Unicode Block、Unicode Script,其对应的排除型字符组都是将开头的\p改为\P,其它不变。
无论Unicode Property、Unicode Block、Unicode Script,都可以视为普通字符组,在字符组[…]中进行拼接组合,比如在PHP和Ruby中,[\p{Han}\p{Po}]既可以匹配汉字字符,又可以匹配任何标点字符。
分享到:
相关推荐
- **实战案例分析**:通过解决实际问题的例子,展示正则表达式的应用范围和灵活性。 - **跨语言对比**:比较不同编程语言中的正则表达式实现,帮助读者了解各自的特点和优势。 - **高级主题探索**:如反向引用、非...
正则表达式(Regular Expression,简称regex)是一种强大的文本处理工具,它用于匹配、查找、替换等操作,涉及字符串处理的...对于编程人员来说,熟练掌握正则表达式不仅可以提高工作效率,还能解决许多复杂的问题。
在IT领域,正则表达式(Regular Expression,简称regex)是一种强大的文本处理工具,它能够进行复杂的模式匹配、查找、替换等操作。在本话题中,我们将探讨如何使用PowerBuilder 11.5这一经典的开发环境来实现正则...
正则表达式是一种强大的文本处理工具,它用于搜索、替换、检查或解析文本。在Python中,正则表达式通过re模块实现,这个模块提供了丰富的函数和方法来处理正则表达式。本文将详细介绍如何在Python中使用正则表达式,...
正则表达式是一种强大的文本处理工具,它用于匹配字符串中的字符组合。虽然C语言标准库本身不直接支持正则表达式,但通过POSIX标准提供的正则表达式库(regex.h),我们可以在C程序中实现复杂的文本匹配和处理功能。...
正则表达式是处理文本数据的强大工具,它们允许我们匹配复杂的字符串模式。在Python中,re模块提供了正则表达式的支持,使得文本搜索和替换变得...随着你对正则表达式的深入学习,你将能够解决更复杂的文本处理问题。
该正则表达式用于匹配 Unicode 编码中的汉字范围,要求汉字在 Unicode 编码表中的范围内。 9. 匹配中文字符的正则表达式:[\u4e00-\u9fa5] 该正则表达式用于匹配中文字符,要求字符在 Unicode 编码表中的范围内。...
用JAVA写的一个将正则表达式转换为NFA的代码,基于Thompson算法的思想,递归构建NFA。jar为源码文件。 输出非确定有限自动状态机的有向图。如正则表达式: c(a|b)NFA为:0-c->1-ep->2-a->3-ep->7 ,0-c->1-ep->4-b->5-...
正则表达式是一种强大的文本处理工具,用于模式匹配、搜索、...这些正则表达式及其应用场景展示了在实际项目中如何高效地使用正则表达式来处理各种文本问题,无论是文本清洗、格式化还是数据验证,都是不可或缺的工具。
Java正则表达式:Pattern类和Matcher类
"电话 email 正则表达式 大全" 本节总结了电话、email 等的正则表达式,涵盖了手机号码、固定电话号码、email 地址、url、整数、浮点数、中文字符串等多种类型的正则表达式。 1. 电话号码正则表达式: 电话号码的...
《Java 基础之正则表达式:强大的文本匹配利器》
在VB.NET中,正则表达式(Regular Expression)是一种强大的文本处理工具,它允许程序员通过模式匹配来处理字符串。这个“vb正则表达式实例”很可能是为了帮助开发者测试和理解正则表达式的工作原理而设计的一个应用...
- **错误检查**:提供更好的错误检测机制,帮助开发者发现和修复正则表达式中的问题。 - **Unicode支持**:对Unicode字符集有良好的支持,能处理多种语言的文本。 3. **正则表达式语法** - **量词**:如`*`, `+`...
标题中的“pb 使用正则表达式源码pbregexp”指的是在PowerBuilder(简称pb)环境中,利用名为“pbregexp”的正则表达式组件来实现源代码级别的正则表达式操作。PowerBuilder是一款流行的可视化的、面向对象的软件...
正则表达式是一种强大的文本处理工具,用于在字符串中进行模式匹配和搜索。在C#编程语言中,正则表达式被广泛应用于数据验证、文本提取、格式转换等多个场景。本项目提供了一个C#编写的正则表达式测试工具,包含完整...
正则表达式语法:正则表达式主要语法详解+编程知识+技术开发; 正则表达式语法:正则表达式主要语法详解+编程知识+技术开发; 正则表达式语法:正则表达式主要语法详解+编程知识+技术开发; 正则表达式语法:正则...
正则表达式教程:30分钟让你精通正则表达式语法 _
《Java 基础之正则表达式:强大的文本匹配利器》 (1)