MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Ruby 正则表达式应用攻略

2022-11-124.2k 阅读

正则表达式基础

在 Ruby 中,正则表达式是一种强大的工具,用于在字符串中匹配、搜索和替换文本模式。正则表达式由普通字符(例如字母、数字和标点符号)和特殊字符(称为元字符)组成。这些元字符赋予了正则表达式强大的模式匹配能力。

字符匹配

  1. 普通字符:最简单的正则表达式就是由普通字符组成,它们匹配自身。例如,正则表达式 ruby 会匹配字符串中出现的 “ruby”。
string = "I love ruby"
if string =~ /ruby/
  puts "匹配成功"
else
  puts "匹配失败"
end

在上述代码中,=~ 操作符用于在字符串中搜索正则表达式。如果找到匹配项,它返回匹配的起始位置;如果没有找到,返回 -1

  1. 字符类:字符类允许匹配一组字符中的任意一个。例如,[aeiou] 匹配任何一个元音字母。
string = "apple"
if string =~ /[aeiou]/
  puts "包含元音字母"
else
  puts "不包含元音字母"
end

方括号内的字符是或的关系,只要字符串中包含其中任意一个字符就算匹配成功。

  1. 否定字符类:使用 ^ 作为字符类的第一个字符,就可以创建否定字符类。例如,[^aeiou] 匹配任何一个非元音字母。
string = "fly"
if string =~ /[^aeiou]/
  puts "包含非元音字母"
else
  puts "不包含非元音字母"
end

重复匹配

  1. * 元字符:表示前面的字符可以出现 0 次或多次。例如,ab* 可以匹配 “a”、“ab”、“abb” 等。
strings = ["a", "ab", "abb"]
strings.each do |str|
  if str =~ /ab*/
    puts "#{str} 匹配成功"
  else
    puts "#{str} 匹配失败"
  end
end
  1. + 元字符:表示前面的字符可以出现 1 次或多次。例如,ab+ 可以匹配 “ab”、“abb”,但不能匹配 “a”。
strings = ["a", "ab", "abb"]
strings.each do |str|
  if str =~ /ab+/
    puts "#{str} 匹配成功"
  else
    puts "#{str} 匹配失败"
  end
end
  1. ? 元字符:表示前面的字符可以出现 0 次或 1 次。例如,ab? 可以匹配 “a” 或 “ab”。
strings = ["a", "ab"]
strings.each do |str|
  if str =~ /ab?/
    puts "#{str} 匹配成功"
  else
    puts "#{str} 匹配失败"
  end
end
  1. 重复次数限定:可以使用 {n} 表示前面的字符恰好出现 n 次,{n,} 表示前面的字符至少出现 n 次,{n,m} 表示前面的字符出现 n 到 m 次。例如,a{3} 匹配 “aaa”,a{3,} 匹配 “aaa” 及更多个 “a”,a{3,5} 匹配 “aaa”、“aaaa”、“aaaaa”。
strings = ["aa", "aaa", "aaaa"]
strings.each do |str|
  if str =~ /a{3,}/
    puts "#{str} 匹配成功"
  else
    puts "#{str} 匹配失败"
  end
end

边界匹配

  1. ^ 元字符:在正则表达式的开头使用 ^ 表示匹配字符串的开头。例如,^ruby 只会匹配以 “ruby” 开头的字符串。
strings = ["ruby is great", "I love ruby"]
strings.each do |str|
  if str =~ /^ruby/
    puts "#{str} 匹配成功"
  else
    puts "#{str} 匹配失败"
  end
end
  1. $ 元字符:在正则表达式的结尾使用 $ 表示匹配字符串的结尾。例如,ruby$ 只会匹配以 “ruby” 结尾的字符串。
strings = ["I love ruby", "ruby is great"]
strings.each do |str|
  if str =~ /ruby$/
    puts "#{str} 匹配成功"
  else
    puts "#{str} 匹配失败"
  end
end
  1. \b 元字符:表示单词边界。例如,\bruby\b 只会匹配作为独立单词的 “ruby”,而不会匹配 “rubber” 中的 “ruby”。
strings = ["ruby", "rubber"]
strings.each do |str|
  if str =~ /\bruby\b/
    puts "#{str} 匹配成功"
  else
    puts "#{str} 匹配失败"
  end
