Ruby Regex 基础教程 – wiki基地


Ruby Regex 基础教程:掌握强大的模式匹配利器

在软件开发中,我们经常需要处理文本数据:查找特定的字符串、验证输入格式、从大段文本中提取信息、或者替换不符合规范的内容。手动编写代码来处理这些任务既繁琐又容易出错。幸运的是,几乎所有现代编程语言都提供了强大的工具来解决这些问题——那就是正则表达式(Regular Expressions,简称 Regex 或 Regexp)

Ruby 作为一种灵活而富有表现力的语言,对正则表达式提供了第一等的支持,使其成为处理文本的绝佳工具。掌握 Ruby 中的正则表达式,将极大地提升你处理字符串的能力。

本文将带你深入了解 Ruby 正则表达式的基础知识,从最简单的匹配到更高级的技巧,并通过丰富的代码示例来帮助你理解和实践。

1. 什么是正则表达式?为什么在 Ruby 中使用它?

正则表达式是一种描述文本模式的强大工具。你可以把它想象成一种微型编程语言,专门用来定义字符串的搜索规则。通过使用特定的字符组合,你可以构建出复杂的模式来匹配符合特定结构的字符串。

在 Ruby 中,正则表达式被封装在 Regexp 类中。Ruby 提供了简洁的语法和多种内置方法来方便地使用正则表达式,例如:

  • 搜索和查找: 快速判断一个字符串是否包含某种模式,或者找到所有符合模式的子串。
  • 验证: 检查用户输入(如邮箱地址、电话号码、日期格式等)是否符合预期的格式。
  • 提取信息: 从非结构化文本中提取出关键数据。
  • 替换和修改: 根据模式查找并替换字符串中的内容。
  • 分割: 使用模式作为分隔符来分割字符串。

总之,任何涉及复杂文本处理的场景,正则表达式都能大显身手。

2. 如何创建 Ruby 正则表达式

在 Ruby 中,创建正则表达式对象有几种常见的方法:

2.1 使用斜杠字面量 /.../

这是最常见也是最推荐的方式,简洁直观。

“`ruby

创建一个匹配 “hello” 的正则表达式

regex1 = /hello/

创建一个匹配数字的正则表达式

regex2 = /\d/
“`

2.2 使用 %r{...} 字面量

当你需要在模式中包含大量斜杠时(例如匹配文件路径),使用 /.../ 会导致大量的反斜杠转义 (\/),使得模式难以阅读。此时,可以使用 %r{...} 字面量,你可以选择任何一对非字母数字的字符作为分隔符,例如 {}()[] 等。

“`ruby

使用斜杠字面量,需要转义斜杠

path_regex_slash = /^\/usr\/local\/bin\//

使用 %r 字面量,避免转义

path_regex_percent_r = %r{^/usr/local/bin/}

puts path_regex_slash == path_regex_percent_r # => true
“`

选择合适的分隔符可以提高可读性。

2.3 使用 Regexp.new 方法

当你需要根据变量或动态生成字符串来构建正则表达式时,可以使用 Regexp.new 方法。

“`ruby
word = “Ruby”

根据变量创建正则表达式

dynamic_regex = Regexp.new(word) # 等同于 /Ruby/

pattern = “^\d{3}-\d{4}$” # 字符串表示的模式,需要转义反斜杠
phone_regex = Regexp.new(pattern) # 等同于 /^\d{3}-\d{4}$/
“`

注意,使用 Regexp.new 时,如果模式字符串中包含反斜杠 (\),你需要使用两个反斜杠 (\\) 来表示一个字面量反斜杠,因为字符串本身会先解释反斜杠。这是与 /.../%r{...} 字面量最大的区别,在字面量中 /^\d{3}-\d{4}$/ 只需要一个反斜杠。

3. Ruby 中的正则表达式匹配操作

Ruby 提供了多种方法来执行正则表达式匹配:

3.1 =~ 运算符

这是最简单的方式,用于检查字符串是否包含某个模式。如果匹配成功,它返回第一个匹配的起始索引;如果匹配失败,则返回 nil

