Ruby 正则表达式入门教程 – wiki基地


Ruby 正则表达式入门教程:模式匹配的利器

正则表达式(Regular Expression,简称 RegEx 或 RegExp)是一种强大的文本处理工具,它使用一套特殊字符组成的模式来描述和匹配字符串中的特定序列。无论是在数据验证、文本搜索、替换、分割,还是在更复杂的文本解析任务中,正则表达式都扮演着核心角色。

对于 Ruby 程序员来说,掌握正则表达式是提升开发效率、处理文本数据的必备技能。Ruby 对正则表达式提供了良好的内置支持,语法简洁且功能强大。

本教程将带你从零开始,逐步深入 Ruby 正则表达式的世界,详细介绍其基本概念、语法元素、常用方法以及一些实践技巧。

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

想象一下,你需要从一堆文本文件中找出所有符合特定格式的电话号码(例如:XXX-XXX-XXXX),或者验证用户输入的邮箱地址是否合法,或者从日志文件中提取出所有错误信息的时间戳。手动编写代码来处理这些任务可能会非常繁琐,需要大量的条件判断和循环。

正则表达式提供了一种声明式的方式来描述这些复杂的文本模式。你只需要定义好“长什么样”的模式,然后就可以让程序去查找、匹配、替换或分割符合这个模式的文本。

正则表达式的优点:

  1. 强大和灵活: 可以描述非常复杂的文本模式。
  2. 高效: 底层通常经过高度优化,执行速度快。
  3. 简洁: 复杂的匹配逻辑可以用相对紧凑的模式表示(虽然有时也会变得难以阅读)。
  4. 通用性: 正则表达式的概念和大部分语法在许多编程语言(Perl, Python, Java, JavaScript, PHP 等)和工具(grep, sed, awk 等)中都是通用的。

正则表达式的应用场景:

  • 数据验证: 检查输入数据(如邮箱、URL、电话号码、密码)是否符合预期的格式。
  • 文本搜索和查找: 在大量文本中快速定位符合特定模式的内容。
  • 文本替换: 查找并替换文本中符合特定模式的部分(例如,格式化日期、清理数据)。
  • 文本分割: 使用模式作为分隔符来拆分字符串。
  • 语法高亮和解析: 许多编辑器和解析器使用正则表达式来识别代码结构。

2. 在 Ruby 中创建正则表达式

在 Ruby 中,创建正则表达式主要有三种方式:

2.1. 字面量(Literal)

这是最常用、最推荐的方式。将正则表达式模式写在两个斜杠 / 之间。

“`ruby

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

pattern = /hello/

puts pattern.class # => Regexp
“`

如果你的模式包含斜杠 /,或者你觉得斜杠作为分隔符不够清晰,可以使用 %r{} 语法,大括号 {} 可以替换成任何一对匹配的符号,如 [], (), <> 等。

“`ruby

使用 %r{} 创建一个包含斜杠的正则表达式

pattern_with_slash = %r{/usr/local/bin}

也可以使用其他分隔符

pattern_with_parens = %r(/path/to/resource)

puts pattern_with_slash
puts pattern_with_parens
“`

字面量创建的正则表达式对象在加载代码时就被解析和编译,因此效率较高,适合模式固定不变的情况。

2.2. 使用 Regexp.new 方法

如果你需要根据变量或运行时信息动态构建正则表达式,可以使用 Regexp.new 方法。

“`ruby
word_to_find = “world”
dynamic_pattern = Regexp.new(word_to_find) # 等价于 /world/

puts dynamic_pattern
“`

需要注意的是,使用 Regexp.new 时,模式字符串中的反斜杠 \ 需要进行双重转义,因为字符串本身会先解释一次反斜杠。

“`ruby

匹配一个点号 (.) – 在正则表达式中,点号是特殊字符,需要转义

字面量写法:

literal_dot = /./
puts “Matching ‘.’ with literal: #{literal_dot.match(“this.is.a.test”)}”

Regexp.new 写法:

字符串中的 \ 需要转义,正则表达式中的 . 需要转义

dynamic_dot = Regexp.new(“\.”) # 或者 Regexp.new(‘.’) 使用单引号字符串避免字符串转义
puts “Matching ‘.’ with Regexp.new: #{dynamic_dot.match(“this.is.a.test”)}”
“`

2.3. 使用 %r//%r()

这是字面量语法的另一种形式,允许你指定不同的分隔符。这在模式本身包含字面量分隔符(如 /)时非常有用,可以避免大量的反斜杠转义。

