Ruby正则表达式学习记录

Ruby对正则表达式支持非常好(built-in support),下面我将近段时间学习的Ruby做个总结,包括

  • Ruby中正则表达式可以做什么
  • 正则表达式的用法,包括匹配的方法,替换,分组匹配等
  • 使用中需要注意的地方
  • 学习资源。

本文中,为了便于描述,将一个正则表达式称为一个模式(pattern)。

Ruby中正则表达式定义

就跟Ruby其他对象一样,正则表达式也是Ruby中的一个对象,是Regexp类的实例。因此它可以被当作参数进行传递,这更便于其在Ruby中的应用。

正则表达式的定义

主要有三种定义方式//, %r、Regexp.new()内。这三种效果相同,实质都是新建了一个Regexp的类,如定义/cat/, %r{cat},Regexp.new(“cat”),都是匹配包含cat的字符串,不过它们之间也有区别

  • 在//之间,要进行转义
  • 在%r{}内,不用进行转义
  • Regexp.new()内,不用进行转义

/mm\/dd/,Regexp.new(“mm/dd”),%r{mm/dd}三者效果相同,而且这三种定义方式都可以加入任意的Ruby的表达式(expressions)#{}

1
2
3
4
5
6
7
8
9
10
str1 = 1+2
str2 = "Hello"
str3 = `echo hello`
str4 = %x{echo hello}
exp1 = /#{str1}/
exp2 = %r{#{str2}}
exp3 = /#{str4}/
exp4 = Regexp.new("#{str3}")
p exp1, exp2, exp3, exp4, exp5
//All output => /3/ /hello/ /hello\n/ /hello\n/

在使用正则表达式时,主要是用//语法,因为它让我们很迅速的写出一个正则表达式。但如果某个模式中需要进行大量的转义,我们可以通过%r语法也可以给我们比较便利的定义一个正则表达式。而标准的Regexp.new的定义方法相对比较麻烦,不符合ruby简介方便的特点。

Regexp.new方法的参数,除可以传递字符串之外,可以传入Regexp对象(Regexp对象的options也会被一起传入),当然new方法中还包含其他参数,主要是指定他的options

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
p Regexp.new('dog', Regexp::EXTENDED) #=> /dog/x
p Regexp.new('cat', true) #=> /cat/i
p Regexp.new(r1) #=> /dog/x
p Regexp.new(r2) #=> /cat/i
output
/dog/x
/cat/i
/dog/x
/cat/i
#All options
p Regexp.new("abc", Regexp::IGNORECASE) #=> /abc/i
p Regexp.new("abc", Regexp::MULTILINE) #=> /abc/m
p Regexp.new("abc # Comment", Regexp::EXTENDED) #=> /abc # Comment/x #这里并不是注释,而是匹配包含“abc#Comment”的字符串
p Regexp.new("abc", Regexp::NOENCODING) #=> /abc/n
p Regexp.new("abc", Regexp::FIXEDENCODING) #=> /abc/
p Regexp.new("abc", Regexp::IGNORECASE | Regexp::MULTILINE) #=> /abc/mi

options选项是对正则表达式的功能的扩充,其中/i(IGNORECASE)代表忽略模式和待匹配字符串的大小写问题,/x(EXTENDED)代表忽略正则中的空格,因为复杂的正则表达式比较难阅读,我们可以通过空格和换行对其进行分割,而且在新行的尾端还可以进行注释#commnet,方便阅读。注意这里是忽略模式中空格,不是待匹配字符串中的空格。/m(MULTILINE)主要是可以使’.’匹配到’\n’,正常情况下,’.’可以匹配除’\n’外的所有字符. /o表示仅仅执行一次#{}。关于编码的问题,后续补充
如下:

1
2
3
4
5
6
7
8
9
p /Cat/ =~ "Oh.Cat No" #=>3
p /cat/ =~ "Oh.Cat No" #=>nil
p /cat/i =~ "Oh.Cat No" #=>3
p /Ca t/ =~ "Oh.Cat No" #=>nil
p /Ca t/x =~ "Oh.Cat No" #=>3
p /Ca t/x =~ "Oh.Ca t No" #=>nil 注意x不能忽略待测字符串中的空格
p /Cat/ =~ "Oh.\nCat No" #=>4 可以匹配包含\n的字符串
p /Cat/ =~ "Oh.C\nat No" #=>nil
p /C.at/m =~ "Oh.C\nat No" #=>3

options是对正则表达式功能的扩展,探其究竟,options选项实际是枚举类型,用一个二进制位代表一个options,因此可以通过|混合使用,比如“Regexp::IGNORECASE | Regexp::MULTILINE”,实际对应的就是/mi,其值为m与i对应值的和. /cat/默认情况options为nil,即/cat/.options 输出为0,Regexp.new方法中含有参数true对应Regexp::IGNORECASE,

1
2
3
4
5
6
7
8
9
10
11
Regexp::IGNORECASE #=> 1
Regexp::EXTENDED #=> 2
Regexp::MULTILINE #=> 4
/cat/.options #=> 0
/cat/ix.options #=> 3
Regexp.new('cat', true).options #=> 1
r = /cat/ix
Regexp.new(r.source, r.options) #=> /cat/ix #可以通过options值初始化一个Regexp
p Regexp.new(r.source, 3) #=> /cat/ix

Regexp中还包含其他API,比如判断二个exp是否相等,/abc/ == /abc/x,结果为false,=== 可以判断与字符串是否全匹配,输出false or true .

1
2
3
/^[a-z]*$/ === "HELLO" #=> false
/^[A-Z]*$/ === "HELLO" #=> true
/^[A-Z]*$/ === "HELLO1" #=> false

其他的API这里不做介绍,有兴趣的可以看这里 列出了所有Method方法。至此我们已经知道exp的定义方法,具体用法会在下文中描述。

正则表达式作用

  • 测试字符串是否符合某个模式
  • 对字符串根据模式进行拆分(提取字符串中符合模式的部分)
  • 根据模式匹配结果对字符串进行替换

Ruby中正则表达式用法

在上文中已经讲解了正则表达式的定义,本节主要说下正则表达的实际使用,即如何进行匹配及匹配返回结果如何使用。

正则表达式匹配结果

为了在下一小节“如何匹配”更好的理解,我把这一部分提前,不过基于第一部分的讲解,理解也没有难度。

=~

  • =~肯定匹配, !~否定匹配。=~表达式返回匹配到的位置索引,失败返回nil,所以我们可以把匹配结果当作if条件(在Ruby中if(0)被视为true,只有nil和false才当做失败,再一次说明Ruby对正则的支持),⚠️:符号左右内容可交换

  • 通常我们可以需要()进行分组,它会存储匹配的结果,变量的数量与圆括号的对数相等,在模式内,\1代表第一个园括号,\n对表第n个园括号,在模式外,通过$n可以获得第n个园括号的匹配结果。⚠️:括号嵌套的情况

  • 通过=~方法,我们只可以取得匹配的索引位置,对于”begin7catend” =~ /\d/,我们不能通过方便获得匹配到的结果(此处指7,假设我们后续需要继续使用它)”。虽然我们可以通过分组的方法获得(见上文),但对于简单的正则表达式而言,我们或许不需要使用分组方法,可通过系统变量$&获得匹配结果,除此之外, $`返回匹配前内容, $’ 返回匹配后内容,这样书写不便于阅读,因此match方法提供更友好的API名称

match方法

  • Ruby中正则表达式还给我们提供了match方法,便于将我们匹配的结果输出。

regexp#match(str),返回MatchData对象,一个数组,从0开始,长度为匹配的数量(注意含有圆括号的情况)。还有match.pre_match返回匹配前内容,match.post_match返回匹配后内容,如果匹配失败返回nil

  • 我们同样可以通过系统变量$&、$’,$`

String中的scan方法

regexp#match()只能匹配一次,如果想匹配所有要用可以使用String中的scan方法,它会返回Array,scan(pattern) → array 或者后接一个block:scan(pattern) {|match, …| block }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#获取匹配值
/cat/ =~ "dog and cat end"
p "#$`->#$&<- #$'" #=>"dog and ->cat<- end"
mt = /cat/.match("bigcatcomes")
p "#$`->#$&<-#$'" #=>"big->cat<-comes"
#=~中分组的匹配结果
idx1 = /(\d\d):(\d\d)(..)/ =~ "12:50am"
p idx1 # => 0
p "#$0" #=>"/Users/cooler/Hacker/ruby/test.rb"
p "Hour is #$1, minute #$2" # => "Hour is 12, minute 50"
p "#$4" #=> ""
#多添加一个((\d\d):(\d\d))注意$n的变化
p idx2 = /((\d\d):(\d\d))(..)/ =~ "12:50am" # => 0
p "#$0" #=>"/Users/cooler/Hacker/ruby/test.rb"
p "Time is #$1"# => "Time is 12:50"
p "Hour is #$2, minute #$3"
p "AM/PM is #$4" # => "AM/PM is am"
p "#$5" #=> ""
#match方法实现上面的匹配
md = /(\d\d):(\d\d)(..)/.match("12:50am") #md为一个MatchData对象
"Hour is #{md[1]}, minute #{md[2]}" # => "Hour is 12, minute 50"
md = /((\d\d):(\d\d))(..)/.match("12:50am")
"Time is #{md[1]}" # => "Time is 12:50"
"Hour is #{md[2]}, minute #{md[3]}" # => "Hour is 12, minute 50"
"AM/PM is #{md[4]}" # => "AM/PM is am"
p md[5] #=> nil
#scan方法
p "abcabcabz".scan(%r{abc}) #=>["abc", "abc"]
a = "cruel world"
a.scan(/\w+/) #=> ["cruel", "world"]
a.scan(/.../) #=> ["cru", "el ", "wor"]
a.scan(/(...)/) #=> [["cru"], ["el "], ["wor"]]
a.scan(/(..)(..)/) #=> [["cr", "ue"], ["l ", "wo"]]
a.scan(/\w+/) {|w| print "<<#{w}>> " } #=> <<cruel>> <<world>>
a.scan(/(.)(.)/) {|x,y| print y, x } #=> rceu lowlr

如何进行匹配

这部分不是讲匹配背后的原理(匹配原理,中文传送门),而是介绍如何高效正确地书写正则表达式。首先列一下常用reference。

几点说明:

  • 1、正则表达式支持Range,不仅可以利用模式中的Range,也可以利用字符串中Range,如代码所示,可以匹配输入行的开始和结尾
  • 2、如果要匹配下面的字符,需要进行转义 : | ( ) [ ] { } + \ ^ $ * ?
  • 3、可以对上表进行如下分类:

    • 锚点(Anchor)^(表示字符串开始)$\A\Z\z\B\b :\Z matches the end of a string unless the string ends with \n, in which case it matches just before the \n.
      • 字符匹配的类型:[]、[^]以及上图中第二列的部分,这里还有一个The POSIX character classes和字符编码的问题,稍后补齐
      • 重复匹配:主要是第三列,较高优先级/AB+/是匹配含有字符A后接不少于一个B的字符串(ABBBB),而不是匹配含有不少于一个(AB)的字符串(ABABAB)当然这就有一个匹配次数的问题,具体见下文的“贪婪匹配vs懒惰匹配”
      • 交替(Alternation):| 低优先级
      • 分组:(),分组为正则表达式提供更丰富的组合。关于分组的情况,在下面会有详细介绍。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#匹配固定模式的输入:以Start开始,以end结尾的字符串
while line = gets
puts line if line =~ /start/ .. line =~ /end/
end
#MatchData对象的API,见下一小节
def show_regexp(string, pattern)
match = pattern.match(string)
if match
"#{match.pre_match}->#{match[0]}<-#{match.post_match}"
else
"no match"
end
end
#锚点
str = "this is\nthe time"
show_regexp(str, /^the/) # => this is\n->the<- time
show_regexp(str, /is$/) # => this ->is<-\nthe time
show_regexp(str, /\Athis/) # => ->this<- is\nthe time
show_regexp(str, /\Athe/) # => no match
show_regexp("this is\nthe time", /\bis/) # => this ->is<-\nthe time
show_regexp("this is\nthe time", /\Bis/) # => th->is<- is\nthe time
#字符匹配的类型
show_regexp('Price $12.', /[^A-Z]/) # => P->r<-ice $12.
show_regexp('Price $12.', /[^\w]/) # => Price-> <-$12.
show_regexp('Price $12.', /[a-z][^a-z]/) # => Pric->e <-$12.
show_regexp('It costs $12.', /\s/) # => It-> <-costs $12.
show_regexp('It costs $12.', /\d/) # => It costs $->1<-2.
#重复匹配
a = "The moon is made of cheese"
show_regexp(a, /\w+/) # => ->The<- moon is made of cheese
show_regexp(a, /\s.*\s/) # => The-> moon is made of <-cheese
show_regexp(a, /\s.*?\s/) # => The-> moon <-is made of cheese
show_regexp(a, /[aeiou]{2,99}/) # => The m->oo<-n is made of cheese
show_regexp(a, /mo?o/) # => The ->moo<-n is made of cheese
# here's the lazy version
show_regexp(a, /mo??o/) # => The ->mo<-on is made of cheese
#重复具有较高优先级
# This matches an 'a' followed by one or more 'n's
show_regexp('banana', /an+/) # => b->an<-ana
# This matches the sequence 'an' one or more times
show_regexp('banana', /(an)+/) # => b->anan<-a
#Alter nation
show_regexp(“red ball blue sky”, /red ball|angry sky/) # => ->red ball<- blue sky
show_regexp(“red ball blue sky”, /red (ball|angry) sky/) # => no match

字符串的替换

  • 很多时候匹配是为了替换,Ruby中进行正则替换非常简单,两个方法即可搞定,sub()+gsub()。sub只替换第一次匹配,gsub(g:global)会替换所有的匹配,没有匹配到返回原字符串的copy
  • 如果想修改原始字符串用sub!()和gsub!(),没有匹配到返回nil。
  • sub或者gsub方法参数除了可以传入替换的字符串之外,还可以接block,对匹配的字符串进行block内的操作
  • 除此之外, 还有一种惯用的替换方法(Ruby 1.9以上), 通过hash方式替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#返回信值
a = "quick brown fox"
a.sub(/[aeiou]/, '*') # => "q*ick brown fox"
a.gsub(/[aeiou]/, '*') # => "q**ck br*wn f*x"
a.sub(/\s\S+/, '') # => "quick fox"
a.gsub(/\s\S+/, '') # => "quick"
a.sub(/\d/, '') # => "quick brown fox" (注意这里)
p a #=>"quick brown fox"
#原地替换
a.gsub!(/\s\S+/, '') # => "quick"
p a # => "quick"
a = "quick brown fox"
a.sub!(/\s\S+/, '') # => "quick fox"
p a # => "quick fox"
#后接Block
a = "quick brown fox"
a.sub(/^./) {|match| match.upcase } # => "Quick brown fox"
a.gsub(/[aeiou]/) {|vowel| vowel.upcase } # => "qUIck brOwn fOx"
#hash替换,更加直接
replacement = { "cat" => "feline", "dog" => "canine" } #不能用正则表达式
replacement.default = "unknown"
"cat and dog".gsub(/\w+/, replacement) # => "feline unknown canine"

分组匹配

  • 分组的一个很重要的应用是查找不同形式的重复,对于分组,我们会根据园括号对数进行编号,从1开始,通过\n就可以引用第n个括号所匹配的结果,从而达到匹配重复模式的效果,其中n的数量与括号的数量应该是一致的,大家可以试下引用\n+1的会出现什么情况,当然还有括号套括号的情况对于数字的变化,会更佳有益于对\n的理解。
  • 我们也可以给某个分组命令,通过名字而不是数字进行匹配,会更便于理解。命名方式如下:( ?pattern )引用方式为 \k (or \k’name’).
  • ⚠️当然使用=~进行匹配时,且模式在=~左边时,匹配名可以当做局部变量进行使用。
  • 命名匹配的方式同样适应于字符串的替换
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    # 理解\n的含义,
    p /(\w)(\w)\1\2/.match('He said "hehellllo"') #=>#<MatchData "hehe" 1:"h" 2:"e">
    p /(\w)(\w)\1\2/.match('He said "Hehellllo"') #=> #<MatchData "llll" 1:"l" 2:"l">
    # match duplicated letter
    show_regexp('He said "Hello"', /(\w)\1/) # => He said "He->ll<-o"
    # match duplicated substrings
    show_regexp('Mississippi', /(\w+)\1/) # => M->ississ<-ippi
    #using name
    # match duplicated letter
    str = 'He said "Hello"'
    show_regexp(str, /(?<char>\w)\k<char>/) # => He said "He->ll<-o"
    # match duplicated adjacent substrings
    str = 'Mississippi'
    show_regexp(str, /(?<seq>\w+)\k'seq'/) # => M->ississ<-ippi
    /(?<hour>\d\d):(?<min>\d\d)(..)/ =~ "12:50am" # => 0
    "Hour is #{hour}, minute #{min}" # => "Hour is 12, minute 50"
    # You can mix named and position-based references
    "Hour is #{hour}, minute #{$2}" # => "Hour is 12, minute 50"
    "Hour is #{$1}, minute #{min}" # => "Hour is 12, minute 50"
    puts "fred:smith".sub(/(\w+):(\w+)/, '\2, \1') #=>smith, fred
    puts "nercpyitno".gsub(/(?<c1>.)(?<c2>.)/, '\k<c2>\k<c1>') #=> encryption

贪婪匹配vs懒惰匹配

这两种匹配属于标准正则表达式内容,与Ruby没关,但新手如果不明白匹配时会发生莫名其妙的错误,所以特别总结一下。

  • 贪婪匹配:尽可能多匹配,正则默认是贪婪匹配。例子:a.*b它将会匹配最长的以a开始,以b结束的字符串。对于aabab的匹配结果是aabab。
  • 懒惰匹配:尽可能少匹配。例子:a.*?b对于aabab的匹配结果是aab和ab。
    一般是在原来表达式结尾加?就由贪婪匹配变成了懒惰匹配。常用的懒惰限定符有(去年最后的问题就是贪婪匹配):
    • ?重复任意次,但尽可能少重复
    • +?重复1次或更多次,但尽可能少重复
    • ??重复0次或1次,但尽可能少重复
    • {n,m}?重复n到m次,但尽可能少重复
    • {n,}?重复n次以上,但尽可能少重复

后续补充

1、 backtracking
2、Regular Expression Extensions
3、Backreferences and Named Matches

参考

http://ruby-doc.org/core-2.1.1/Regexp.html

http://ruby-doc.org/core-2.1.2/String.html#method-i-scan
regexp.rb

Programming Ruby 4th《第7章》