Ruby 正则表达式入门教程:模式匹配的利器
正则表达式(Regular Expression,简称 RegEx 或 RegExp)是一种强大的文本处理工具,它使用一套特殊字符组成的模式来描述和匹配字符串中的特定序列。无论是在数据验证、文本搜索、替换、分割,还是在更复杂的文本解析任务中,正则表达式都扮演着核心角色。
对于 Ruby 程序员来说,掌握正则表达式是提升开发效率、处理文本数据的必备技能。Ruby 对正则表达式提供了良好的内置支持,语法简洁且功能强大。
本教程将带你从零开始,逐步深入 Ruby 正则表达式的世界,详细介绍其基本概念、语法元素、常用方法以及一些实践技巧。
1. 什么是正则表达式?为什么使用它?
想象一下,你需要从一堆文本文件中找出所有符合特定格式的电话号码(例如:XXX-XXX-XXXX),或者验证用户输入的邮箱地址是否合法,或者从日志文件中提取出所有错误信息的时间戳。手动编写代码来处理这些任务可能会非常繁琐,需要大量的条件判断和循环。
正则表达式提供了一种声明式的方式来描述这些复杂的文本模式。你只需要定义好“长什么样”的模式,然后就可以让程序去查找、匹配、替换或分割符合这个模式的文本。
正则表达式的优点:
- 强大和灵活: 可以描述非常复杂的文本模式。
- 高效: 底层通常经过高度优化,执行速度快。
- 简洁: 复杂的匹配逻辑可以用相对紧凑的模式表示(虽然有时也会变得难以阅读)。
- 通用性: 正则表达式的概念和大部分语法在许多编程语言(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(/cat/).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
。但在启用多行模式 (/m
或 Regexp::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)
圆括号 ()
在正则表达式中有两个主要作用:
- 分组 (Grouping): 将多个元素视为一个整体,以便对其应用量词或进行其他操作。
- 捕获 (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 = /(?
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
匹配字符串中的 cat
或 dog
。如果写成 cats|dogs
,则匹配完整的 cats
字符串或完整的 dogs
字符串。使用分组 (cat|dog)s
则表示匹配 cat
或 dog
后紧跟 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 WORLDm
(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` 模式下,如果你需要匹配字面意义的空格或 `#`,你需要对它们进行转义 (`\ ` 或 `\#`)。
ruby
* `o` (compile once): 如果正则表达式字面量或 `Regexp.new` 的参数包含内嵌的 Ruby 变量,使用 `/o` 选项可以确保正则表达式只在变量第一次求值时编译一次。这对于循环中使用的正则表达式可以提高性能。
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 提供了 sub
和 gsub
方法,结合正则表达式进行字符串替换。
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(/(?
或更推荐的 Ruby 语法:
puts date_named.sub(/(?
“#{match[:day]}/#{match[:month]}/#{match[:year]}”
} # => Date is 26/10/2023.
“`
使用块进行替换:
sub
和 gsub
也可以接受一个块。匹配到的内容会作为参数传递给块,块的返回值将作为替换字符串。这使得替换逻辑更加灵活。
“`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 = /[(?
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 正则表达式学习的第一步!祝你在模式匹配的世界里探索愉快!