“`ruby

匹配文件路径 /var/log/messages

path_pattern = %r{/var/log/messages} # 使用 {} 作分隔符,避免转义 /

等价于 /\/var\/log\/messages/ – 可读性大大降低

“`

在本教程中,我们将主要使用最常见的字面量 /pattern/ 形式。

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

创建了正则表达式后,下一步就是使用它来对字符串进行匹配操作。Ruby 提供了几种进行匹配的方法:

3.1. =~ 运算符

这是进行简单匹配的最快捷方式。当正则表达式与字符串进行 =~ 操作时:

  • 如果匹配成功,它返回匹配开始位置的索引(一个整数)。
  • 如果匹配失败,它返回 nil

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

match_index = text =~ pattern

if match_index
puts “Pattern found at index: #{match_index}” # => Pattern found at index: 6
else
puts “Pattern not found”
end

no_match_index = text =~ /ruby/
puts “Ruby not found: #{no_match_index.inspect}” # => Ruby not found: nil
“`

=~ 运算符虽然简洁,但它只告诉你是否匹配以及匹配的起始位置。如果你需要获取更多匹配信息(如匹配到的具体文本、捕获的子串等),你需要使用 match 方法。

3.2. match 方法

match 方法是更强大的匹配工具。当调用 string.match(regexp) 时:

  • 如果匹配成功,它返回一个 MatchData 对象,包含了所有匹配的详细信息。
  • 如果匹配失败,它返回 nil

MatchData 对象包含了原始字符串、匹配到的文本、捕获的分组、匹配的位置等信息。

“`ruby
text = “hello beautiful world”
pattern = /beautiful/

match_data = text.match(pattern)

if match_data
puts “MatchData object: #{match_data.inspect}”
puts “Matched text: #{match_data[0]}” # => beautiful (或 match_data.to_s)
puts “Start index: #{match_data.begin(0)}” # => 6
puts “End index: #{match_data.end(0)}” # => 15 (匹配结束位置后一位)
puts “Pre-match: #{match_data.pre_match}” # => hello
puts “Post-match: #{match_data.post_match}” # => world
else
puts “Pattern not found”
end
“`

对于更复杂的模式匹配,尤其是涉及捕获分组(稍后介绍)时,match 方法返回的 MatchData 对象是获取信息的关键。

注意: 执行 =~match 成功后,Ruby 会设置一些特殊的全局变量(如 $, $&, $‘, $\``,$1,$2等),它们引用最近一次成功匹配的信息。例如,$&引用整个匹配到的字符串,$`引用匹配前的部分,$’引用匹配后的部分。虽然这些变量有时很方便,但在大型或多线程程序中应谨慎使用,因为它们是全局的,可能导致意外行为。推荐优先使用MatchData` 对象。

4. 正则表达式的基本元素:元字符和字面字符

正则表达式由两种基本类型的字符组成:

  • 字面字符 (Literal Characters): 除了元字符之外的所有字符,它们代表自身。例如,字母 a 匹配字符 a,数字 5 匹配字符 5
  • 元字符 (Metacharacters): 具有特殊含义的字符,它们不代表自身,而是用于描述模式的结构或行为。

以下是一些最基本的元字符:

  • . (点号): 匹配除换行符 \n 之外的任意单个字符
  • \ (反斜杠): 转义字符。用于取消一个元字符的特殊含义,使其变成字面字符;或者用于引入一个特殊序列(如 \d, \s, \n)。

示例:

“`ruby
puts “abc”.match(/a.c/).to_s # => abc (匹配 ‘a’, 任意字符, ‘c’)
puts “a c”.match(/a.c/).to_s # => a c (匹配 ‘a’, 空格, ‘c’)
puts “a\nc”.match(/a.c/).to_s # => “” (匹配失败,. 不匹配 \n 默认情况下)

如何匹配字面意义的点号?需要用 \ 转义

puts “price: $9.99”.match(/.\d{2}/).to_s # => .99 (匹配字面点号后跟两位数字)

如何匹配字面意义的反斜杠?需要用 \ 转义

puts “C:\Users\”.match(/C:\\Users/).to_s # => C:\Users (匹配字面 C:\Users)
“`

5. 字符集合 (Character Sets)

字符集合(也称为字符类)用方括号 [] 表示,它匹配方括号内的任意一个字符

  • [abc]: 匹配 ‘a’, ‘b’, 或 ‘c’ 中的任意一个字符。
  • [0123456789]: 匹配任意一个数字。
  • [aeiouAEIOU]: 匹配任意一个元音字母(大小写)。