“`ruby
text = “hello world”
regex = /world/

match_index = text =~ regex
puts match_index # => 6 (从索引 6 开始匹配)

no_match_index = text =~ /goodbye/
puts no_match_index # => nil
“`

=~ 运算符常用于条件判断:

“`ruby
if text =~ /world/
puts “找到了 ‘world'” # 输出:找到了 ‘world’
end

if text =~ /ruby/
puts “找到了 ‘ruby'”
else
puts “没有找到 ‘ruby'” # 输出:没有找到 ‘ruby’
end
“`

3.2 String#match 方法

match 方法比 =~ 更强大。如果匹配成功,它返回一个 MatchData 对象,包含了匹配的详细信息;如果匹配失败,则返回 nil。这是获取匹配结果和捕获组的首选方法。我们稍后会详细介绍 MatchData 对象。

“`ruby
text = “hello world”
regex = /world/

match_data = text.match(regex)
puts match_data.class # => MatchData
puts match_data[0] # => “world” (整个匹配到的子串)

no_match_data = text.match(/goodbye/)
puts no_match_data # => nil
“`

3.3 String#match? 方法

如果你只关心字符串是否匹配某个模式,而不需要获取匹配的详细信息或捕获组,可以使用 match? 方法。它只返回 truefalse,效率通常比 match != nil 更高。

ruby
text = "hello world"
puts text.match?(/world/) # => true
puts text.match?(/ruby/) # => false

4. 正则表达式核心语法元素

理解正则表达式的语法是使用的关键。以下是一些核心的语法元素:

4.1 字面量字符

大多数字符在正则表达式中都代表它们自身,会匹配文本中完全相同的字符。

ruby
/cat/ # 匹配字符串 "cat"
/123/ # 匹配字符串 "123"
/hello world/ # 匹配字符串 "hello world"

4.2 特殊字符(元字符)

有些字符在正则表达式中有特殊的含义,称为元字符。如果你想匹配这些字符本身,需要使用反斜杠 \ 进行转义。

常见的元字符包括:. ^ $ * + ? { } ( ) [ ] | \

ruby
/\./ # 匹配一个字面量点号 "."
/\^/ # 匹配一个字面量插入符号 "^"
/\\/ # 匹配一个字面量反斜杠 "\"
/\?/ # 匹配一个字面量问号 "?"
/\[/ # 匹配一个字面量左方括号 "["

4.3 字符类 []

方括号 [] 定义一个字符集。它会匹配方括号中 任意一个 字符。

ruby
/[aeiou]/ # 匹配任意一个元音字母 (a, e, i, o, u)
/[0123456789]/ # 匹配任意一个数字
/[abcXYZ]/ # 匹配 'a', 'b', 'c', 'X', 'Y', 或 'Z'

范围表示: 在字符类中,可以使用连字符 - 来表示一个字符范围。

ruby
/[0-9]/ # 匹配任意一个数字 (等同于 [0123456789])
/[a-z]/ # 匹配任意一个小写字母
/[A-Z]/ # 匹配任意一个大写字母
/[a-zA-Z]/ # 匹配任意一个字母 (大小写不敏感)
/[0-9a-fA-F]/ # 匹配任意一个十六进制数字

否定字符类 [^] 在方括号内部的开头使用 ^ 表示匹配 在该集合中的任何字符。

ruby
/[^0-9]/ # 匹配任意一个非数字字符
/[^aeiou]/ # 匹配任意一个非元音字符

注意,在 [] 内部,大部分元字符会失去其特殊含义(除了 ]-^)。

4.4 常用字符类简写

为了方便,正则表达式提供了一些常用的字符类简写:

  • \d: 匹配任意一个数字 (等同于 [0-9])
  • \D: 匹配任意一个非数字字符 (等同于 [^0-9])
  • \w: 匹配任意一个词字符 (字母、数字或下划线) (等同于 [a-zA-Z0-9_])
  • \W: 匹配任意一个非词字符 (等同于 [^a-zA-Z0-9_])
  • \s: 匹配任意一个空白字符 (空格、制表符 \t、换行符 \n、回车符 \r、换页符 \f 等)
  • \S: 匹配任意一个非空白字符
  • .: 匹配 除换行符 \n 的任意单个字符。如果使用 m 选项 (Multiline Mode),. 会匹配包括换行符在内的所有字符。

