ZetCode

Ruby 正则表达式

最后修改于 2023 年 10 月 18 日

在本 Ruby 教程中,我们将讨论 Ruby 中的正则表达式。

正则表达式用于文本搜索和更高级的文本操作。 正则表达式内置于 grep、sed 等工具中; 文本编辑器如 vi、emacs; 编程语言如 Tcl、Perl、Python。 Ruby 也内置了对正则表达式的支持。

从另一个角度来看,正则表达式语法构成了一种用于匹配文本的特定领域语言。

模式是定义我们正在搜索或操作的文本的正则表达式。 它由文本字面量和元字符组成。 模式放在两个分隔符内。 在 Ruby 中,这些是 // 字符。 它们通知正则表达式函数模式的开始和结束位置。

以下是元字符的部分列表

.匹配任何单个字符。
*匹配前一个元素零次或多次。
[ ]括号表达式。 匹配括号内的字符。
[^ ]匹配未包含在括号内的单个字符。
^匹配字符串内的起始位置。
$匹配字符串内的结束位置。
|交替运算符。

=~ 运算符将正则表达式与字符串匹配,如果找到,则返回字符串中匹配的偏移量,否则返回 nil。 Regexp 类用于开发正则表达式。 还有两种创建正则表达式的简写方式。 以下示例将展示它们。

simple.rb
#!/usr/bin/ruby

re = Regexp.new 'Jane'
p "Jane is hot".match re

p "Jane is hot" =~ /Jane/
p "Jane is hot".match %r{Jane}

在第一个示例中,我们展示了将正则表达式应用于字符串的三种方法。

re = Regexp.new 'Jane'
p "Jane is hot".match re

在上面的两行中,我们创建了一个包含简单正则表达式文本的 Regexp 对象。 使用 match 方法,我们将此正则表达式对象应用于“Jane is hot”句子。 我们检查单词“Jane”是否在句子中。

p "Jane is hot" =~ /Jane/
p "Jane is hot".match %r{Jane}

这两行做同样的事情。 双正斜杠 // 和 %r{} 字符是更冗长的第一种方法的简写形式。 在本教程中,我们使用正斜杠。 这在许多语言中是事实标准。

$ ./simple.rb
#<MatchData "Jane">
0
#<MatchData "Jane">

在所有三种情况下都有匹配。 match 方法返回匹配的数据,如果未找到匹配则返回 nil=~ 运算符返回匹配文本的第一个字符,否则返回 nil

点字符

点字符是一个正则表达式字符,它匹配任何单个字符。 请注意,必须存在某个字符; 它可能被省略。

dot.rb
#!/usr/bin/ruby

p "Seven".match /.even/
p "even".match /.even/
p "eleven".match /.even/
p "proven".match /.even/

在第一个示例中,我们使用 match 方法将正则表达式应用于字符串。 match 方法在成功时返回匹配的数据,否则返回 nil

p "Seven".match /.even/

“Seven”是我们在其上调用 match 方法的字符串。 该方法的参数是模式。 /.even/ 正则表达式模式查找以任意字符开头,后跟“even”字符的文本。

$ ./dot.rb
#<MatchData "Seven">
nil
#<MatchData "leven">
nil

从输出中,我们可以看到哪些字符串匹配,哪些没有匹配。

正如我们上面所说,如果有点字符,则必须有任意字符。 它可能被省略。 如果我们想搜索文本,其中字符可能被省略怎么办? 换句话说,我们想要同时适用于“seven”和“even”的模式。 为此,我们可以使用 ? 重复字符。 ? 重复字符表示前面的字符可以存在 0 次或 1 次。

dot2.rb
#!/usr/bin/ruby

p "seven".match /.even/
p "even".match /.even/
p "even".match /.?even/

该脚本使用 ? 重复字符。

p "even".match /.even/

此行打印 nil,因为正则表达式期望在“even”字符串之前有一个字符。

p "even".match /.?even/

在这里,我们稍微修改了正则表达式。 .? 代表无字符或一个任意字符。 这次有匹配。

$ ./dot2.rb
#<MatchData "seven">
nil
#<MatchData "even">

Ruby 正则表达式方法

在前面的两个示例中,我们使用 match 方法来处理正则表达式。 除了 match 之外的其他方法也接受正则表达式作为参数。

regex_methods.rb
#!/usr/bin/ruby

puts "motherboard" =~ /board/
puts "12, 911, 12, 111"[/\d{3}/]

puts "motherboard".gsub /board/, "land"

p "meet big deep nil need".scan /.[e][e]./
p "This is Sparta!".split(/\s/)