使用连字符 - 表示范围:

  • [0-9]: 匹配任意一个数字(0到9)。
  • [a-z]: 匹配任意一个小写英文字母。
  • [A-Z]: 匹配任意一个大写英文字母。
  • [a-zA-Z]: 匹配任意一个英文字母(大小写)。
  • [0-9a-fA-F]: 匹配任意一个十六进制数字。

否定字符集合:

在方括号内开头使用脱字符 ^ 表示匹配不在方括号内的任意一个字符

  • [^0-9]: 匹配任意一个非数字字符。
  • [^aeiou]: 匹配任意一个非元音字母。
  • [^>]: 匹配任意一个非 > 字符。

示例:

“`ruby
puts “color”.match(/col[ou]r/).to_s # => color (匹配 col 后面跟 o 或 u,再跟 r)
puts “colour”.match(/col[ou]r/).to_s # => colour

puts “digit: 7”.match(/[^0-9]/).to_s # => : (匹配第一个非数字字符)
puts “number 123”.match(/[0-9]/).to_s # => 1 (匹配第一个数字字符)
“`

需要转义的特殊字符在 [] 内通常不需要转义:

在字符集合 [] 内部,大多数元字符失去了特殊含义,成为字面字符。例如 . + * ? () {} |[] 内都代表自身。但 -(用于范围)和 ^(在开头用于否定)仍然是特殊的,如果需要匹配字面意义的 -^,需要将它们放在非开头位置或进行转义。 ] 字符在 [] 内总是需要转义 \]

ruby
puts "[bracket]".match(/[\[\]]/).to_s # => [ (匹配字面意义的 '[' 或 ']')
puts "hyphen-ated".match(/[a-z-]/).to_s # => h (匹配任意小写字母或 '-')

6. 量词 (Quantifiers)

量词用于指定其前面的元素(一个字符、一个字符集合、一个分组等)出现的次数。

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

示例:

“`ruby
puts “caat”.match(/cat/).to_s # => caat (匹配 c, 0个或多个 a, t)
puts “cat”.match(/ca
t/).to_s # => cat
puts “ct”.match(/ca*t/).to_s # => ct

puts “caat”.match(/ca+t/).to_s # => caat (匹配 c, 1个或多个 a, t)
puts “cat”.match(/ca+t/).to_s # => cat
puts “ct”.match(/ca+t/).to_s # => “” (匹配失败)

puts “color”.match(/colou?r/).to_s # => color (匹配 col, 0个或1个 u, r)
puts “colour”.match(/colou?r/).to_s # => colour

puts “aaaaa”.match(/a{3}/).to_s # => aaa (匹配连续的 3 个 a)
puts “aaaaa”.match(/a{3,}/).to_s # => aaaaa (匹配至少 3 个 a)
puts “aaaaa”.match(/a{1,3}/).to_s # => aaa (匹配 1 到 3 个 a)
“`

贪婪与非贪婪匹配 (Greedy vs. Lazy Matching):

默认情况下,量词是“贪婪的”,它们会尽可能多地匹配字符,同时仍能使整个模式匹配成功。

例如,对于字符串 "<a><b>" 和模式 /<.*>/

  • . 匹配任意字符
  • * 匹配前面的任意字符 0 次或多次
  • .* 会尽可能多地匹配字符

贪婪匹配结果:匹配到 "<a><b>" 整个字符串。因为 .* 会一直匹配到最后一个 > 之前的所有字符。

ruby
text = "<a><b>"
puts text.match(/<.*>/).to_s # => <a><b>

如果你希望匹配的是最短可能的匹配,可以在量词后面加上 ? 使其变成“非贪婪的”或“惰性的”。

  • ??: 匹配 0 次或 1 次,但优先匹配 0 次。
  • *?: 匹配 0 次或多次,但优先匹配 0 次。
  • +?: 匹配 1 次或多次,但优先匹配 1 次。
  • {n,m}?: 匹配 n 到 m 次,但优先匹配 n 次。

使用非贪婪量词 .*?

“`ruby
text = “

匹配 < 后跟 0个或多个任意字符(非贪婪),再跟 >

puts text.match(/<.*?>/).to_s # =>
“`

非贪婪量词在匹配 HTML/XML 标签、引号括起来的字符串等场景中非常有用,可以避免匹配到超出预期范围的内容。

7. 边界匹配 (Anchors)