ruby
/\d{3}/ # 匹配三个连续的数字
/\w+/ # 匹配一个或多个词字符
/.\s./ # 匹配任意字符,后面跟着一个空白字符,再跟着任意字符

4.5 锚点

锚点不匹配实际的字符,而是匹配字符出现的位置。

  • ^: 匹配字符串的开头。在多行模式 (m 选项) 下,也匹配每一行的开头(紧跟在换行符之后)。
  • $: 匹配字符串的结尾。在多行模式 (m 选项) 下,也匹配每一行的结尾(紧跟在换行符之前)。
  • \b: 匹配一个词的边界。词边界指一个词字符 (\w) 和一个非词字符 (\W) 之间的位置,或者一个词字符与字符串的开头/结尾之间的位置。
  • \B: 匹配一个非词边界。

ruby
/^hello/ # 匹配以 "hello" 开头的字符串
/world$/ # 匹配以 "world" 结尾的字符串
/^\d{5}$/ # 匹配一个恰好由 5 个数字组成的字符串 (如邮政编码)
/\bcat\b/ # 匹配独立的单词 "cat",而不是 "catalog" 或 "cation" 中的 "cat"
/\Bcat\B/ # 匹配 "catalog" 或 "cation" 中的 "cat",而不是独立的 "cat"

4.6 量词

量词用于指定前面的元素(字符、字符类、组等)出现的次数。量词默认是贪婪的,即尽可能多地匹配字符。

  • ?: 匹配前面的元素 0 次或 1 次。 (可选)
  • *: 匹配前面的元素 0 次或多次。
  • +: 匹配前面的元素 1 次或多次。
  • {n}: 匹配前面的元素恰好 n 次。
  • {n,}: 匹配前面的元素至少 n 次。
  • {n,m}: 匹配前面的元素至少 n 次,但不超过 m 次。

ruby
/colou?r/ # 匹配 "color" 或 "colour"
/a*/ # 匹配零个或多个 'a'
/a+/ # 匹配一个或多个 'a'
/\d{4}/ # 匹配恰好 4 个数字
/\d{2,4}/ # 匹配 2 到 4 个数字
/\w{5,}/ # 匹配至少 5 个词字符

非贪婪量词: 在量词后面加上 ? 可以使其变为非贪婪模式,即尽可能少地匹配字符。

  • ??: 匹配 0 次或 1 次,非贪婪
  • *?: 匹配 0 次或多次,非贪婪
  • +?: 匹配 1 次或多次,非贪婪
  • {n,m}?: 匹配 n 到 m 次,非贪婪
  • {n,}?: 匹配至少 n 次,非贪婪

非贪婪模式在匹配 HTML/XML 标签等嵌套结构时特别有用。

“`ruby
html = “

paragraph 1

paragraph 2

贪婪匹配:匹配从第一个

到最后一个

的所有内容

greedy_match = html.match(/

.*<\/p>/)
puts “贪婪匹配: #{greedy_match[0]}”

输出:贪婪匹配:

paragraph 1

paragraph 2

非贪婪匹配:只匹配第一个

nongreedy_match = html.match(/

.*?<\/p>/)
puts “非贪婪匹配: #{nongreedy_match[0]}”

输出:非贪婪匹配:

paragraph 1

“`

4.7 分组与捕获 ()

圆括号 () 用于将正则表达式的一部分组合在一起,作为一个单元处理。这主要有两个目的:

  1. 应用量词: 将量词应用到整个组,而不是组内的单个字符。
    ruby
    /(ab)+/ # 匹配一个或多个连续的 "ab" (如 "ab", "abab", "ababab")
    /(ha){3}/ # 匹配恰好三个连续的 "ha" (即 "hahaha")
  2. 捕获匹配内容: () 默认会“捕获”它所匹配到的内容,并将其作为单独的匹配结果存储起来,可以通过 MatchData 对象或 $1, $2 等全局变量访问。

