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"]Ruby