Ruby 正则表达式的字面构造器:
//
试一下:
>> //.class
=> Regexp
模式匹配有两个部分组成,一个正则表达式(regexp),还有一个字符串。正则表达式预测字符串,字符串要么满足预测,要么不满足。
看看是不是匹配可以使用 match 方法。做个实验:
>> puts "匹配!" if /abc/.match("the alphabet starts with abc.")
匹配!
=> nil
>> puts "匹配!" if "the alphabet starts with abc.".match(/abc/)
匹配!
=> nil
除了 match 方法,还有个模式匹配操作符 =~ ,把它放在字符串与正则表达式的中间用:
>> puts "匹配!" if /abc/ =~ "the alphabet starts with abc."
匹配!
=> nil
>> puts "匹配!" if "the alphabet starts with abc." =~ /abc/
匹配!
=> nil
没有匹配就会返回 nil 。有匹配的话,match 与 =~ 返回的东西不一样。 =~ 返回是匹配开始的字符的数字索引,match 返回的是 MatchData 类的一个实例。做个实验:
>> "the alphabet starts with abc" =~ /abc/
=> 25
>> /abc/.match("the alphabet starts with abc")
=> #<MatchData "abc">
匹配模式
// 中间的东西可不是字符串,它是你对字符串做的预测与限制。
字面字符
正则表达式里的字面字符匹配它自己,比如:
/a/
匹配的就是字母 a 。
有些字符有特别的意思,如果你不想让它表达特别的意思,可以使用 \ 线 escape 一下它:
/\?/
通配符
. 表示除了换行符以外的任意字符。
/h.t/
匹配 hot,hit ...
字符类
字符类会在一组方括号里:
/h[oi]t/
在上面的正则表达式里,那个字符类的意思是匹配 o 或 i 。也就是上面这个模式会匹配 “hot”,“hit”,但不匹配 “h!t” 或其它的东西。
下面这个字符类匹配小写字符 a 到 z :
/[a-z]/
^ 符号在字符类里表示否定:
/[^a-z]/
匹配十六进制数字,在字符类里可能得用几个字符范围:
/[A-Fa-f0-9]/
匹配数字 0 - 9:
/[0-9]/
0 - 9 太常用了,所以还有个简单的形式,d 表示 digit:
/\d/
数字,字符,还有下划线,w 表示 word:
/\w/
空白,比如空格,tab,还有换行符,s 表示 space:
/\s/
大写的表示否定形式:\D,\W,\S。
匹配
给你 yes/no 结果的匹配的操作:
regex.match(string)
string.match(regex)
子匹配
比如我们有行文字是关于一个人的:
Peel,Emma,Mrs.,talented amateur
我想得到人的 last name,还有 title。我们知道字段是用逗号分隔开的,我们也知道顺序:last name,first name,title,occupation 。
首先是一些字母字符,
然后是一个逗号,
然后又是一些字母字符,
接着还是一个逗号
然后是 Mr. 或 Mrs.
匹配上面这种字符器的模式:
/[A-Za-z]+,[A-Za-z]+,Mrs?\./
s? 意思是这个 s 可以有也可以没有。这样 Mrs? 也就会匹配 Mr 或 Mrs 。在 irb 上做个实验:
>> /[A-Za-z]+,[A-Za-z]+,Mrs?\./.match("Peel,Emma,Mrs.,talented amateur")
=> #<MatchData "Peel,Emma,Mrs.">
我们得到了一个 MatchData 对象。现在我们要 Pell 还有 Mrs 怎么办? 可以使用括号对匹配模式分组:
/([A-Za-z]+),[A-Za-z]+,(Mrs?\.)/
再试试这个匹配模式:
>> /([A-Za-z]+),[A-Za-z]+,(Mrs?\.)/.match("Peel,Emma,Mrs.,talented amateur")
=> #<MatchData "Peel,Emma,Mrs." 1:"Peel" 2:"Mrs.">
使用 $1 可以得到第一个分组里的匹配,$2 可以得到第二个分组里的匹配:
>> puts $1
Peel
=> nil
>> puts $2
Mrs.
=> nil
匹配成功与失败
没找到匹配,返回的值就是 nil,试试:
>> /a/.match("b")
=> nil
如果匹配成功会返回 MatchData 对象,它的布尔值是 true。还有些关于匹配的信息,比如匹配在哪里开始,覆盖了多少字符串,在分组里获得了什么等等。
想使用 MatchData 得先把它存储起来。练习一下:
string = "我的电话号码是 (123) 555-1234."
phone_re = /\((\d{3})\)\s+(\d{3})-(\d{4})/
m = phone_re.match(string)
unless m
puts "没有匹配"
exit
end
print "整个字符串:"
puts m.string
print "匹配:"
puts m[0]
puts "三个分组:"
3.times do |index|
puts "#{index + 1}:#{m.captures[index]}"
end
puts "得到第一个分组匹配的内容:"
puts m[1]
结果是:
整个字符串:我的电话号码是 (123) 555-1234.
匹配:(123) 555-1234
三个分组:
1:123
2:555
3:1234
得到第一个分组匹配的内容:
123
得到捕获的两种方法
从 MatchData 对象里得到匹配模式分组捕获到的内容:
m[1]
m[2]
...
m[0] 得到的是匹配的全部内容。
另一种得到分组捕获内容的方法是使用 captures 方法,它返回的是一个数组,数组里的项目就是捕获的子字符串。
m[1] == m.captures[0]
m[2] == m.captures[1]
再看个例子:
>> /((a)((b)c))/.match("abc")
=> #<MatchData "abc" 1:"abc" 2:"a" 3:"bc" 4:"b">
命名捕获
>> re = /(?<first>\w+)\s+((?<middle>\w\.)\s+)?(?<last>\w+)/
匹配:
>> m = re.match("Samuel L. Jackson")
=> #<MatchData "Samuel L. Jackson" first:"Samuel" middle:"L." last:"Jackson">
>> m[:first]
=> "Samuel"
>> m[:last]
=> "Jackson"
MatchData 的其它信息
接着之前的电话号码的例子:
print "匹配之前的部分:"
puts m.pre_match
print "匹配之后的部分:"
puts m.post_match
print "第二个捕获开始字符:"
puts m.begin(2)
print "第三个捕获结束字符:"
puts m.end(3)
输出的结果是:
匹配之前的部分:我的电话号码是
匹配之后的部分:.
第二个捕获开始字符:14
第三个捕获结束字符:22
begin 与 end 方法待验证。
Quantifiers,Anchors,Modifiers
Quantifiers(限定符),Anchors(标记),Modifiers(修饰符)。
限定符
限定符可以指定在匹配里某个东西要匹配的次数。
零或一
?
例:
/Mrs?/
s 可以出现零次或一次。
零或多
*
例:
/\d*/
一或多
+
例:
/\d+/
Greedy quantifier
*与+ 这两个限定符都很 greedy。意思就是它们会尽可能的匹配更多的字符。
观察下面这个例子里的 .+ 匹配的是什么:
>> string = "abc!def!ghi!"
=> "abc!def!ghi!"
>> match = /.+!/.match(string)
=> #<MatchData "abc!def!ghi!">
>> puts match[0]
abc!def!ghi!
=> nil
你可能期望返回的是子字符 "abc!" ,不过我们得到的是 "abc!def!ghi!"。限定符 + 贪婪的吃掉了它能覆盖的所有的字符,一直到最后一个 ! 号结束。
我们可以在 + 与 * 后面添加一个 ? 号,让它们不那么贪婪。再试一下:
>> string = "abc!def!ghi!"
=> "abc!def!ghi!"
>> match = /.+?!/.match(string)
=> #<MatchData "abc!">
>> puts match[0]
abc!
=> nil
再做个实验:
>> /(\d+?)/.match("Digits-R-Us 2345")
=> #<MatchData "2" 1:"2">
>> puts $1
2
=> nil
再看个匹配:
>> /\d+5/.match("Digits-R-Us 2345")
=> #<MatchData "2345">
这样再试一下:
>> /(\d+)(5)/.match("Digits-R-Us 2345")
=> #<MatchData "2345" 1:"234" 2:"5">
具体重复的次数
把次数放到 {} 里。下面匹配的是三个数字,小横线,接着是四个数字:
/\d{3}-\d{4}/
也可能是一个范围,下面匹配的是 1 到 10 个数字:
/\d{1,10}/
大括号里第一个数字是最小值,下面匹配的是 3 个或更多的数字:
/\d{3,}/
括号的限制
>> /([A-Z]){5}/.match("Matt DAMON")
=> #<MatchData "DAM" 1:"N">
你期望的匹配可能是 DAMON,但实际匹配的是 N 。如果你想匹配 DAMON ,需要这样做:
>> /([A-Z]{5})/.match("Matt DAMON")
=> #<MatchData "DAMON" 1:"DAMON">
anchors 与 assertions
anchors(标记,锚) 与 assertions(断言):在处理字符匹配之前先要满足一些条件。
^ 表示行的开始,$ 行的结尾。
Ruby 里的注释是 # 号开头的,匹配它的模式可以像这样:
/^\s*#/
^ 匹配的是行的最开始。
anchors
^:行的开始
$:行的结尾
\A:字符串的开始
\z:字符串的结尾
\Z:字符串的结尾,模式:/from the earth.\Z/,匹配:"from the earth\n"
\b:字边界
lookahead assertions
你想匹配一组数字,它的结尾必须有点,但你不想在匹配的内容里包含这个点,可以这样做:
>> str = "123 456. 789"
=> "123 456. 789"
>> m = /\d+(?=\.)/.match(str)
=> #<MatchData "456">
lookbehind assertions
我要匹配 Damon,但必须它的前面得有 matt。
模式:
/(?<=Matt )Damon/
试一下:
>> /(?<=Matt )Damon/.match("Matt Damon")
=> #<MatchData "Damon">
>> /(?<=Matt )Damon/.match("Matt1 Damon")
=> nil
我要匹配 Damon,但它的前面不能是 matt 。
模式:
/(?<!Matt )Damon/
试一下:
>> /(?<!Matt )Damon/.match("Matt Damon")
=> nil
>> /(?<!Matt )Damon/.match("Matt1 Damon")
=> #<MatchData "Damon">
不捕获
使用:?:
>> str = "abc def ghi"
=> "abc def ghi"
>> m = /(abc) (?:def) (ghi)/.match(str)
=> #<MatchData "abc def ghi" 1:"abc" 2:"ghi">
条件匹配
条件表达式:(?(1)b|c) ,如果获取到了 $1,就匹配 b ,不然就匹配的是 c :
>> re = /(a)?(?(1)b|c)/
=> /(a)?(?(1)b|c)/
>> re.match("ab")
=> #<MatchData "ab" 1:"a">
>> re.match("b")
=> nil
>> re.match("c")
=> #<MatchData "c" 1:nil>
有名字的:
/(?<first>a)?(?(<first>)b|c)/
modifiers
i 这个修饰符表示不区分大小写:
/abc/i
m 表示多行:
/abc/m
x 可以改变正则表达式解析器对待空格的看法,它会忽略掉在正则表达式里的空格,除了你用 \ 符号 escape 的空白。
/
\((\d{3})\) # 3 digits inside literal parens (area code)
\s # One space character
(\d{3}) # 3 digits (exchange)
- # Hyphen
(\d{4}) # 4 digits (second part of number
/x
转换字符串与正则表达式
string-to-regexp
在正则表达式里使用插值:
>> str = "def"
=> "def"
>> /abc#{str}/
=> /abcdef/
如果字符串里包含在正则表达式里有特别意义的字符,比如点(.):
>> str = "a.c"
=> "a.c"
>> re = /#{str}/
=> /a.c/
>> re.match("a.c")
=> #<MatchData "a.c">
>> re.match("abc")
=> #<MatchData "abc">
你可以 escape 这些特殊的字符:
>> Regexp.escape("a.c")
=> "a\\.c"
>> Regexp.escape("^abc")
=> "\\^abc"
这样再试试:
>> str = "a.c"
=> "a.c"
>> re = /#{Regexp.escape(str)}/
=> /a\.c/
>> re.match("a.c")
=> #<MatchData "a.c">
>> re.match("abc")
=> nil
也可以:
>> Regexp.new('(.*)\s+Black')
=> /(.*)\s+Black/
这样也行:
>> Regexp.new('Mr\. David Black')
=> /Mr\. David Black/
>> Regexp.new(Regexp.escape("Mr. David Black"))
=> /Mr\.\ David\ Black/
regexp-to-string
正则表达式可以使用字符串的形式表示它自己:
>> puts /abc/
(?-mix:abc)
inspect:
>> /abc/.inspect
=> "/abc/"
使用正则表达式的方法
Ruby 里的一些方法可以使用正则表达式作为它们的参数。
比如在一个数组里,你想找出字符长度大于 10 ,并且包含一个数字的项目:
array.find_all {|e| e.size > 10 and /\d/.match(e) }
String#scan
找到一个字符串里包含的所有的数字:
>> "testing 1 2 3 testing 4 5 6".scan(/\d/)
=> ["1", "2", "3", "4", "5", "6"]
分组:
>> str = "Leopold Auer was the teacher of Jascha Heifetz."
=> "Leopold Auer was the teacher of Jascha Heifetz."
>> violinists = str.scan(/([A-Z]\w+)\s+([A-Z]\w+)/)
=> [["Leopold", "Auer"], ["Jascha", "Heifetz"]]
可以这样用:
violinists.each do |fname,lname|
puts "#{lname}'s first name was #{fname}."
end
输出的是:
Auer's first name was Leopold.
Heifetz's first name was Jascha.
合并到一块儿:
str.scan(/([A-Z]\w+)\s+([A-Z]\w+)/) do |fname, lname|
puts "#{lname}'s first name was #{fname}."
end
再做个实验:
"one two three".scan(/\w+/) {|n| puts "Next number: #{n}" }
输出的是:
Next number: one
Next number: two
Next number: three
如果你提供了一个代码块,scan 不会存储把结果存储到一个数组里,它会把每个结果都发送给代码块,然后扔掉结果。也就是你可以 scan 一个很长的东西,不用太担心内存的问题。
StringScanner
StringScanner 在 strscan 扩展里,它里面提供了一些扫描与检查字符串的工具。可以使用位置与指针移动。
>> require 'strscan'
=> true
>> ss = StringScanner.new("Testing string scanning")
=> #<StringScanner 0/23 @ "Testi...">
>> ss.scan_until(/ing/)
=> "Testing"
>> ss.pos
=> 7
>> ss.peek(7)
=> " string"
>> ss.unscan
=> #<StringScanner 0/23 @ "Testi...">
>> ss.pos
=> 0
>> ss.skip(/Test/)
=> 4
>> ss.rest
=> "ing string scanning"
String#split
split 可以把一个字符串分离成多个子字符串,返回的这些子字符串会在一个数组里。split 可以使用正则表达式或者纯文字作为分隔符。
试一下:
>> "Ruby".split(//)
=> ["R", "u", "b", "y"]
把一个基于文字的配置文件的内容转换成 Ruby 的数据结构。
>> line = "first_name=matt;last_name=damon;country=usa"
=> "first_name=matt;last_name=damon;country=usa"
>> record = line.split(/=|;/)
=> ["first_name", "matt", "last_name", "damon", "country", "usa"]
hash:
>> data = []
=> []
>> record = Hash[*line.split(/=|;/)]
=> {"first_name"=>"matt", "last_name"=>"damon", "country"=>"usa"}
>> data.push(record)
=> [{"first_name"=>"matt", "last_name"=>"damon", "country"=>"usa"}]
split 的第二个参数,可以设置返回的项目数:
>> "a,b,c,d,e".split(/,/,3)
=> ["a", "b", "c,d,e"]
sub/sub! 与 gsub/gsub!
sub 与 gsub,可以修改字符串里的内容。gsub 修改整个字符串,sub 最多只修改一个地方。
sub
>> "hit hit".sub(/i/,"o")
=> "hot hit"
代码块:
>> "hit".sub(/i/) {|s| s.upcase}
=> "hIt"
gsub
>> "hit hit".gsub(/i/,"o")
=> "hot hot"
捕获
>> "oHt".sub(/([a-z])([A-Z])/, '\2\1')
=> "Hot"
>> "double every word".gsub(/\b(\w+)/, '\1 \1')
=> "double double every every word word"
=== 与 grep
===
所有的 Ruby 对象都认识 === 这个信息,如果你没覆盖它的话,它跟 == 是一样的。如果你覆盖了 === ,那它的功能就是新的意思了。
在正则表达式里,=== 的意思是匹配的测试。
puts "Match!" if re.match(string)
puts "Match!" if string =~ re
puts "Match!" if re === string
试一下:
print "Continue? (y/n) "
answer = gets
case answer
when /^y/i
puts "Great!"
when /^n/i
puts "Bye!"
exit
else
puts "Huh?"
end
grep
>> ["USA", "UK", "France", "Germany"].grep(/[a-z]/)
=> ["France", "Germany"]
select 也可以:
["USA", "UK", "France", "Germany"].select {|c| /[a-z]/ === c }
代码块:
>> ["USA", "UK", "France", "Germany"].grep(/[a-z]/) {|c| c.upcase }
=> ["FRANCE", "GERMANY"]