边界匹配器(或称锚点)不匹配实际的字符,而是匹配位置。它们用于将模式固定在字符串的特定位置。

  • ^: 匹配字符串的开始。
  • $: 匹配字符串的结束。
  • \b: 匹配一个单词边界。
  • \B: 匹配一个非单词边界。

^$ 示例:

“`ruby
puts “start”.match(/^start/).to_s # => start (必须以 start 开头)
puts “start and end”.match(/^start/).to_s # => start
puts “not start”.match(/^start/).to_s # => “” (匹配失败)

puts “end”.match(/end$/).to_s # => end (必须以 end 结尾)
puts “start and end”.match(/end$/).to_s # => end
puts “ending”.match(/end$/).to_s # => “” (匹配失败)

匹配整个字符串

puts “exactly”.match(/^exactly$/).to_s # => exactly (字符串必须正好是 “exactly”)
puts ” exactly “.match(/^exactly$/).to_s # => “” (有空格,匹配失败)
“`

\b (单词边界) 示例:

\b 匹配单词字符 (\w,即字母、数字、下划线) 和非单词字符 (\W) 之间的位置。它也匹配字符串的开始和结束位置,如果这些位置紧邻单词字符。

“`ruby
text = “word words wording sword”

匹配完整的单词 “word”

puts text.match(/\bword\b/).to_s # => word

如果没有 \b,会匹配到 words 和 wording 中的 “word” 子串

puts text.match(/word/).to_s # => word

puts “word”.match(/\bword\b/).to_s # => word (字符串开头和结尾是边界)
puts “word!”.match(/\bword\b/).to_s # => word (! 是非单词字符)
puts “!word”.match(/\bword\b/).to_s # => word (! 是非单词字符)
puts “keyword”.match(/\bword\b/).to_s # => “” (k 是单词字符,不是边界)
“`

\B (非单词边界) 示例:

\B 匹配一个非单词边界的位置。例如,在两个单词字符之间,或在两个非单词字符之间。

“`ruby
text = “keyword words”

匹配作为子串的 “word”,而不是完整的单词

puts text.match(/\Bword\b/).to_s # => word (匹配 keyword 中的 word)
“`

\A, \Z, \z 的区别 (高级边界):

在 Ruby 中,还有更精确的边界匹配器:

  • \A: 只匹配字符串的绝对开头。不受多行模式 (m 选项) 的影响。
  • \Z: 匹配字符串的结尾,但在最后一个换行符 \n 之前(如果存在)。
  • \z: 匹配字符串的绝对结尾。不受多行模式 (m 选项) 的影响。

通常情况下,^ 等同于 \A$ 等同于 \Z。但在启用多行模式 (/mRegexp::MULTILINE) 时,^$ 会匹配每一行的开头和结尾(即 \n 之后和之前的位置),而 \A, \Z, \z 仍然只匹配整个字符串的开始和结束。

示例:

“`ruby
text_multiline = “Line 1\nLine 2\nLine 3”

默认行为:

puts text_multiline.match(/^Line/).to_s # => Line (匹配第一行开头)
puts text_multiline.match(/3$/).to_s # => 3 (匹配最后一行结尾)
puts text_multiline.match(/\ALine/).to_s # => Line (匹配字符串绝对开头)
puts text_multiline.match(/3\Z/).to_s # => 3 (匹配字符串结尾,\n前)
puts text_multiline.match(/3\z/).to_s # => 3 (匹配字符串绝对结尾)

puts text_multiline.match(/\nLine/).to_s # => \nLine (匹配换行符后跟着 Line)

在多行模式下 (/m):

注意:在 Ruby 的正则表达式中,/m 选项(Regexp::MULTILINE)的默认行为是让 ‘.’ 匹配换行符。

行锚点 (^ 和 $) 的多行行为由 /m 或 /n (弃用) /u /e 之外的选项控制,但通常 ^和$ 本身就是多行模式下的行锚点。

实际测试 Ruby 文档和行为表明,^和$ 默认就是多行感知的,除非使用了 /m (它主要影响 . 的行为)或其他编码选项。

更准确地说,在 Ruby 中,^ 和 $ 总是匹配行的开始和结束,而 \A 和 \z 总是匹配字符串的开始和结束。

\Z 匹配字符串结尾,但在可能的最终换行符之前。

让我们用更清晰的例子说明 \Z 和 \z 的区别:

text_end_newline = “Hello\n”
puts text_end_newline.match(/o\Z/).to_s # => o (匹配到 \n 前的 o)
puts text_end_newline.match(/o\z/).to_s # => “” (匹配不到,因为 \z 在字符串的绝对结尾,o 不在那里)
puts text_end_newline.match(/\n\z/).to_s # => \n (匹配字符串末尾的 \n)
“`