4.8 选择/或 |

竖线 | 用于表示“或”的关系,匹配 | 左边或右边的模式。

ruby
/(cat|dog)/ # 匹配 "cat" 或 "dog"
/grep|sed|awk/ # 匹配 "grep", "sed", 或 "awk"
/^(\d{3}|\(\d{3}\))\s*-?\s*\d{3}-\d{4}$/ # 匹配美国电话号码格式,区号可以是 123 或 (123)

4.9 反向引用 \1, \2, …

反向引用用于在同一个正则表达式中引用之前捕获组匹配到的内容。\1 引用第一个捕获组的内容,\2 引用第二个,以此类推。

“`ruby
/(.+)\s\1/ # 匹配由空白字符分隔的两个相同的单词 (如 “hello hello”, “word word”)

(.+) 捕获一个或多个任意字符 (第一个单词),\s 匹配空白,\1 引用第一个捕获组的内容

“`

4.10 非捕获分组 (?:...)

使用 (?:...) 可以将模式组合起来以便应用量词或进行分支选择,但它不会捕获匹配的内容,也不会创建一个新的捕获组。这在只需要分组而不需要提取特定内容时非常有用,可以提高性能。

ruby
/(?:abc)+/ # 匹配一个或多个 "abc" (如 "abc", "abcabc"),但不捕获 "abc" 这个组
/(?:cat|dog)s/ # 匹配 "cats" 或 "dogs",不捕获 "cat" 或 "dog"

4.11 零宽度断言(Lookarounds)

零宽度断言用于判断匹配位置的前后是否符合某种模式,但它们本身不消耗任何字符。它们是强大的工具,用于在特定上下文环境中进行匹配。

  • 肯定前瞻 (Positive Lookahead): (?=...) 匹配当前位置,前提是当前位置后面紧跟着 ... 定义的模式。
    ruby
    /Ruby(?=\s*on\s*Rails)/ # 匹配 "Ruby",但只在后面跟着 " on Rails" 时匹配。它只匹配 "Ruby",不包括后面的部分。
  • 否定前瞻 (Negative Lookahead): (?!...) 匹配当前位置,前提是当前位置后面紧跟着 ... 定义的模式。
    ruby
    /\d{3}(?!\d)/ # 匹配连续的三个数字,但后面不能是数字。用于匹配不是更长数字一部分的三个数字序列。
  • 肯定后顾 (Positive Lookbehind): (?<=...) 匹配当前位置,前提是当前位置前面紧跟着 ... 定义的模式。注意: 大多数正则引擎要求后顾断言的模式具有固定长度,但在 Ruby 中,部分非固定长度的后顾是支持的,但使用时需谨慎。
    ruby
    /(?<=USD)\d+(\.\d{2})?/ # 匹配美元符号 "USD" 后面的数字金额。它只匹配数字部分。
  • 否定后顾 (Negative Lookbehind): (?<!...) 匹配当前位置,前提是当前位置前面紧跟着 ... 定义的模式。
    ruby
    /(?<!\$)\d+/ # 匹配数字,但前面不能是美元符号 "$"。

Lookarounds 是高级特性,但能解决很多复杂的匹配问题。

5. MatchData 对象详解

当使用 string.match(regex) 成功匹配时,返回的是一个 MatchData 对象。这个对象包含了丰富的匹配信息。