该示例展示了一些可以使用正则表达式的方法。

puts "motherboard" =~ /board/

=~ 是一个运算符,它将右侧的正则表达式应用于左侧的字符串。

puts "12, 911, 12, 111"[/\d{3}/]

正则表达式可以放在字符串后面的方括号之间。 此行打印第一个包含三个数字的字符串。

puts "motherboard".gsub /board/, "land"

使用 gsub 方法,我们将“board”字符串替换为“land”字符串。

p "meet big deep nil need".scan /.[e][e]./

scan 方法在字符串中查找匹配项。 它会查找所有出现次数,而不仅仅是第一次出现。 该行打印所有与模式匹配的字符串。

p "This is Sparta!".split(/\s/)

split 方法使用给定的正则表达式作为分隔符来分割字符串。 \s 字符类型代表任何空白字符。

$ ./regex_methods.rb
6
911
motherland
["meet", "deep", "need"]
["This", "is", "Sparta!"]

我们看到了输出。

Ruby 特殊变量

一些使用正则表达式的方法会激活一些特殊变量。 它们包含最后匹配的字符串、最后匹配之前的字符串和最后匹配之后的字符串。 这些变量使程序员的工作更容易。

svars.rb
#!/usr/bin/ruby

puts "Her name is Jane" =~ /name/

p $`
p $&
p $'

该示例展示了三个特殊变量。

puts "Her name is Jane" =~ /name/

在这行中,我们有一个简单的正则表达式匹配。 我们在“Her name is Jane”句子中查找“name”字符串。 我们使用 =~ 运算符。 此运算符还设置了三个特殊变量。 该行返回数字 4,这是匹配的开始位置。

p $`

$` 特殊变量包含最后匹配之前的文本。

p $&

$& 包含匹配的文本。

p $'

并且 $' 变量包含最后匹配之后的文本。

$ ./svars.rb
4
"Her "
"name"
" is Jane"

锚点

锚点匹配给定文本内字符的位置。 我们展示了三个锚定字符。 ^ 字符匹配行的开头。 $ 字符匹配行的结尾。 \b 字符匹配单词边界。

anchors.rb
#!/usr/bin/ruby

sen1 = "Everywhere I look I see Jane"
sen2 = "Jane is the best thing that happened to me"

p sen1.match /^Jane/
p sen2.match /^Jane/

p sen1.match /Jane$/
p sen2.match /Jane$/

在第一个示例中,我们使用 ^$ 锚定字符。

sen1 = "Everywhere I look I see Jane"
sen2 = "Jane is the best thing that happened to me"

我们有两个句子。 单词“Jane”位于第一个句子的开头和第二个句子的结尾。

p sen1.match /^Jane/
p sen2.match /^Jane/

在这里,我们查找单词“Jane”是否位于这两个句子的开头。

p sen1.match /Jane$/
p sen2.match /Jane$/

在这里,我们查找文本是否在句子的结尾处匹配。

$ ./anchors.rb
nil
#<MatchData "Jane">
#<MatchData "Jane">
nil

这些是结果。

一个常见的请求是仅包括整个单词的匹配。 默认情况下,我们计算任何匹配项,包括在更大或复合词中的匹配项。 让我们看一个例子来澄清问题。

boundaries.rb
#!/usr/bin/ruby

text = "The cat also known as the domestic cat is a small,
usually furry, domesticated, carnivorous mammal."

p text.scan /cat/

p $`
p $&
p $'

我们有一个句子。 在这个句子中,我们查找字符串 cat。 使用 scan,我们查找句子中所有“cat”字符串,而不仅仅是第一次出现。

text = "The cat also known as the domestic cat is a small,
usually furry, domesticated, carnivorous mammal."

问题是文本中有三个“cat”字符串。 除了匹配指代哺乳动物的“cat”之外,/cat/ 还匹配单词“domesticated”中的第 8-10 个字母。 这不是我们在此情况下所寻找的。

$ ./boudaries.rb
["cat", "cat", "cat"]
"The cat also known as the domestic cat is a small, \nusually furry, domesti"
"cat"
"ed, carnivorous mammal."

在下一个示例中,使用 \b 锚点将消除对“domesticated”的最后一次匹配。

\b 字符用于设置我们正在寻找的单词的边界。

boundaries2.rb
#!/usr/bin/ruby

text = "The cat also known as the domestic cat is a small,
usually furry, domesticated, carnivorous mammal."

p text.scan /\bcat\b/