对于大多数常见的匹配任务,^$ 就足够了。只有当你需要严格区分整个字符串的开头/结尾和每一行的开头/结尾时,才需要用到 \A, \Z, \z

8. 特殊字符序列 (Shorthand Character Classes)

为了方便,正则表达式定义了一些常用的字符集合的简写形式。

  • \d: 匹配任意数字字符,等价于 [0-9]
  • \D: 匹配任意非数字字符,等价于 [^0-9]
  • \w: 匹配任意“单词”字符(字母、数字或下划线),等价于 [a-zA-Z0-9_]
  • \W: 匹配任意非“单词”字符,等价于 [^a-zA-Z0-9_]
  • \s: 匹配任意空白字符(空格、制表符 \t、换行符 \n、回车符 \r、换页符 \f 等),等价于 [ \t\n\r\f\v]
  • \S: 匹配任意非空白字符,等价于 [^ \t\n\r\f\v]

示例:

ruby
puts "Phone: 123-456-7890".match(/\d{3}-\d{3}-\d{4}/).to_s # => 123-456-7890
puts "ID: user_123".match(/\w+/).to_s # => ID (匹配一个或多个单词字符)
puts "Space out".match(/\s/).to_s # => " " (匹配一个空格)
puts "NoSpace".match(/\S/).to_s # => N (匹配一个非空白字符)

这些简写形式极大地提高了正则表达式的可读性和编写效率。

9. 分组和捕获 (Grouping and Capturing)

圆括号 () 在正则表达式中有两个主要作用:

  1. 分组 (Grouping): 将多个元素视为一个整体,以便对其应用量词或进行其他操作。
  2. 捕获 (Capturing): 匹配到的分组内容会被“捕获”并存储起来,以便后续引用或提取。

分组示例:

“`ruby

匹配连续出现的 “ab” 两次

puts “abab”.match(/(ab){2}/).to_s # => abab

匹配 “go” 或 “stop” 后面跟 “ing”

puts “going”.match(/(go|stop)ing/).to_s # => going
puts “stopping”.match(/(go|stop)ing/).to_s # => stopping
“`

在没有分组的情况下,量词和选择符 | 会按照特定的优先级结合。例如,go|stop+ing 意味着 go 或 (stop 出现一次或多次) 后面跟 ing。使用分组可以明确指定结合关系。

捕获示例:

当使用圆括号 () 进行分组时,匹配到的子字符串会被自动捕获。match 方法返回的 MatchData 对象可以让你访问这些捕获的内容。match_data[0] 是整个匹配到的字符串,match_data[1] 是第一个捕获组的内容,match_data[2] 是第二个捕获组的内容,以此类推。

“`ruby

匹配一个日期格式 YYYY-MM-DD,并捕获年、月、日

date_pattern = /(\d{4})-(\d{2})-(\d{2})/
text = “Today’s date is 2023-10-26.”

match_data = text.match(date_pattern)

if match_data
puts “Full match: #{match_data[0]}” # => 2023-10-26
puts “Year: #{match_data[1]}” # => 2023
puts “Month: #{match_data[2]}” # => 10
puts “Day: #{match_data[3]}” # => 26
# 你也可以使用名称访问,如果使用了命名捕获组 (稍后介绍)
end
“`

