Ruby
- 1、预备知识
- 2、正则功能详解
- 2.1、列表
- 2.2、字符组
- 2.3、Unicode属性
- 2.4、字符组简记法
- 2.5、单词边界
- 2.6、行起始/结束位置
- 2.7、环视
- 2.8、匹配模式
- 2.9、捕获分组的引用
- 3、正则API简介
- 3.1、Regexp
- 3.2、Regexp.match(text)
- 3.3、Regexp.quote(text)和Regexp.escape(text)
- 3.4、String.index(Regexp)
- 3.5、String.scan(Regexp)
- 3.6、String.slice(Regexp)
- 3.7、String.split(Regexp)
- 3.8、String.sub(Regexp,Str)
- 3.9、String.gsub(Regexp,String)
- 4、常用操作示例
- 4.1、验证
- 4.2、提取
- 4.3、替换
- 4.4、切分
- 5、Ruby 1.9的新变化
Ruby内建了对正则表达式的支持,提供了相对完备的正则表达式功能。但Ruby 1.8的正则表达式也存在不少限制,最突出的是不支持逆序环视。Ruby 1.9采用全新的Oniguruma(鬼车)正则引擎,补充了包括逆序环视在内的许多功能,最重要的是全面支持Unicode(但是也造成了一些奇怪的现象,下面你会看到)。不过考虑到目前Ruby 1.9并没有完全取代Ruby 1.8,所以我们会讲解Ruby 1.8和Ruby 1.9的差异,末尾详细说明了Ruby 1.9中正则表达式的功能变化。
1、预备知识
Ruby的正则表达式对象是Regexp。最常见的生成方法类似/regex/,即在首尾两个斜线/之间,用正则文字给出正则表达式,比如/\d+/。
为继续讲解方便,先介绍进行正则表达式匹配的方法:Ruby中的正则匹配非常简单,直接使用操作符=~连接正则表达式与字符串即可。如果可以匹配,则返回第一次匹配发生的起始位置,否则返回nil。请注意,正则表达式和字符串的左右顺序并不影响结果。
Ruby中有两种字符串:双引号字符串和单引号字符串。两者的主要区别在于:双引号字符串需要进行字符串转义处理,而单引号字符串不需要进行字符串转义处理。所以如果要作为正则表达式的字符串,最好使用单引号字符串,这样就可以直接使用\d,而不用写成\d;作为普通文本的字符串,则最好使用双引号字符串。
2、正则功能详解
2.1、列表
下表列出了Ruby的正则功能。
2.2、字符组
Ruby中的正则引擎可以正确识别ASCII编码,但无法正确识别GBK编码,因此无法避免GBK编码的“错误匹配”问题。
解决的办法是使用Unicode编码环境,指定程序使用Unicode编码的办法是在源文件的顶部写上这样一行:
Ruby 1.9之后版本的正则表达式才提供了相对完备的Unicode支持,如果你使用的是Ruby 1.8,正则表达式中又有非ASCII字符,则最好在表达式末尾写上u显式指定Unicode模式(Ruby1.9中,正则表达式默认使用Unicode编码,所以不用显式指定)。
解决的办法是使用Unicode编码环境,指定程序使用Unicode编码的办法是在源文件的顶部写上这样一行:
Ruby 1.9之后版本的正则表达式才提供了相对完备的Unicode支持,如果你使用的是Ruby 1.8,正则表达式中又有非ASCII字符,则最好在表达式末尾写上u显式指定Unicode模式(Ruby1.9中,正则表达式默认使用Unicode编码,所以不用显式指定)。
另外要注意的是,在Ruby 1.8中,即便指定程序使用Unicode编码,匹配位置仍然是按照字节计算的,Ruby1.9中则是按照字符计算的。
在Ruby中可以用字符组[\u{4E00}-\u{9FFF}]匹配任意中文字符,需要注意的是,这种写法只有Ruby 1.9以上版本才支持,并且使用时必须显式指定Unicode模式。
Ruby支持POSIX字符组。在Ruby 1.8中,POSIX字符组只能匹配ASCII字符(指定Unicode模式也不行),而且不可使用[:ascii:]和[:word:];而在Ruby 1.9中,POSIX字符组可以匹配Unicode字符,同时支持[:ascii:]和[:word:]。
2.3、Unicode属性
Ruby 1.9支持使用Unicode Script,只是使用UnicodeScript时,必须显式指定u模式,否则会报错。所以,如果要匹配中文字符,则可以使用\p{Han}。
2.4、字符组简记法
Ruby的字符组简记法都采用ASCII编码匹配规则。如果指 定了Unicode模式,则会换用Unicode匹配规则。
2.5、单词边界
在Ruby 1.8中,如果没有指定Unicode模式,\w采用ASCII匹配规则,单词边界\b也采用这种匹配规则。
如果指定了Unicode模式,则\b也采用Unicode匹配规则。
Ruby 1.9中的\w虽然采用ASCII匹配规则,但\b的表现与Ruby 1.8中指定Unicode方式一样,这一点值得特别注意。
2.6、行起始/结束位置
Ruby默认采用多行模式,所以^不但可以匹配整个字符串的开始位置,还可以匹配字符串内部的文本行的起始位置;而\A无论在什么情况下都匹配整个字符串的起始位置。
同样,因为默认采用多行模式,所以$可以匹配文本内部行的结束位置,\Z、\z匹配的是整个字符串的结束位置(如果结束位置有换行符,则\Z匹配这个换行符之前的位置)。
重复一次,Ruby的m模式不等于其他语言中的多行模式,而是等于通常的单行模式,它只影响点号.的匹配。
2.7、环视
在Ruby中的肯定顺序环视(?=…)和否定顺序环视(?!…)内可以使用任意形式的子表达式。
对逆序环视的支持则不那么好:Ruby 1.8完全不支持逆序环视;Ruby 1.9支持逆序环视,但是逆序环视中的表达式匹配的文本长度必须是固定的,完全不能使用+、*、?之类的量词:如果使用了多选结构,只能出现竖线,比如regex1|regex2,而不能出现括号,比如(regex1|regex2)。
2.8、匹配模式
下表列出了Ruby的正则表达式支持的匹配模式。
匹配模式可以在表达式中以(?modifier)指定,或者在结束分隔符之后写上模式对应的修饰符,也可以在生成Regexp对象时,通过预定义常量作为参数指定。
Ruby支持用(?-modifier)停用某个模式。
2.9、捕获分组的引用
在Ruby中,如果要在正则表达式内部引用捕获分组,则应当使用\num记法,其中num为对应捕获分组的编号。
如果要在替换时引用捕获分组,则应当使用$num记法。
如果使用了命名分组,在表达式内部则应当使用\k<name>来引用。
在replacement字符串中同样使用\k<name>来引用。
3、正则API简介
3.1、Regexp
Ruby中的正则表达式对象是Regexp,一般使用/regex/来生成。如果习惯更为“正统”的模式,也可以用Regexp.new()来生成,参数是表达式对应的字符串。
之前讲过,Ruby中有两种字符串,其中双引号字符串需要进行字符串转义处理,而单引号字符串不需要进行字符串转义处理。所以,作为正则表达式的字符串,最好使用单引号字符串。
如果要指定匹配模式,可以添加第2个参数(虽然实际参数是一个整数,但是Ruby其实有3个预定义常量,即Regexp::IGNORECASE、Regexp::EXTENDED、Regexp::MULTILINE)。
Ruby中也可以混用多个模式,用|连接预定义常量。
Regexp.new()的第1个参数也可以不用字符串,而是用类似/regex/的形式。
如果使用这种形式,那么有三种形式指定匹配模式,下面各举一个例子,其结果相同。
另外还有Regexp.compile()方法,它与Regexp.new()完全一致。
值得注意的还有Regexp对象的编码。在Ruby 1.9中,Regexp对象自身是具有编码的,在默认情况下,如果正则表达式中出现的都是ASCII字符,且程序采用了兼容ASCII的编码,则正则表达式默认采用ASCII编码;否则,采用与程序相同的编码。调用Regexp.encoding?可以得到编码信息。
要正确使用具有编码属性的正则表达式,只有两种情况:Regexp与字符串使用相同的编码;或者Regexp使用ASCII编码,而字符串使用兼容ASCII的编码,否则会报错。
如果不确定使用的Regexp对象的编码是否兼容ASCII,可以使用fix_encoding?来判断。不兼容的情况下会返回true;如果一个Regexp对象的编码兼容ASCII,它可以尝试匹配任何使用了ASCII编码的字符串,而不会报错。
3.2、Regexp.match(text)
之前看到的匹配都是依靠操作符=~进行的,许多读者可能不熟悉,所以这里先详细介绍它。=~用来执行正则表达式和字符串的匹配,返回匹配成功的起始位置;如果匹配不成功,则返回nil。因此,可以用这个办法来进行数据验证(验证数据时,别忘了在正则表达式两端加上\A和\z)。
如果匹配成功,会生成一个MatchData对象,通过Regexp.last_match可以获得它,其中包含了匹配的详细信息。
也可以不用显式通过MatchData对象来获得这些信息,Ruby提供了若干特殊变量,使用它们同样也可以达到目的。下表列出了常用的特殊变量。
Regexp.match(text)方法与=~差不多,主要的区别在于,它返回的并不是匹配的起始位置,而是一个MatchData对象,如果匹配不成功,则返回nil。所以,上面的代码可以这样获得MatchData。
Regex.match(string)与=~的另一点不同是,它可以通过可选参数设定偏移值,设定尝试匹配的开始位置,这样就可以在字符串中逐次找到匹配。在下面的例子中,每次匹配成功之后,记录下匹配的结束位置,作为下次开始尝试的起点,就实现了逐次匹配。
3.3、Regexp.quote(text)和Regexp.escape(text)
这两个方法是等价的,它们消除text中可能出现的任何元字符的特殊含义。
3.4、String.index(Regexp)
这个方法默认等价于=~操作,如果匹配成功,则返回匹配开始的位置,否则返回nil。
如果不指定第2个参数,它等价于Regex.match()或者=~。
它也可以指定第2个参数,设定开始尝试匹配的偏移值。此时可以遍历文本,获得逐次匹配的结果。
3.5、String.scan(Regexp)
=~操作符和Regexp.match(string)方法只能进行单次匹配,如果希望一次性找到某个正则表达式在字符串中的所有匹配,就应该使用String.scan(Regexp),它返回各次匹配所对应文本组成的数组。
如果正则表达式包含捕获分组,则返回数组的元素本身也是数组。
请注意,这时候并不会返回整个表达式匹配的文本(也就是编号为0的捕获分组捕获的文本),如果需要得到整个表达式匹配的文本,可以给整个表达式首尾加上括号。
也可以用String.scan()进行更复杂的字符串处理,方法是在括号后面加上{|match,…|block},其中match,…表示各捕获分组匹配的文本,而block则是对这些文本进行处理的程序代码,比如下面这样。
3.6、String.slice(Regexp)
这个方法返回Regexp在String中能匹配的文本。
也可以设定第2个参数,指定对应捕获分组的编号,返回对应的捕获分组匹配的文本。
如果正则表达式可以在文本中找到多次匹配,那么String.slice(Regexp)只返回第一次匹配的结果。
如果将String.slice(Regexp)写成String.slice!(Regexp),则String自身也受到影响:在返回匹配文本的同时,还会从原字符串中删除匹配的结果。
3.7、String.split(Regexp)
这个方法用正则表达式来切分字符串,它返回一个数组,其中的元素是切分之后的文本。
如果切分的末尾留下空字符串,在默认情况下会忽略它,而字符串开头的空字符串不会忽略。
这个方法也可以指定第2个参数,它是一个整数,假设为n,按照n取值的不同,有几种情况,以下分别举例说明。
3.8、String.sub(Regexp,Str)
String.sub(Regexp,Str)用来执行正则表达式替换,Regexp是匹配想要替换文本的表达式,而Str是希望更新的字符串,调用之后只会对Regexp第一次能匹配的文本进行替换。
如果第1个参数不是Regexp而是普通字符串,则进行普通字符串替换。
也可以在String参数中,用\num引用Regexp对应捕获分组所匹配的文本。
如果需要进行更灵活的替换,则可以使用String.sub(Regexp){| match | block}方式,其中的match是Regexp所匹配的文本,即一个String对象,我们可以写一段代码作为block,进行更复杂的处理,比如将单词转换为大写。
如果需要在block中获得其他匹配的内容,可以使用之前讲过的$1、$2、$`等特殊变量。
String.sub(Regexp,String)方法并不会修改原来的字符串,它会先复制原字符串,对其进行修改之后返回。如果String对象包含的内容很多,复制可能比较消耗资源,因此Ruby也提供了直接对原字符串生效的方法String.sub!(Regexp,String)。
如果表达式在String中能多次匹配,String.sub()只会对第一次匹配进行替换;如果需要对所有匹配都进行替换,则需要使用String.gsub(Regexp,String)。下面来看这个方法。
3.9、String.gsub(Regexp,String)
这个方法在使用上完全等价于String.sub(Regexp,String),唯一不同的是它会对每次匹配都进行替换。
相应的,String.gsub!(Regexp,String)也会修改原来的字符串。
4、常用操作示例
4.1、验证
4.2、提取
逐步提取:
一次性提取:
4.3、替换
简单替换:
在replacement中使用整个表达式匹配的文本:
4.4、切分
5、Ruby 1.9的新变化
Ruby 1.8的正则表达式还不够完善,许多常用的特性不能支持,而Ruby 1.9采用了全新的Oniguruma(鬼车)引擎,对正则表达式的支持则强大了很多。下面列出Ruby1.9中正则表达式的主要变化,更详细的信息请参考http://oniguruma.rubyforge.org/oniguruma/。
新增了合并多个表达式的方法
这个方法返回的仍然是一个Regexp,它“合拢”了参数中所有的Regexp,任何一段文本,只要能被其中某个表达式匹配,就能被Regexp匹配。
关于这个方法,有一个常见的误区:在数据验证时,想当然地将各条件对应的表达式用Regexp.union()“归并”,但如果这些条件必须同时满足,就很可能出错。
比如需要验证的字符串有这样两条要求:第一,全部由数字或字母组成;第二,长度在6~12之间。如果想当然地用Regexp.union(),就会出错。
这是因为Regexp.union()“归并”的表达式并不要求“同时生效”,只要其中有一条能匹配,则匹配成功。此处字符串0123可以由表达式^[a-zA-Z0-9]*$匹配,所以返回结果0。
新增了逆序环视
新增了忽略优先量词
新增了字符组中的集合运算操作符&&
新增了指定Unicode码值的表示法
如果指定了多字节编码,十进制和十六进制的数字序列可以用来表示单个多字节字符,因此可以在字符组中指定多字节字符的范围。
如果不知道自己使用的Ruby的版本,或者不确认其使用了Oniguruma引擎,则可以使用下面的函数来判断。
如果希望无缝兼容Ruby 1.8和1.9,不妨自己包装一个方法,根据要处理的字符串来生成对应编码的Regexp。