end

Ruby 中的正则表达式对象

在 Ruby 中,正则表达式可以用两种方式表示:字面量形式(例如 /pattern/)和 Regexp 类的实例。

字面量形式

正则表达式字面量是由斜杠(/)包围的模式。例如:

regex = /ruby/
string = "I love ruby"
if string =~ regex
  puts "匹配成功"
end

这种方式简洁明了,适用于大多数简单的正则表达式场景。

Regexp 类

Regexp 类提供了更灵活的方式来创建和操作正则表达式。可以使用 Regexp.new 方法来创建一个 Regexp 对象。

pattern = "ruby"
regex = Regexp.new(pattern)
string = "I love ruby"
if string =~ regex
  puts "匹配成功"
end

Regexp 类还支持一些选项,例如忽略大小写(i)、多行匹配(m)等。

regex = Regexp.new("ruby", Regexp::IGNORECASE)
string = "I love Ruby"
if string =~ regex
  puts "匹配成功"
end

上述代码中,Regexp::IGNORECASE 选项使得正则表达式在匹配时忽略大小写。

捕获组

捕获组是正则表达式中用圆括号括起来的部分。它们允许我们在匹配成功后提取出特定的子字符串。

基本捕获组

例如,正则表达式 (\d{3})-(\d{2})-(\d{4}) 可以用于匹配格式为 “XXX-XX-XXXX” 的日期,其中每个括号内的部分就是一个捕获组。

string = "123-45-6789"
if string =~ /(\d{3})-(\d{2})-(\d{4})/
  puts "年: #{$1}, 月: #{$2}, 日: #{$3}"
end

在匹配成功后,$1$2$3 分别代表第一个、第二个和第三个捕获组匹配到的内容。

命名捕获组

从 Ruby 2.4 开始,支持命名捕获组。通过 (?<name>pattern) 的形式来定义命名捕获组。例如:

string = "123-45-6789"
if string =~ /(?<year>\d{3})-(?<month>\d{2})-(?<day>\d{4})/
  puts "年: #{$~[:year]}, 月: #{$~[:month]}, 日: #{$~[:day]}"
end

这里 $~ 是一个特殊的变量,它包含了匹配的结果,通过 $~[:name] 的形式可以访问命名捕获组匹配到的内容。

正则表达式的应用

字符串匹配

字符串匹配是正则表达式最常见的应用之一。除了前面介绍的 =~ 操作符外,String 类还提供了一些其他方法来进行匹配。

  1. match 方法match 方法返回一个 MatchData 对象,如果匹配成功;否则返回 nil
string = "I love ruby"
match = string.match(/ruby/)
if match
  puts "匹配成功,匹配内容: #{match[0]}"
else
  puts "匹配失败"
end

MatchData 对象包含了关于匹配的详细信息,例如匹配的内容、捕获组的内容等。

  1. scan 方法scan 方法返回字符串中所有匹配的子字符串组成的数组。
string = "I love ruby, ruby is fun"
matches = string.scan(/ruby/)
puts matches.inspect

上述代码会输出 ["ruby", "ruby"],即字符串中所有匹配 “ruby” 的子字符串。

字符串替换

  1. sub 方法sub 方法用于替换字符串中第一个匹配的子字符串。
string = "I love ruby, ruby is fun"
new_string = string.sub(/ruby/, "Python")
puts new_string

上述代码会将字符串中第一个 “ruby” 替换为 “Python”,输出为 “I love Python, ruby is fun”。

  1. gsub 方法gsub 方法用于替换字符串中所有匹配的子字符串。
string = "I love ruby, ruby is fun"
new_string = string.gsub(/ruby/, "Python")
puts new_string

上述代码会将字符串中所有 “ruby” 替换为 “Python”,输出为 “I love Python, Python is fun”。

分割字符串

split 方法可以使用正则表达式作为分隔符来分割字符串。

string = "apple,banana;cherry"
parts = string.split(/[,\;]/)
puts parts.inspect

上述代码会使用逗号和分号作为分隔符来分割字符串,输出为 ["apple", "banana", "cherry"]

高级正则表达式技巧

零宽断言