捕获组是从左到右按照左括号 ( 出现的顺序编号的。

非捕获分组 (Non-capturing Groups):

如果你只需要使用圆括号进行分组,但不需要捕获其内容,可以使用非捕获分组 (?:...)。这可以提高性能,并且不会占用捕获组的编号。

“`ruby

匹配 “cat” 或 “dog”,后面跟一个或多个 “s”

使用捕获分组:

puts “cats and dogs”.match(/(cat|dog)s+/).to_s # => cats

match_data[1] 是 “cat” 或 “dog”

使用非捕获分组:

puts “cats and dogs”.match(/(?:cat|dog)s+/).to_s # => cats

如果这里是第一个捕获组,match_data[1] 将是 nil 或下一个捕获组

“`

在不需要引用分组内容时,优先使用非捕获分组。

命名捕获组 (Named Capture Groups):

为了提高可读性,可以使用 (?<name>...) 的语法为捕获组命名。

“`ruby

使用命名捕获组匹配日期

date_pattern_named = /(?\d{4})-(?\d{2})-(?\d{2})/
text = “Birth date: 1990-05-15”

match_data = text.match(date_pattern_named)

if match_data
puts “Full match: #{match_data[0]}”
puts “Year: #{match_data[:year]}” # 使用符号作为键访问
puts “Month: #{match_data[“month”]}” # 也可以使用字符串作为键访问
puts “Day: #{match_data[:day]}”
puts “As hash: #{match_data.names}” # 捕获组名称数组
puts “As hash: #{match_data.to_h}” # 转换为名称 => 捕获内容的哈希
end
“`

命名捕获组使得从 MatchData 对象中提取特定信息变得更加清晰和方便。

10. 选择符 (Alternation)

竖线 | 用作选择符,表示“或”的关系。它允许你匹配多个模式中的任意一个。

“`ruby

匹配 “cat” 或 “dog”

puts “The cat sat.”.match(/cat|dog/).to_s # => cat
puts “The dog barked.”.match(/cat|dog/).to_s # => dog

结合分组:匹配 “cat” 或 “dog”,后面跟着 “s”

puts “cats”.match(/(cat|dog)s/).to_s # => cats
puts “dogs”.match(/(cat|dog)s/).to_s # => dogs
“`

没有分组时,| 的作用范围是整个正则表达式。例如,cat|dog 匹配字符串中的 catdog。如果写成 cats|dogs,则匹配完整的 cats 字符串或完整的 dogs 字符串。使用分组 (cat|dog)s 则表示匹配 catdog 后紧跟 s

11. 修饰符 (Options)

修饰符用于改变正则表达式的匹配行为。在 Ruby 中,修饰符可以放在字面量斜杠 / 后面,或者作为 Regexp.new 的第二个参数。

常用的修饰符:

  • i (case-insensitive): 忽略大小写进行匹配。
    ruby
    puts "Hello World".match(/world/i).to_s # => World
    puts "HELLO WORLD".match(/world/i).to_s # => HELLO WORLD
  • m (multiline): 使 . 匹配包括换行符 \n 在内的任意字符。注意,如前所述,在 Ruby 中 ^$ 默认就是多行感知的,这个选项主要影响 .
    ruby
    text_multiline = "Line 1\nLine 2"
    # 默认 . 不匹配 \n
    puts text_multiline.match(/Line.*Line/).to_s # => ""
    # /m 选项使 . 匹配 \n
    puts text_multiline.match(/Line.*Line/m).to_s # => Line 1\nLine 2
  • x (extended): 忽略模式中的非转义空白字符,并支持 # 开头的行内注释。这使得编写复杂、可读性强的正则表达式成为可能。
    “`ruby
    # 匹配 YYYY-MM-DD 格式的日期,使用 /x 提高可读性
    date_pattern_readable = /
    (?\d{4}) # 匹配年份,4位数字

    • 匹配字面量连字符

      (?\d{2}) # 匹配月份,2位数字

    • 匹配字面量连字符

      (?\d{2}) # 匹配日期,2位数字
      /x

    text = “Date: 2024-01-10”
    match_data = text.match(date_pattern_readable)
    puts “Readable pattern match: #{match_data.to_h}” # => {“year”=>”2024”, “month”=>”01”, “day”=>”10”}
    注意,在 `/x` 模式下,如果你需要匹配字面意义的空格或 `#`,你需要对它们进行转义 (`\ ` 或 `\#`)。
    * `o` (compile once): 如果正则表达式字面量或 `Regexp.new` 的参数包含内嵌的 Ruby 变量,使用 `/o` 选项可以确保正则表达式只在变量第一次求值时编译一次。这对于循环中使用的正则表达式可以提高性能。
    ruby
    word = “apple”
    1000.times do
    # 每次迭代都会重新构建和编译正则表达式 (如果 word 可能变化)
    # “some text about #{word}”.match(/#{word}/)

    # 使用 /o 选项,正则表达式只在 word 首次求值时编译
    “some text about #{word}”.match(/#{word}/o)
    end
    ``
    通常字面量
    /pattern/本身就是编译一次的,/o主要用于字面量中包含#{…}` 插值的情况。

修饰符可以组合使用,例如 /pattern/ix

12. 替换 (Substitution)

Ruby 提供了 subgsub 方法,结合正则表达式进行字符串替换。

  • sub(pattern, replacement): 替换第一个匹配到的部分。
  • gsub(pattern, replacement): 替换所有匹配到的部分。

使用字符串进行替换:

替换字符串可以使用特殊的序列来引用捕获组:

  • \&\0: 引用整个匹配到的字符串 (match_data[0])。
  • \ + 数字: 引用对应的捕获组 (match_data[1], match_data[2], 等)。例如 \1, \2.
  • \': 引用匹配点之后的字符串 (match_data.post_match)。
  • \``: 引用匹配点之前的字符串 (match_data.pre_match`)。

“`ruby
text = “Replace the first word and the second word.”

替换第一个 “word” 为 “match”

puts text.sub(/word/, “match”) # => Replace the first match and the second word.

替换所有 “word” 为 “match”

puts text.gsub(/word/, “match”) # => Replace the first match and the second match.

使用捕获组进行替换:将 YYYY-MM-DD 格式转换为 DD/MM/YYYY

date = “Date is 2023-10-26.”
puts date.sub(/(\d{4})-(\d{2})-(\d{2})/, ‘\3/\2/\1’) # => Date is 26/10/2023. (\3是日,\2是月,\1是年)

使用命名捕获组

date_named = “Date is 2023-10-26.”
puts date_named.sub(/(?\d{4})-(?\d{2})-(?\d{2})/, ‘\k/\k/\k‘) # => Date is 26/10/2023.

或更推荐的 Ruby 语法:

puts date_named.sub(/(?\d{4})-(?\d{2})-(?\d{2})/) { |match|
“#{match[:day]}/#{match[:month]}/#{match[:year]}”
} # => Date is 26/10/2023.
“`

使用块进行替换:

subgsub 也可以接受一个块。匹配到的内容会作为参数传递给块,块的返回值将作为替换字符串。这使得替换逻辑更加灵活。

“`ruby
text = “prices: $1.50, $10, $0.75”

将所有美元价格转换为两小数位格式

puts text.gsub(/\$\d+(.\d+)?/) { |price_match|
# price_match 就是匹配到的 “$1.50”, “$10”, “$0.75” 等
# 移除 $ 符号,转换为浮点数,再格式化
amount = price_match[1..-1].to_f # 切掉 $ 符号并转为浮点数
sprintf(“$%.2f”, amount) # 格式化为两小数位
}

=> prices: $1.50, $10.00, $0.75

“`

使用块进行替换是处理复杂替换逻辑的强大方式。

13. 分割 (Splitting)

split 方法可以使用正则表达式作为分隔符来分割字符串,返回一个字符串数组。

“`ruby
text = “apple,orange;banana grape”

使用逗号或分号或空格作为分隔符

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

移除开头和结尾的空白符

text_with_whitespace = ” word1 word2 \n word3 ”
words = text_with_whitespace.split(/\s+/) # 使用一个或多个空白符作为分隔
puts words.inspect # => [“word1”, “word2”, “word3”]
“`

如果正则表达式包含捕获组,那么捕获到的分隔符内容也会包含在结果数组中。

“`ruby
text = “item1:value1;item2:value2”

使用冒号或分号作为分隔符,并捕获冒号和分号

parts = text.split(/(:|;)/)
puts parts.inspect # => [“item1”, “:”, “value1”, “;”, “item2”, “:”, “value2”]
“`

通常在 split 中使用非捕获分组 (?:...) 可以避免捕获分隔符本身。

“`ruby

使用非捕获分组分隔符

parts = text.split(/(?:|;)/)
puts parts.inspect # => [“item1”, “value1”, “item2”, “value2”]
“`

14. 实践示例

示例 1: 简单的邮箱格式验证 (简化版)

“`ruby
def is_simple_email?(email)
# 模式解释:
# ^ – 匹配字符串开头
# \w+ – 匹配一个或多个单词字符 (用户名部分)
# @ – 匹配字面量 @ 符号
# \w+ – 匹配一个或多个单词字符 (域名第一部分)
# (.\w+)+ – 匹配一个点号后跟一个或多个单词字符,整个模式 (.\w+) 重复一次或多次 (域名后续部分,如 .com, .co.uk)
# $ – 匹配字符串结尾
email =~ /^\w+@\w+(.\w+)+$/i # 使用 =~ 快速检查是否匹配,/i 忽略大小写
end

puts is_simple_email?(“[email protected]”) # => true (返回匹配索引)
puts is_simple_email?(“[email protected]”) # => true (虽然模式更复杂才能完全匹配所有邮箱,但这个模式也能匹配一部分)
puts is_simple_email?(“invalid-email”) # => nil
puts is_simple_email?(“[email protected]”) # => nil
“`

示例 2: 从日志行中提取信息

假设日志行格式为 [TIMESTAMP] [LEVEL] Message...

“`ruby
log_line = “[2023-10-26 10:00:00] [INFO] User logged in.”

模式解释:

[ – 匹配字面量 [

(?[^]]+) – 捕获时间戳,匹配一个或多个非 ] 字符

] – 匹配字面量 ]