“`ruby
text = “The quick brown fox jumps over the lazy dog.”
regex = /(\w+) (\w+) (\w+)/ # 匹配三个连续的单词并捕获它们

match_data = text.match(regex)

if match_data
puts “匹配成功!”
puts “整个匹配: #{match_data[0]}” # => “The quick brown”
puts “第一个捕获组: #{match_data[1]}” # => “The”
puts “第二个捕获组: #{match_data[2]}” # => “quick”
puts “第三个捕获组: #{match_data[3]}” # => “brown”

# 你可以通过索引或者捕获组的数字来访问
puts match_data[0] == match_data.to_s # => true

# 访问所有捕获组
puts “所有捕获组: #{match_data.captures.inspect}” # => [“The”, “quick”, “brown”]

# 匹配前的字符串部分
puts “匹配前: #{match_data.pre_match}” # => “”

# 匹配后的字符串部分
puts “匹配后: #{match_data.post_match}” # => ” fox jumps over the lazy dog.”

# 匹配开始的索引
puts “开始索引: #{match_data.begin(0)}” # => 0 (整个匹配)
puts “第一组开始索引: #{match_data.begin(1)}” # => 0 (“The”)

# 匹配结束的索引 (结束索引是匹配部分的下一个字符的索引)
puts “结束索引: #{match_data.end(0)}” # => 15 (“The quick brown” 的下一个字符 ‘ ‘)
puts “第一组结束索引: #{match_data.end(1)}” # => 3 (“The” 的下一个字符 ‘ ‘)

# 使用 named captures (命名捕获组)
named_regex = /(?\w+) (?\w+)/
named_match = text.match(named_regex)

if named_match
puts “命名捕获组示例:”
puts “第一个词: #{named_match[:first]}” # => “The”
puts “第二个词: #{named_match[“second”]}” # => “quick” (可以用符号或字符串访问)
puts “所有命名捕获组: #{named_match.named_captures.inspect}” # => {“first”=>”The”, “second”=>”quick”}
end

else
puts “没有匹配到。”
end
“`

MatchData 对象是获取和处理匹配结果的核心。

