🦄 2024 独立开发者训练营,一起创业!(早鸟优惠在6天后结束)查看介绍 / 立即报名 →

Day 11:Ruby 正则表达式

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
微信好友

用微信扫描二维码,
加我好友。

微信公众号

用微信扫描二维码,
订阅宁皓网公众号。

240746680

用 QQ 扫描二维码,
加入宁皓网 QQ 群。

统计

14696
分钟
0
你学会了
0%
完成

社会化网络

关于

微信订阅号

扫描微信二维码关注宁皓网,每天进步一点