p $`
p $&
p $'

通过包含 \b 元字符改进了该示例。

p text.scan /\bcat\b/

使用上面的正则表达式,我们查找“cat”字符串作为整个单词。 我们不计算子词。

$ ./boundaries2.rb
["cat", "cat"]
"The cat also known as the domestic "
"cat"
" is a small, \nusually furry, domesticated, carnivorous mammal."

这次有两个匹配项。 并且特殊变量正确显示了最后匹配之前和之后的文本。

字符类

我们可以使用方括号将字符组合成字符类。 字符类匹配括号中指定的任何字符。 /[ab]/ 模式表示 a 或 b,与 /ab/ 表示 a 后面跟着 b 相对。

char_classes.rb
#!/usr/bin/ruby

words = %w/ sit MIT fit fat lot pad /

pattern = /[fs]it/

words.each do |word|
   if word.match pattern
       puts "#{word} matches the pattern"
   else
       puts "#{word} does not match the pattern"
   end
end

我们有一个由六个三个字母的单词组成的数组。 我们将正则表达式应用于数组的字符串,该数组包含特定的字符集。

pattern = /[fs]it/

这是模式。 该模式在数组中查找“fit”和“sit”字符串。 我们使用字符集中的“f”或“s”。

$ ./char_classes.rb
sit matches the pattern
MIT does not match the pattern
fit matches the pattern
fat does not match the pattern
lot does not match the pattern
pad does not match the pattern

有两个匹配项。

在下一个示例中,我们进一步探索字符类。

char_classes2.rb
#!/usr/bin/ruby

p "car".match %r{[abc][a][rs]}
p "car".match /[a-r]+/
p "23af 433a 4ga".scan /\b[a-f0-9]+\b/

该示例有三个带有字符类的正则表达式。

p "car".match %r{[abc][a][rs]}

此行中的正则表达式由三个字符类组成。 每个字符类对应一个字符。 [abc] 是 a、b 或 c。 [a] 仅为 a。 第三个 [rs] 为 r 或 s。 与“car”字符串匹配。

p "car".match /[a-r]+/

我们可以在字符类中使用连字符 — 字符。 连字符是一个元字符,表示一个包含字符的范围:此处为 a、b、c、d、e、f、g、h、i、j、k、l、m、n、o、p、q 或 r。 由于字符类仅适用于一个字符,因此我们还使用 + 重复字符。 这表示字符集中的上一个字符可以重复一次或多次。 “car”字符串满足这些条件。

p "23af 433a 4ga".scan /\b[a-f0-9]+\b/

在这行中,我们有一个由三个子字符串组成的字符串。 使用 scan 方法,我们检查十六进制数。 我们有两个范围。 第一个 [a-f] 代表从 a 到 f 的字符。 第二个 [0-9] 代表数字 0 到 9。 + 指定这些字符可以重复多次。 最后,\b 元字符创建边界,仅接受仅由这些字符组成的字符串。

$ ./char_classes2.rb
#<MatchData "car">
#<MatchData "car">
["23af", "433a"]

如果字符类的第一个字符是插入符号 ^,则该类被反转。 它匹配除了指定的字符之外的任何字符。

caret.rb
#!/usr/bin/ruby

p "ABC".match /[^a-z]{3}/
p "abc".match /[^a-z]{3}/

在该示例中,我们在字符类中使用插入符号字符。

p "ABC".match /[^a-z]{3}/

我们寻找一个包含 3 个字母的字符串。 这些字母可能不是从 a 到 z 的字母。 “ABC”字符串与正则表达式匹配,因为所有三个字符都是大写字符。

p "abc".match /[^a-z]{3}/

这个“abc”字符串不匹配。 所有这三个字符都在从搜索中排除的范围内。

$ ./caret.rb
#<MatchData "ABC">
nil

在这里,我们有示例输出。

量词

标记或组后的量词指定前一个元素允许出现的频率。

?     - 0 or 1 match
*     - 0 or more
+     - 1 or more
{n}   - exactly n
{n,}  - n or more
{,n}  - n or less (??)
{n,m} - range n to m

上面是常见量词的列表。

nchars.rb
#!/usr/bin/ruby

p "seven dig moon car lot fire".scan /\w{3}/
p "seven dig moon car lot fire".scan /\b\w{3}\b/

在该示例中,我们希望选择那些正好包含三个字符的单词。 \w 字符是一个单词字符,\w{3} 表示上一个单词字符出现三次。

p "seven dig moon car lot fire".scan /\w{3}/

第一行只是从每个字符串中剪掉前三个字符。 这与我们想要的并不完全相同。

p "seven dig moon car lot fire".scan /\b\w{3}\b/

这是一个改进的搜索。 我们将之前的模式放在 \b 边界元字符之间。 现在,搜索将仅查找正好包含三个字符的单词。

$ ./nchars.rb
["sev", "dig", "moo", "car", "lot", "fir"]
["dig", "car", "lot"]

{n,m} 是一个用于包含从 n 到 m 个字符的字符串的重复结构。

rchars.rb
#!/usr/bin/ruby

p "I dig moon lottery it fire".scan /\b\w{2,4}\b/

在上面的示例中,我们选择包含两个、三个或四个字符的单词。 我们再次使用边界 \b 元字符来选择整个单词。

$ ./rchars.rb
["dig", "moon", "it", "fire"]

该示例打印一个包含 2-4 个字符的单词的数组。

在下一个示例中,我们介绍了 ? 元字符。 跟在字符后面的 ? 是可选的。 从形式上看,在 ? 之前的字符可以出现一次或 0 次。

qmark.rb
#!/usr/bin/ruby

p "color colour colors colours".scan /colou?rs/
p "color colour colors colours".scan /colou?rs?/

p "color colour colors colours".scan /\bcolor\b|\bcolors\b|\bcolour\b|\bcolours\b/

假设我们有一个文本,我们想在其中查找颜色词。 这个词有两种不同的拼写方式,英语“colour”和美式英语“color”。 我们想找到这两个出现的地方,再加上我们也想找到它们的名词复数形式。

p "color colour colors colours".scan /colou?rs/

colou?rs 模式查找“colours”和“colors”。 在 ? 元字符之前的 u 字符是可选的。

p "color colour colors colours".scan /colou?rs?/

colou?rs? 模式使 u 和 s 字符成为可选。 因此,我们找到了所有四个颜色组合。

p "color colour colors colours".scan /\bcolor\b|\bcolors\b|\bcolour\b|\bcolours\b/

相同的请求可以使用交替来编写。

$ ./qmark.rb
["colors", "colours"]
["color", "colour", "colors", "colours"]
["color", "colour", "colors", "colours"]

在本节的最后一个示例中,我们展示了 + 元字符。 它允许前面的字符重复 1 次或多次。

numbers.rb
#!/usr/bin/ruby

nums = %w/ 234 1 23 53434 234532453464 23455636
    324f 34532452343452 343 2324 24221 34$34232/

nums.each do |num|
    m = num.match /[0-9]+/

    if m.to_s.eql? num
        puts num
    end
end

在该示例中,我们有一个数字数组。 数字可以有一个或多个数字字符。

nums = %w/ 234 1 23 53434 234532453464 23455636
    324f 34532452343452 343 2324 24221 34$34232/

这是一个字符串数组。 其中两个不是数字,因为它们包含非数字字符。 它们必须被排除。

nums.each do |num|
    m = num.match /[0-9]+/

    if m.to_s.eql? num
        puts num
    end
end

我们遍历数组并将正则表达式应用于每个字符串。 该表达式是 [0-9]+,它代表从 0..9 的任何字符,重复 0 次或多次。 默认情况下,正则表达式也会查找子字符串。 在 34$34232 中,引擎认为 34 是一个数字。 \b 边界在此处不起作用,因为我们没有具体的字符,并且引擎不知道从哪里停止查找。 这就是为什么我们在该块中包含了一个 if 条件。 仅当匹配项等于原始字符串时,该字符串才被视为数字。

$ ./numbers.rb
234
1
23
53434
234532453464
23455636
34532452343452
343
2324
24221

这些值是数字。

不区分大小写的搜索

我们可以执行不区分大小写的搜索。 正则表达式可以后跟一个选项。 它是一个以某种方式修改模式的单个字符。 在不区分大小写的搜索的情况下,我们应用 i 选项。

icase.rb
#!/usr/bin/ruby

p "Jane".match /Jane/
p "Jane".match /jane/
p "Jane".match /JANE/

p "Jane".match /jane/i
p "Jane".match /Jane/i
p "Jane".match /JANE/i

该示例显示了区分大小写和不区分大小写的搜索。

p "Jane".match /Jane/
p "Jane".match /jane/
p "Jane".match /JANE/

在这三行中,字符必须与模式完全匹配。 只有第一行给出匹配项。

p "Jane".match /jane/i
p "Jane".match /Jane/i
p "Jane".match /JANE/i

在这里,我们使用 i 选项,它跟随第二个 / 字符。 我们执行不区分大小写的搜索。 所有这三行都匹配。

$ ./icase.rb
#<MatchData "Jane">
nil
nil
#<MatchData "Jane">
#<MatchData "Jane">
#<MatchData "Jane">

交替

下一个示例解释了交替运算符 |。 此运算符使创建具有多个选择的正则表达式成为可能。

alternation.rb
#!/usr/bin/ruby

names = %w/Jane Thomas Robert Lucy Beky
    John Peter Andy/

pattern = /Jane|Beky|Robert/

names.each do |name|

    if name =~ pattern
        puts "#{name} is my friend"
    else
        puts "#{name} is not my friend"
    end
end

我们在 names 数组中有 8 个名称。 我们在那个数组中查找字符串的多个组合。

pattern = /Jane|Beky|Robert/

这是搜索模式。 它说,Jane、Beky 和 Robert 是我的朋友。 如果你找到他们中的任何一个,你就会找到我的朋友。

$ ./alternation.rb
Jane is my friend
Thomas is not my friend
Robert is my friend
Lucy is not my friend
Beky is my friend
John is not my friend
Peter is not my friend
Andy is not my friend

在这里,我们看到了脚本的输出。

子模式

我们可以使用括号 在模式内创建子模式。

subpatterns.rb
#!/usr/bin/ruby

p "bookworm" =~ /book(worm)?$/
p "book" =~ /book(worm)?$/
p "worm" =~ /book(worm)?$/
p "bookstore" =~ /book(worm)?$/

我们有以下正则表达式模式:book(worm)?$(worm) 是一个子模式。 只有两个字符串可以匹配:'book' 或 'bookworm'。 ? 字符跟随子模式,这意味着子模式可能在最终模式中出现 0 次,1 次。 $ 字符在这里用于字符串的精确结尾匹配。 如果没有它,像 bookstore 和 bookmania 这样的单词也会匹配。

subpatterns2.rb
#!/usr/bin/ruby

p "book" =~ /book(shelf|worm)?$/
p "bookshelf" =~ /book(shelf|worm)?$/
p "bookworm" =~ /book(shelf|worm)?$/
p "bookstore" =~ /book(shelf|worm)?$/

子模式通常与交替结合使用,以创建多个单词组合。例如,book(shelf|worm)匹配'bookshelf'和'bookworm',而book(shelf|worm)?匹配'bookshelf','bookworm'和'book'。

$ ./subpatterns2.rb
0
0
0
nil

最后一个子模式不匹配。请记住,0 并不意味着没有匹配。对于 =~ 运算符,它指的是匹配字符串的第一个字符的索引。

电子邮件示例

在最后一个例子中,我们创建一个正则表达式模式来检查电子邮件地址。

email.rb
#!/usr/bin/ruby

emails = %w/ luke@gmail.com andy@yahoo.com 23214sdj^as
    f3444@gmail.com /

pattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9-]+\.[a-zA-Z.]{2,5}$/

emails.each do |email|

    if email.match pattern
        puts "#{email} matches"
    else
        puts "#{email} does not match"
    end

end

请注意,此示例仅提供一种解决方案。它不一定是最好的解决方案。

emails = %w/ luke@gmail.com andy@yahoocom 23214sdj^as
    f3444@gmail.com /

这是一个电子邮件数组。其中只有两个是有效的。

pattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9-]+\.[a-zA-Z.]{2,5}$/

这是模式。第一个 ^ 和最后一个 $ 字符在这里是为了获得精确的模式匹配。不允许模式前后有任何字符。电子邮件被分为五个部分。

第一部分是本地部分。这通常是公司、个人或昵称的名称。[a-zA-Z0-9._-]+ 列出了我们可以在本地部分中使用的所有可能字符。它们可以被使用一次或多次。第二部分是字面 @ 字符。第三部分是域名部分。它通常是电子邮件提供商的域名:例如,yahoo 或 gmail。字符集 [a-zA-Z0-9-]+ 指定了可以在域名中使用的所有字符。+ 量词使得

使用一个或多个这些字符。第四部分是点字符。它前面是转义字符 \。这是因为点字符是一个元字符,具有特殊含义。通过转义它,我们得到一个字面点。最后一部分是顶级域。该模式如下所示:[a-zA-Z.]{2,5} 顶级域可以有 2 到 5 个字符,如 sk、net、info 或 travel。还有一个点字符。这是因为一些顶级域(如 co.uk)有两个部分。

$ ./email.rb
luke@gmail.com matches
andy@yahoocom does not match
23214sdj^as does not match
f3444@gmail.com matches

正则表达式将两个字符串标记为有效的电子邮件地址。

在本章中,我们介绍了 Ruby 中的正则表达式。