全局变量:string =~ regexstring.match(regex) 成功执行后,Ruby 会设置一些特殊的全局变量来引用最近一次匹配的结果:

  • $~$&: 最近一次整个匹配到的子串 (match_data[0])
  • $1, $2, $3, …: 第 1, 2, 3, … 个捕获组匹配到的内容 (match_data[1], match_data[2], …)
  • $ $: 匹配前的字符串部分 (match_data.pre_match`)
  • $'$+: 匹配后的字符串部分 (match_data.post_match)

虽然这些全局变量使用起来很方便,但它们是全局的,可能会在复杂的代码中引起混淆,因此通常推荐使用 MatchData 对象来获取匹配结果。

“`ruby
text = “hello world”
text =~ /world/ # 执行匹配

puts $& # => “world”
puts $’ # => “hello ”
puts $` # => ” world”
“`

6. Ruby String 方法与正则表达式的结合

正则表达式在 Ruby 中与 String 类的许多方法紧密集成,使得文本处理任务变得非常高效。

6.1 String#gsubString#gsub!

用于全局替换(global substitution),将字符串中所有匹配模式的部分替换为指定的内容。gsub! 是修改原字符串的破坏性方法,gsub 返回一个新的修改后的字符串。

替换内容可以是另一个字符串:

“`ruby
text = “hello world, hello Ruby”

将所有 “hello” 替换为 “hi”

new_text = text.gsub(/hello/, “hi”)
puts new_text # => “hi world, hi Ruby”

将所有数字替换为空字符串

phone = “Call me at 123-456-7890.”
cleaned_phone = phone.gsub(/\d/, “”)
puts cleaned_phone # => “Call me at –.”
“`

替换内容也可以是正则表达式捕获组的反向引用:

“`ruby

将 “姓 名” 格式改为 “名, 姓”

name = “Doe John”
formatted_name = name.gsub(/(\w+)\s+(\w+)/, ‘\2, \1’)
puts formatted_name # => “John, Doe”
“`

替换内容也可以是一个代码块(block)。代码块接收每次匹配到的子串作为参数,并返回用于替换的字符串。

“`ruby
text = “Price: $100, Discount: 20%”

将所有数字乘以 2

doubled_numbers = text.gsub(/\d+/) { |match| match.to_i * 2 }
puts doubled_numbers # => “Price: 200, Discount: 40%”

结合 MatchData 对象,使用捕获组

text = “Email: [email protected], User: [email protected]

提取用户名部分并转换为大写

uppercase_users = text.gsub(/(\w+)@(\w+.\w+)/) do |match|
# match 是整个匹配到的子串,不是 MatchData 对象
# 如果需要捕获组,可以使用 MatchData 对象
md = $~ # $~ 是最近一次匹配的 MatchData 对象
“#{md[1].upcase}@#{md[2]}”
end
puts uppercase_users # => “Email: [email protected], User: [email protected]

更直接的方式是使用 MatchData 对象作为 block 参数 (Ruby 2.4+)

uppercase_users_v24 = text.gsub(/(\w+)@(\w+.\w+)/) do |md|
“#{md[1].upcase}@#{md[2]}”
end
puts uppercase_users_v24 # => “Email: [email protected], User: [email protected]
“`

6.2 String#subString#sub!

gsub 类似,但只替换第一次匹配到的内容。sub! 是破坏性方法。

“`ruby
text = “hello world, hello Ruby”

只将第一个 “hello” 替换为 “hi”

new_text = text.sub(/hello/, “hi”)
puts new_text # => “hi world, hello Ruby”
“`

替换内容和代码块的使用方式与 gsub 相同。

6.3 String#scan

scan 方法查找字符串中所有与模式匹配的非重叠子串,并返回一个包含这些子串的数组。

如果正则表达式包含捕获组,scan 的行为会改变:
* 如果只有一个捕获组,返回的数组包含所有匹配到的该捕获组的内容。
* 如果有多个捕获组,返回的数组包含所有匹配到的捕获组内容的数组。

“`ruby
text = “The quick brown fox.”

匹配所有单词

words = text.scan(/\w+/)
puts words.inspect # => [“The”, “quick”, “brown”, “fox”]

匹配所有元音字母

vowels = text.scan(/[aeiou]/)
puts vowels.inspect # => [“e”, “u”, “i”, “o”, “o”]

匹配所有由字母组成的词 (使用捕获组)

word_capture = text.scan(/(\w+)/)
puts word_capture.inspect # => [[“The”], [“quick”], [“brown”], [“fox”]] (单捕获组,返回数组的数组)

匹配所有形如 ‘词1 词2’ 的对 (使用多个捕获组)

word_pairs = “one two three four”.scan(/(\w+)\s+(\w+)/)
puts word_pairs.inspect # => [[“one”, “two”], [“three”, “four”]] (多捕获组,返回数组的数组)

匹配所有形如 ‘词1 词2’ 的对 (不捕获组,匹配整个模式)

word_pairs_no_capture = “one two three four”.scan(/\w+\s+\w+/)
puts word_pairs_no_capture.inspect # => [“one two”, “three four”]
“`

6.4 String#split

split 方法使用指定的模式作为分隔符来分割字符串,并返回分割后的子串数组。如果模式包含捕获组,捕获到的内容也会被包含在结果数组中。

“`ruby
text = “apple,banana;cherry:date”

使用逗号或分号或冒号作为分隔符

fruits = text.split(/[,;:]/)
puts fruits.inspect # => [“apple”, “banana”, “cherry”, “date”]

分割并保留分隔符 (使用捕获组)

parts = text.split(/([,;:])/)
puts parts.inspect # => [“apple”, “,”, “banana”, “;”, “cherry”, “:”, “date”]
“`

7. 正则表达式选项/标志 (Options)

在 Ruby 中,可以在正则表达式字面量的末尾添加一个或多个字母来指定匹配选项。

  • i (case-insensitive): 忽略大小写进行匹配。
  • m (multiline): 使 ^$ 匹配每一行的开头和结尾,而不仅仅是整个字符串的开头和结尾。同时,使 . 匹配包括换行符在内的所有字符。
  • x (extended/verbose): 忽略模式中的空白字符(空格、制表符、换行符),并允许在模式中使用 # 开头的行内注释。这有助于提高复杂正则表达式的可读性。

“`ruby

忽略大小写

puts “Ruby” =~ /ruby/i # => 0 (匹配成功)
puts “Ruby” =~ /ruby/ # => nil (匹配失败)

多行模式

text = “Line 1\nLine 2\nLine 3”
puts text.scan(/^Line/) # => [“Line”] (^只匹配字符串开头)
puts text.scan(/^Line/m) # => [“Line”, “Line”, “Line”] (m模式下^匹配每一行开头)

puts text.scan(/Line./) # => [“Line “, “Line “] (. 不匹配换行符)
puts text.scan(/Line./m) # => [“Line\n”, “Line\n”] (. 匹配换行符)

扩展模式 (Verbose)

匹配一个带可选区号的电话号码 (XXX-XXX-XXXX 或 XXXXXXXXXX)

不使用 x 选项

regex_compact = /^\d{3}[-.]?\d{3}[-.]?\d{4}$/

使用 x 选项,提高可读性

regex_verbose = /
^ # 匹配字符串开头
\d{3} # 匹配区号 (3个数字)
[-.]? # 可选的分隔符 (. 或 -)
\d{3} # 匹配中间三位数字
[-.]? # 可选的分隔符
\d{4} # 匹配最后四位数字
$ # 匹配字符串结尾
/x

phone1 = “123-456-7890”
phone2 = “987.654.3210”
phone3 = “5555555555”
phone4 = “123-456” # 不匹配

puts phone1 =~ regex_compact # => 0
puts phone2 =~ regex_verbose # => 0
puts phone3 =~ regex_verbose # => 0
puts phone4 =~ regex_verbose # => nil
“`

选项可以组合使用,例如 /pattern/ix

8. 构建和调试正则表达式的技巧

构建复杂的正则表达式可能会令人望而生畏。以下是一些实用技巧:

  • 从小处着手: 先匹配模式中最简单的部分,然后逐步添加更复杂的元素(量词、分组、断言等)。
  • 使用在线工具: 有许多优秀的在线正则表达式测试工具,例如 Regex101.com 或 Rubular.com (专门为 Ruby 设计)。这些工具可以可视化地展示你的模式如何匹配文本,并解释模式的各个部分。
  • 利用 Ruby 交互式环境 (IRB/Pry): 在 IRB 或 Pry 中实时测试你的正则表达式和字符串方法。
  • 使用 x 选项提高可读性: 对于复杂的模式,使用 x 选项并添加注释可以极大地改善理解和维护。
  • 优先使用非捕获组 (?:...) 如果你不需要捕获组的内容,使用非捕获组可以稍微提高性能并避免创建不必要的 MatchData 元素。
  • 注意贪婪 vs. 非贪婪: 如果匹配结果不是你预期的,考虑量词是否是贪婪的,并尝试使用非贪婪量词 (*?, +? 等)。
  • 理解锚点: ^$ (\A\Z/\z 在不同模式下的细微区别,尽管本文只介绍了 ^$),以及 \b\B 对于精确匹配非常重要。

9. 实际应用示例

让我们看几个更贴近实际的例子:

示例 1:验证电子邮件地址的基本格式

“`ruby
def is_valid_email?(email)
# 简单的邮箱格式验证:local_part@domain
# local_part: 至少一个非空白、非@字符
# domain: 至少一个非空白、非@字符,后面跟着一个点,再跟着至少一个非空白、非@字符
email_regex = /\A[^@\s]+@[^@\s]+.[^@\s]+\z/
# \A: 匹配字符串绝对开头
# \z: 匹配字符串绝对结尾
# [^@\s]+: 匹配一个或多个非@且非空白字符
# .: 匹配字面量点号
email =~ email_regex
end

puts is_valid_email?(“[email protected]”) # => true
puts is_valid_email?(“invalid-email”) # => nil
puts is_valid_email?(“user@domain.”) # => nil
puts is_valid_email?(“@domain.com”) # => nil
“`
注意: 验证电子邮件地址的正则表达式可能非常复杂,这个例子只是一个非常基本的模式,用于说明概念。一个符合RFC标准的邮箱正则表达式会极其复杂。

示例 2:从文本中提取所有 URL

“`ruby
text = “Visit our website at https://www.example.com or our blog at http://blog.example.org. FTP link: ftp://ftp.example.net”

匹配 http://, https:// 或 ftp:// 开头,后面跟着非空白字符组成的 URL

使用非捕获组 (?:…) 将协议部分分组

使用捕获组 (…) 捕获 URL 的实际内容

url_regex = /(?:https?|ftp):\/\/[^\s]+/

urls = text.scan(url_regex)
puts urls.inspect # => [“https://www.example.com”, “http://blog.example.org”, “ftp://ftp.example.net”]

如果只想捕获 URL 的主体部分(不包括协议)

url_body_regex = /(?:https?|ftp):\/\/(.+)/ # 这个会匹配到换行符及后面的内容

更精确的匹配 URL (这个模式更复杂,仅作示例)

匹配协议、域名、端口(可选)、路径(可选)、查询参数(可选)等

这是一个简化的版本,实际 URLs 规则更复杂

better_url_regex = %r{
(?:https?|ftp):// # 协议 (非捕获组)
( # 捕获组 1: URL 主体
(?:[^:@\s]+(?::[^:@\s])?@)? # 用户信息 (可选)
(?:a-z0-9?.)+ # 域名部分 (一个或多个标签)
[a-z]{2,} # 顶级域名 (至少两个字母)
(?::\d+)? # 端口 (可选)
(?:[^:/?#\s]
)? # 路径 (可选)
(?:\?[^#\s])? # 查询参数 (可选)
(?:#\S
)? # Fragment (可选)
)
}ix # i: 忽略大小写,x: 忽略空白和注释

text_complex_url = “Check https://www.Example.com:8080/path/to/page?id=123#section at the start and end http://localhost:3000/.”

complex_urls = text_complex_url.scan(better_url_regex).flatten.compact # scan 多捕获组返回数组的数组,flatten 获取所有捕获组
puts complex_urls.inspect

=> [“www.Example.com:8080/path/to/page?id=123#section”, “localhost:3000/.”]

“`

示例 3:查找并替换文本中的日期格式

MM/DD/YYYY 格式的日期转换为 YYYY-MM-DD 格式。

“`ruby
text = “Order placed on 12/25/2023. Shipment expected by 01/15/2024.”

匹配并捕获月、日、年

date_regex = /(\d{2})\/(\d{2})\/(\d{4})/

使用 gsub 和反向引用进行替换

formatted_text = text.gsub(date_regex, ‘\3-\1-\2’)
puts formatted_text

输出:Order placed on 2023-12-25. Shipment expected by 2024-01-15.

使用 block 替换 (更灵活)

formatted_text_block = text.gsub(date_regex) do |match|
md = $~ # 获取 MatchData
year = md[3]
month = md[1]
day = md[2]
“#{year}-#{month}-#{day}”
end
puts formatted_text_block

输出:Order placed on 2023-12-25. Shipment expected by 2024-01-15.

“`

10. 总结与进一步学习

恭喜你!你已经掌握了 Ruby 正则表达式的基础知识,包括:

  • 创建正则表达式对象
  • 使用 =~, match, match? 进行匹配
  • 核心语法元素:字面量、元字符、字符类、量词、锚点
  • 分组与捕获、非捕获组、反向引用
  • 零宽度断言 (Lookarounds) 的概念
  • MatchData 对象的使用
  • gsub, sub, scan, split 等 Ruby 方法与 Regex 的结合
  • 常用的正则表达式选项

正则表达式是一个非常深奥的领域,本文只是冰山一角。要真正精通它,需要大量的练习和实践。

下一步学习方向:

  • 深入理解回溯 (Backtracking): 正则表达式引擎如何进行匹配,以及回溯可能导致的性能问题 (ReDoS)。
  • 更高级的特性: 条件匹配、递归匹配等 (这些在 Ruby Regex 中相对少用或支持有限)。
  • 练习!练习!练习! 尝试解决实际问题,使用在线工具测试,逐步构建复杂模式。

正则表达式是文本处理领域的瑞士军刀。在 Ruby 的强大支持下,它能让你以简洁高效的方式解决各种复杂的文本模式匹配和操作问题。现在就开始动手实践吧!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部