\s+ – 匹配一个或多个空白符

[ – 匹配字面量 [

(?\w+) – 捕获级别,匹配一个或多个单词字符

] – 匹配字面量 ]

\s+ – 匹配一个或多个空白符

(?.*) – 捕获消息,匹配任意字符 0 次或多次(到行尾)

log_pattern = /[(?[^]]+)]\s+[(?\w+)]\s+(?.*)/

match_data = log_line.match(log_pattern)

if match_data
puts “Timestamp: #{match_data[:timestamp]}” # => 2023-10-26 10:00:00
puts “Level: #{match_data[:level]}” # => INFO
puts “Message: #{match_data[:message]}” # => User logged in.
else
puts “Log format mismatch”
end
“`

15. 更多高级概念 (简要提及)

正则表达式还有一些更高级的概念,入门阶段可以先了解它们的存在:

  • 前瞻和后顾 (Lookarounds): 用于匹配一个位置,这个位置后面或前面跟着(或不跟着)某个模式,但这个模式本身不包含在匹配结果中。例如 (?=...) 前瞻肯定,(?<=...) 后顾肯定。
  • 反向引用 (Backreferences): 在同一个正则表达式模式中,使用 \1, \2 等来引用前面已经捕获到的组匹配的内容。例如 (\w+)\s+\1 可以匹配重复的单词,如 “hello hello”。
  • 占有型量词 (Possessive Quantifiers): 在量词后面加上 + (例如 *+, ++, {n,m}+)。它们比贪婪量词更进一步,一旦匹配了尽可能多的字符,就不会回溯。性能更高,但可能导致一些模式无法匹配。

这些高级特性在处理非常复杂的模式时非常有用,但对于大多数日常任务,本教程介绍的基础知识已经足够。

16. 学习和调试正则表达式的技巧

  • 从简单开始: 尝试匹配最小的部分,然后逐步增加复杂度。
  • 逐步构建: 不要试图一次写出一个复杂的正则表达式。先写核心部分,再添加边界、量词、分组等。
  • 利用在线工具: 许多网站提供了在线的正则表达式测试工具,如 Rubular (rubular.com) 或 RegExr (regexr.com)。你可以在这些工具中输入你的正则表达式和测试文本,实时查看匹配结果和解释。这对于学习和调试非常有帮助。
  • 使用 /x 选项: 对于复杂的正则表达式,使用 /x 选项添加注释和格式化,可以大大提高可读性和可维护性。
  • 查阅文档: Ruby 的官方文档是最好的参考资料。Regexp 类文档详细列出了所有支持的元字符、语法和选项。
  • 实践、实践、再实践: 正则表达式需要大量的练习才能熟练掌握。尝试在自己的项目中使用它来解决文本处理问题。

17. 总结

正则表达式是处理文本模式的强大工具。Ruby 对正则表达式提供了原生支持,通过字面量 /.../Regexp.new 创建,使用 =~match 进行匹配。

本教程详细介绍了 Ruby 正则表达式的关键元素:

  • 基本元字符: .\
  • 字符集合: [], [^], -
  • 量词: ?, *, +, {n}, {n,}, {n,m},以及贪婪与非贪婪 (? 后缀)
  • 边界匹配: ^, $, \b, \B, \A, \Z, \z
  • 特殊序列: \d, \D, \w, \W, \s, \S
  • 分组与捕获: (), (?:), (?<name>...)
  • 选择符: |
  • 修饰符: i, m, x, o
  • 常用方法: match, =~, sub, gsub, split

掌握这些基础知识,你就能解决 Ruby 中大部分文本处理任务。随着经验的积累,你可以进一步探索更高级的正则表达式特性。

正则表达式的语法虽然紧凑,但也可能变得复杂难以阅读。关键在于多加练习,并利用好调试工具和 /x 这样的提高可读性的功能。

希望这篇教程能帮助你迈出 Ruby 正则表达式学习的第一步!祝你在模式匹配的世界里探索愉快!


发表评论

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

滚动至顶部