零宽断言是一种特殊的正则表达式结构,它匹配一个位置而不是实际的字符。

  1. 正向先行断言(?=pattern) 表示匹配前面的位置,后面跟着 pattern。例如,ruby(?= is fun) 会匹配 “ruby”,但前提是 “ruby” 后面跟着 “ is fun”。
string = "ruby is fun"
if string =~ /ruby(?= is fun)/
  puts "匹配成功"
end
  1. 负向先行断言(?!pattern) 表示匹配前面的位置,后面不跟着 pattern。例如,ruby(?! is fun) 会匹配 “ruby”,前提是 “ruby” 后面不跟着 “ is fun”。
strings = ["ruby is great", "ruby is fun"]
strings.each do |str|
  if str =~ /ruby(?! is fun)/
    puts "#{str} 匹配成功"
  else
    puts "#{str} 匹配失败"
  end
end
  1. 正向回顾后发断言(?<=pattern) 表示匹配后面的位置,前面跟着 pattern。例如,(?<=I love )ruby 会匹配 “ruby”,前提是 “ruby” 前面是 “I love ”。
string = "I love ruby"
if string =~ /(?<=I love )ruby/
  puts "匹配成功"
end
  1. 负向回顾后发断言(?<!pattern) 表示匹配后面的位置,前面不跟着 pattern。例如,(?<!I hate )ruby 会匹配 “ruby”,前提是 “ruby” 前面不是 “I hate ”。
strings = ["I love ruby", "I hate ruby"]
strings.each do |str|
  if str =~ /(?<!I hate )ruby/
    puts "#{str} 匹配成功"
  else
    puts "#{str} 匹配失败"
  end
end

分支结构

分支结构使用 | 来表示或的关系。例如,ruby|python 会匹配 “ruby” 或 “python”。

strings = ["ruby", "python"]
strings.each do |str|
  if str =~ /ruby|python/
    puts "#{str} 匹配成功"
  else
    puts "#{str} 匹配失败"
  end
end

正则表达式的嵌套

可以在正则表达式中嵌套其他正则表达式。例如,((\d{3})-(\d{2})-(\d{4})) 是一个嵌套的正则表达式,其中外层括号内包含了一个完整的日期格式匹配,而内层括号又定义了捕获组。

string = "123-45-6789"
if string =~ /((\d{3})-(\d{2})-(\d{4}))/
  puts "完整日期: #{$1}, 年: #{$2}, 月: #{$3}, 日: #{$4}"
end

性能优化

在使用正则表达式时,性能是一个需要考虑的重要因素。以下是一些优化正则表达式性能的建议。

简化正则表达式

尽量简化正则表达式,避免使用不必要的复杂结构。例如,如果只需要匹配一个固定的字符串,直接使用普通字符匹配即可,而不要使用复杂的字符类或重复限定。

# 推荐
string = "ruby"
if string == "ruby"
  puts "匹配成功"
end

# 不推荐
if string =~ /^r[au]by$/
  puts "匹配成功"
end

预编译正则表达式

如果需要多次使用同一个正则表达式,预编译它可以提高性能。可以使用 Regexp.new 方法创建 Regexp 对象并进行预编译。

regex = Regexp.new("pattern")
strings = ["string1", "string2"]
strings.each do |str|
  if str =~ regex
    puts "#{str} 匹配成功"
  end
end

避免过度使用捕获组

捕获组会增加正则表达式的计算量,尽量避免使用不必要的捕获组。如果只是为了分组而不是捕获内容,可以使用非捕获组 (?:pattern)

# 使用捕获组
string = "123-45-6789"
if string =~ /(\d{3})-(\d{2})-(\d{4})/
  puts "年: #{$1}, 月: #{$2}, 日: #{$3}"
end

# 使用非捕获组
if string =~ /(?:\d{3})-(?:\d{2})-(?:\d{4})/
  puts "匹配日期格式"
end

通过合理运用上述技巧和优化方法,可以在 Ruby 中更高效地使用正则表达式来处理字符串操作,无论是在数据验证、文本解析还是其他相关场景中,都能发挥出正则表达式强大的功能。同时,在实际应用中,要根据具体需求仔细编写和调试正则表达式,以确保其准确性和高效性。