212 Ruby 正则【Rails后端开发训练营】

脚本和自动化任务通常需要从输入数据中提取文本的特定部分或将它们从一种格式修改为另一种格式。本章将帮助您学习正则表达式。

为什么需要正则表达式

正则表达式是一种用于文本处理的多功能工具。大部分的脚本语言把正则表达式内置于标准库中。即使没有,那也能找到第三方库。

正则表达式的语法和特性因编程语言而异。Ruby 的正则是基于 Onigmo 正则表示库。

虽然 String 类 预装了各种处理文本的方法。但是依然无法代替正则表达式。正则表达式预期说是一个库,不如说一门用于文本处理的迷你编程语言。

一下是一些常见的正则表达式应用场景:

清理字符串以确保它满足一组已知的规则。例如,检查给定的字符串是否与密码规则匹配。
在抽象级别过滤或提取部分,如字母、数字、标点符号等。
合格的字符串替换。例如,在字符串的开头或结尾,仅基于周围文本等的整个单词。

Regexp 类

正则表达式 ( regexps ) 是描述字符串内容的模式。它们用于测试字符串是否包含给定模式,或提取匹配的部分。它们是用/pat/和%r{pat}文字或 Regexp.new 构造函数创建的。

match?方法

首先,一个简单的例子来测试一个字符串是否是另一个字符串的一部分。通常,您会使用该 include?方法并将字符串作为参数传递。对于正则表达式,请使用该 match?方法并将搜索字符串括在//分隔符(正则表达式文字)中。

>> sentence = 'This is a sample string'

# check if 'sentence' contains the given string argument
>> sentence.include?('is')
=> true
>> sentence.include?('z')
=> false

# check if 'sentence' matches the pattern as described by the regexp argument
>> sentence.match?(/is/)
=> true
>> sentence.match?(/z/)
=> false

该 match?方法接受一个可选的第二个参数,它指定开始搜索的索引。

>> sentence = 'This is a sample string'

>> sentence.match?(/is/, 2)
=> true
>> sentence.match?(/is/, 6)
=> false

某些正则表达式功能是通过传递修饰符来启用的,修饰符由字母字符表示。如果您使用过命令行,修饰符类似于命令选项,例如 grep -i 将执行不区分大小写的匹配。

>> sentence = 'This is a sample string'

>> sentence.match?(/this/)
=> false
# 'i' is a modifier to enable case insensitive matching
>> sentence.match?(/this/i)
=> true

正则表达式字面量重用和插值

正则表达式字面量可以保存在变量中。这有助于提高代码清晰度、作为方法参数传递、启用重用等。

>> pet = /dog/i
>> pet
=> /dog/i

>> 'They bought a Dog'.match?(pet)
=> true
>> 'A cat crossed their path'.match?(pet)
=> false
```ruby

与双引号字符串文字类似,您可以在正则表达式文字中使用插值和转义序列。
```ruby
>> "cat\tdog".match?(/\t/)
=> true
>> "cat\tdog".match?(/\a/)
=> false

>> greeting = 'hi'
>> /#{greeting} there/
=> /hi there/
>> /#{greeting.upcase} there/
=> /HI there/
>> /#{2**4} apples/
=> /16 apples/

sub 和 gsub 方法

用于搜索和替换,使用 sub 或 gsub 方法。该 sub 方法将仅替换匹配的第一次出现,而 gsub 将替换所有出现。与输入字符串匹配的正则表达式模式必须作为第一个参数传递。第二个参数指定将替换与模式匹配的部分的字符串。

>> greeting = 'Have a nice weekend'

# replace first occurrence of 'e' with 'E'
>> greeting.sub(/e/, 'E')
=> "HavE a nice weekend"
# replace all occurrences of 'e' with 'E'
>> greeting.gsub(/e/, 'E')
=> "HavE a nicE wEEkEnd"

就地替换的用途 sub!和 gsub!方法。

>> word = 'cater'

# this will return a string object, won't modify 'word' variable
>> word.sub(/cat/, 'wag')
=> "wager"
>> word
=> "cater"

# this will modify 'word' variable itself
>> word.sub!(/cat/, 'wag')
=> "wager"
>> word
=> "wager"

正则表达式运算符

Ruby 还提供了用于正则表达式匹配的运算符。

=~匹配运算符返回第一个匹配项的索引,nil 如果未找到匹配项
!~true 如果字符串不包含给定的正则表达式,false 则匹配运算符返回,否则返回
===匹配运算符返回 true 或 false 类似的 match?方法

>> sentence = 'This is a sample string'

# can also use: /is/ =~ sentence
>> sentence =~ /is/
=> 2
>> sentence =~ /z/
=> nil

# can also use: /z/ !~ sentence
>> sentence !~ /z/
=> true
>> sentence !~ /is/
=> false

就像 match?方法一样,=~和!~都可以在条件语句中使用。

>> sentence = 'This is a sample string'

>> puts 'hi' if sentence =~ /is/
hi

>> puts 'oh' if sentence !~ /z/
oh

该===运营商就派上用场了与可枚举的方法,如 grep,grep_v,all?,any?,等。

>> sentence = 'This is a sample string'

# regexp literal has to be on LHS and input string on RHS
>> /is/ === sentence
=> true
>> /z/ === sentence
=> false

>> words = %w[cat attempt tattle]
>> words.grep(/tt/)
=> ["attempt", "tattle"]
>> words.all?(/at/)
=> true
>> words.none?(/temp/)
=> false

限定模式

限制是通过为某些字符和转义序列分配特殊含义而实现的。具有特殊含义的字符在正则表达式中称为元字符。如果您需要逐字匹配这些字符,则需要使用\字符对它们进行转义。

字符串限定

此限制是关于限定正则表达式仅在输入字符串的开头或结尾匹配。这些提供类似于字符串方法 start_with?和 end_with?. 有三种不同的转义序列与字符串级别的正则表达式锚点相关。首先是\A将匹配限制为字符串的开头。

# \A is placed as a prefix to the search term
>> 'cater'.match?(/\Acat/)
=> true
>> 'concatenation'.match?(/\Acat/)
=> false

>> "hi hello\ntop spot".match?(/\Ahi/)
=> true
>> "hi hello\ntop spot".match?(/\Atop/)
=> false

将匹配限制到字符串的末尾,\z使用。

# \z is placed as a suffix to the search term
>> 'spare'.match?(/are\z/)
=> true
>> 'nearest'.match?(/are\z/)
=> false

>> words = %w[surrender unicorn newer door empty eel pest]
>> words.grep(/er\z/)
=> ["surrender", "newer"]
>> words.grep(/t\z/)
=> ["pest"]

字符串 anchor 的另一端\Z。它类似于\z但如果换行符是最后一个字符,则\Z允许在换行符之前进行匹配。

# same result for both \z and \Z
# as there is no newline character at the end of string
>> 'dare'.sub(/are\z/, 'X')
=> "dX"
>> 'dare'.sub(/are\Z/, 'X')
=> "dX"

# different results as there is a newline character at the end of string
>> "dare\n".sub(/are\z/, 'X')
=> "dare\n"
>> "dare\n".sub(/are\Z/, 'X')
=> "dX\n"

结合开始和结束字符串锚点,您可以将匹配限制为整个字符串。类似于使用==运算符比较字符串。

>> 'cat'.match?(/\Acat\z/)
=> true
>> 'cater'.match?(/\Acat\z/)
=> false
>> 'concatenation'.match?(/\Acat\z/)
=> false

锚点本身可以​​用作模式。帮助在字符串的开头或结尾插入文本,模拟字符串连接操作。这些可能感觉不是有用的功能,但结合其他正则表达式功能,它们成为一个非常方便的工具。

>> 'live'.sub(/\A/, 're')
=> "relive"
>> 'send'.sub(/\A/, 're')
=> "resend"

>> 'cat'.sub(/\z/, 'er')
=> "cater"
>> 'hack'.sub(/\z/, 'er')
=> "hacker"

行锚

字符串输入可能包含单行或多行。换行符\n用作行分隔符。有两个行锚,^元字符用于匹配行首和$匹配行尾。如果输入字符串中没有换行符,它们的行为将分别与\A和\z锚点相同。

>> pets = 'cat and dog'

>> pets.match?(/^cat/)
=> true
>> pets.match?(/^dog/)
=> false

>> pets.match?(/dog$/)
=> true

>> pets.match?(/^dog$/)
=> false

这里有一些多行示例来区分线锚和字符串锚。

# check if any line in the string starts with 'top'
>> "hi hello\ntop spot".match?(/^top/)
=> true

# check if any line in the string ends with 'er'
>> "spare\npar\ndare".match?(/er$/)
=> false

# filter all lines ending with 'are'
>> "spare\npar\ndare".each_line.grep(/are$/)
=> ["spare\n", "dare"]

# check if any complete line in the string is 'par'
>> "spare\npar\ndare".match?(/^par$/)
=> true

就像字符串锚一样,您可以将线锚单独用作模式。gsub 并且 puts 将在这里用来更好地说明转型。gsub 如果您既不指定替换字符串也不传递块,该方法将返回一个 Enumerator。这为使用所有那些美妙的 Enumerator 和 Enumerable 方法铺平了道路。

>> str = "catapults\nconcatenate\ncat"

>> puts str.gsub(/^/, '1: ')
1: catapults
1: concatenate
1: cat
>> puts str.gsub(/^/).with_index(1) { |m, i| "#{i}: " }
1: catapults
2: concatenate
3: cat

>> puts str.gsub(/$/, '.')
catapults.
concatenate.
cat.

如果字符串末尾有换行符,则有额外的行尾匹配,但没有额外的行首匹配。

>> puts "1\n2\n".gsub(/^/, 'foo ')
foo 1
foo 2
>> puts "1\n\n".gsub(/^/, 'foo ')
foo 1
foo 

# note the number of lines in output
>> puts "1\n2\n".gsub(/$/, ' baz')
1 baz
2 baz
 baz
>> puts "1\n\n".gsub(/$/, ' baz')
1 baz
 baz
 baz

如果您正在处理基于 Windows 操作系统的文本文件,则必须将\r\n行尾转换为\n第一个。许多 Ruby 方法都可以轻松处理这些问题。例如,您可以指定用于 File.open 方法的行结尾,split 字符串方法默认处理所有空格等。或者,您可以\r使用量词处理可选字符(请参阅贪婪量词部分)。

词锚

第三种限制是词锚。字母(不分大小写)、数字和下划线字符都可以作为单词字符。您可能想知道为什么还有数字和下划线,为什么不只有字母?这来自变量和函数命名约定——通常允许使用字母、数字和下划线。因此,该定义更倾向于编程语言而不是自然语言。

转义序列\b表示单词边界。这适用于单词的开头和结尾的锚定。单词开头表示单词之前的字符是非单词字符或没有字符(字符串的开头)。同理,词尾是指词后的字符为非词字符或无字符(字符串尾)。这意味着\b没有单词字符就不能有单词边界。

>> words = 'par spar apparent spare part'

# replace 'par' irrespective of where it occurs
>> words.gsub(/par/, 'X')
=> "X sX apXent sXe Xt"
# replace 'par' only at the start of word
>> words.gsub(/\bpar/, 'X')
=> "X spar apparent spare Xt"
# replace 'par' only at the end of word
>> words.gsub(/par\b/, 'X')
=> "X sX apparent spare part"
# replace 'par' only if it is not part of another word
>> words.gsub(/\bpar\b/, 'X')
=> "X spar apparent spare part"

单独使用词边界作为模式,您可以获得更多创意:

# space separated words to double quoted csv
# note the use of 'tr' string method
>> puts words.gsub(/\b/, '"').tr(' ', ',')
"par","spar","apparent","spare","part"

>> '-----hello-----'.gsub(/\b/, ' ')
=> "----- hello -----"

# make a programming statement more readable
# shown for illustration purpose only, won't work for all cases
>> 'foo_baz=num1+35*42/num2'.gsub(/\b/, ' ')
=> " foo_baz = num1 + 35 * 42 / num2 "
# excess space at start/end of string can be stripped off
# later you'll learn how to add a qualifier so that strip is not needed
>> 'foo_baz=num1+35*42/num2'.gsub(/\b/, ' ').strip
=> "foo_baz = num1 + 35 * 42 / num2"

词边界也有一个相反的锚点。\B匹配任何\b不匹配的地方。这种二元性也可以在其他一些转义序列中看到。否定逻辑在许多文本处理情况下都很方便。但是小心使用它,你最终可能会匹配你不想要的东西!

>> words = 'par spar apparent spare part'

# replace 'par' if it is not start of word
>> words.gsub(/\Bpar/, 'X')
=> "par sX apXent sXe part"
# replace 'par' at the end of word but not whole word 'par'
>> words.gsub(/\Bpar\b/, 'X')
=> "par sX apparent spare part"
# replace 'par' if it is not end of word
>> words.gsub(/par\B/, 'X')
=> "par spar apXent sXe Xt"
# replace 'par' if it is surrounded by word characters
>> words.gsub(/\Bpar\B/, 'X')
=> "par spar apXent sXe part"

这里有一些独立的模式用法来比较和对比两个词锚。

>> 'copper'.gsub(/\b/, ':')
=> ":copper:"
>> 'copper'.gsub(/\B/, ':')
=> "c:o:p:p:e:r"

>> '-----hello-----'.gsub(/\b/, ' ')
=> "----- hello -----"
>> '-----hello-----'.gsub(/\B/, ' ')
=> " - - - - -h e l l o- - - - - "

交替和分组

很多时候,您想检查输入字符串是否与多个模式匹配。例如,对象的颜色是绿色、蓝色还是红色。在编程术语中,您需要执行 OR 条件。本章将展示如何在这种情况下使用交替。这些模式之间可以有一些共同元素,在这种情况下,分组有助于形成更简洁的表达式。本章还将讨论用于确定哪个交替获胜的优先规则。

或有条件

与逻辑 OR 结合的条件表达式的计算结果为 true 是否满足任何条件。同样,在正则表达式中,您可以使用 | 元字符组合多个模式来表示逻辑 OR。如果在输入字符串中找到任何替代模式,则匹配将成功。这些替代方案具有正则表达式的全部功能,例如它们可以拥有自己的独立锚点。这里有一些例子。

# match either 'cat' or 'dog'
>> 'I like cats'.match?(/cat|dog/)
=> true
>> 'I like dogs'.match?(/cat|dog/)
=> true
>> 'I like parrots'.match?(/cat|dog/)
=> false

# replace either 'cat' at start of string or 'cat' at end of word
>> 'catapults concatenate cat scat'.gsub(/\Acat|cat\b/, 'X')
=> "Xapults concatenate X sX"

# replace either 'cat' or 'dog' or 'fox' with 'mammal'
>> 'cat dog bee parrot fox'.gsub(/cat|dog|fox/, 'mammal')
=> "mammal mammal bee parrot mammal"

正则表达式联合方法

您可能会从上面的示例中推断出,可能存在需要大量交替的情况。该 Regexp.union 方法可用于自动构建交替列表。它接受一个数组作为参数或逗号分隔的参数列表。

>> Regexp.union('car', 'jeep')
=> /car|jeep/

>> words = %w[cat dog fox]
>> pat = Regexp.union(words)
>> pat
=> /cat|dog|fox/
>> 'cat dog bee parrot fox'.gsub(pat, 'mammal')
=> "mammal mammal bee parrot mammal"

分组

通常,regexp 替代方案中有一些共同点。它可以是普通字符或正则表达式限定符,如锚点。在这种情况下,您可以使用一对括号元字符将它们分组。与 a(b+c)d = abd+acd 数学类似,您可以 a(b|c)d = abd|acd 使用正则表达式。

# without grouping
>> 'red reform read arrest'.gsub(/reform|rest/, 'X')
=> "red X read arX"
# with grouping
>> 'red reform read arrest'.gsub(/re(form|st)/, 'X')
=> "red X read arX"

# without grouping
>> 'par spare part party'.gsub(/\bpar\b|\bpart\b/, 'X')
=> "X spare X party"
# taking out common anchors
>> 'par spare part party'.gsub(/\b(par|part)\b/, 'X')
=> "X spare X party"
# taking out common characters as well
# you'll later learn a better technique instead of using empty alternate
>> 'par spare part party'.gsub(/\bpar(|t)\b/, 'X')
=> "X spare X party"

Regexp.source 方法

该 Regexp.source 方法有助于在另一个正则表达式中插入正则表达式文字。例如,将锚点添加到使用该 Regexp.union 方法创建的交替列表中。

>> words = %w[cat par]
>> alt = Regexp.union(words)
>> alt
=> /cat|par/
>> alt_w = /\b(#{alt.source})\b/
>> alt_w
=> /\b(cat|par)\b/

>> 'cater cat concatenate par spare'.gsub(alt, 'X')
=> "Xer X conXenate X sXe"
>> 'cater cat concatenate par spare'.gsub(alt_w, 'X')
=> "cater X concatenate X spare"

优先规则

使用交替时有一些棘手的情况。如果它用于测试匹配以获取 true/false 字符串输入,则没有歧义。但是,对于字符串替换等其他事情,这取决于几个因素。比如说,你想替换 are 或者 spared——哪个应该优先?较大的单词 spared 或其中的子字符串 are 或基于其他内容?

在 Ruby 中,在输入字符串中最早匹配的 regexp 替代项获得优先权。正则表达式运算符=~有助于说明这个概念。

>> words = 'lion elephant are rope not'

>> words =~ /on/
=> 2
>> words =~ /ant/
=> 10

# starting index of 'on' < index of 'ant' for given string input
# so 'on' will be replaced irrespective of order of regexp
>> words.sub(/on|ant/, 'X')
=> "liX elephant are rope not"
>> words.sub(/ant|on/, 'X')
=> "liX elephant are rope not"

那么,如果两个或多个备选方案在同一索引上匹配会发生什么?然后优先级按照声明的顺序从左到右。

>> mood = 'best years'

>> mood =~ /year/
=> 5
>> mood =~ /years/
=> 5

# starting index for 'year' and 'years' will always be same
# so, which one gets replaced depends on the order of alternation
>> mood.sub(/year|years/, 'X')
=> "best Xs"
>> mood.sub(/years|year/, 'X')
=> "best X"

另一个例子 gsub 是开车回家的问题。

>> words = 'ear xerox at mare part learn eye'

# this is going to be same as: gsub(/ar/, 'X')
>> words.gsub(/ar|are|art/, 'X')
=> "eX xerox at mXe pXt leXn eye"

# this is going to be same as: gsub(/are|ar/, 'X')
>> words.gsub(/are|ar|art/, 'X')
=> "eX xerox at mX pXt leXn eye"

# phew, finally this one works as needed
>> words.gsub(/are|art|ar/, 'X')
=> "eX xerox at mX pX leXn eye"

如果您不希望子字符串破坏您的替换,一个可靠的解决方法是根据长度对替换进行排序,最长的在前。

>> words = %w[hand handy handful]

>> alt = Regexp.union(words.sort_by { |w| -w.length })
>> alt
=> /handful|handy|hand/

>> 'hands handful handed handy'.gsub(alt, 'X')
=> "Xs X Xed X"

# without sorting, order will come into play
>> 'hands handful handed handy'.gsub(Regexp.union(words), 'X')
=> "Xs Xful Xed Xy"

转义元字符

本章将展示如何逐字匹配元字符,用于手动以及以编程方式构建的模式。您还将了解 regexp 支持的转义序列以及它们与字符串的不同之处。

用 \ 转义

您已经看到了一些有助于组成正则表达式文字的元字符和转义序列。要逐字匹配元字符,即删除它们的特殊含义,请在这些字符前加上一个\字符。要指示文字\字符,请使用\.

为了给示例增添一点趣味,下面使用了块形式来用表达式修改字符串的匹配部分。在后面的章节中,您将看到更多直接处理匹配部分的方法。

# even though ^ is not being used as anchor, it won't be matched literally
>> 'a^2 + b^2 - C*3'.match?(/b^2/)
=> false
# escaping will work
>> 'a^2 + b^2 - C*3'.gsub(/(a|b)\^2/) { |m| m.upcase }
=> "A^2 + B^2 - C*3"

# match ( or ) literally
>> '(a*b) + c'.gsub(/\(|\)/, '')
=> "a*b + c"

>> '\learn\by\example'.gsub(/\\/, '/')
=> "/learn/by/example"

如前所述,正则表达式只是处理文本的另一种工具。本书中提供的一些示例和练习也可以使用普通的字符串方法解决。对于现实世界的用例,首先问问自己是否需要正则表达式?

>> eqn = 'f*(a^b) - 3*(a^b)'

# straightforward search and replace, no need regexp shenanigans
>> eqn.gsub('(a^b)', 'c')
=> "f*c - 3*c"

正则表达式转义方法

动态构造正则表达式时如何转义所有元字符?放松,Regexp.escape 方法已经让你满意了。无需手动处理所有元字符或担心未来版本的更改。

>> eqn = 'f*(a^b) - 3*(a^b)'
>> expr = '(a^b)'

>> puts Regexp.escape(expr)
\(a\^b\)

# replace only at the end of string
>> eqn.sub(/#{Regexp.escape(expr)}\z/, 'c')
=> "f*(a^b) - 3*c"

该 Regexp.union 方法自动对字符串参数应用转义。

# array of strings, assume alternation precedence sorting isn't needed
>> terms = %w[a_42 (a^b) 2|3]

>> pat = Regexp.union(terms)
>> pat
=> /a_42|\(a\^b\)|2\|3/

>> 'ba_423 (a^b)c 2|3 a^b'.gsub(pat, 'X')
=> "bX3 Xc X a^b"

Regexp.union 还将处理正确混合字符串和正则表达式模式。(?-mix:在下面的输出中看到的将在修改器章节中解释。

>> Regexp.union(/^cat|dog$/, 'a^b')
=> /(?-mix:^cat|dog$)|a\^b/

转义分隔符

另一个要跟踪转义的字符是用于定义正则表达式文字的分隔符。或者,您可以使用/与定义正则表达式文字不同的分隔符%r来避免转义。此外,您不必担心#{}插值内未转义的分隔符。

>> path = '/abc/123/foo/baz/ip.txt'

# \/ is also known as 'leaning toothpick syndrome'
>> path.sub(/\A\/abc\/123\//, '~/')
=> "~/foo/baz/ip.txt"

# a different delimiter improves readability and reduces typos
>> path.sub(%r#\A/abc/123/#, '~/')
=> "~/foo/baz/ip.txt"

转义序列

在正则表达式文字中,可以使用转义序列 as\t和\n分别表示制表符和换行符等字符。这些类似于它们在普通字符串文字中的处理方式(有关详细信息,请参阅 ruby-doc: Strings)。然而,像\b(词边界)和\s(参见转义序列字符集部分)这样的转义对于正则表达式是不同的。八进制转义\nnn 必须是三位数以避免与 Backreferences 冲突。

>> "a\tb\tc".gsub(/\t/, ':')
=> "a:b:c"

>> "1\n2\n3".gsub(/\n/, ' ')
=> "1 2 3"

如果未定义转义序列,它将匹配被转义的字符。例如,%将匹配%而不是\后跟%.

>> 'h%x'.match?(/h\%x/)
=> true
>> 'h\%x'.match?(/h\%x/)
=> false

>> 'hello'.match?(/\l/)
=> true

如果您使用转义符表示元字符,它将按字面意思处理,而不是按其元字符特性处理。

# \x20 is hexadecimal for space character
>> 'h e l l o'.gsub(/\x20/, '')
=> "hello"
# \053 is octal for + character
>> 'a+b'.match?(/a\053b/)
=> true

# \x7c is '|' character
>> '12|30'.gsub(/2\x7c3/, '5')
=> "150"
>> '12|30'.gsub(/2|3/, '5')
=> "15|50"

点元字符和量词

本章介绍点元字符和量词。顾名思义,量词允许您指定字符或分组应匹配的次数。使用*字符串运算符,您可以执行诸如’no’ * 5get 之类的操作"nonononono"。这可以节省您的手动重复,并且可以根据需要以编程方式重复字符串对象。量词支持这种简单的重复以及指定重复范围的方法。该范围具有关于开始值和结束值的有界或无界的灵活性。结合点元字符(如果需要还可以交替),量词允许您在模式之间构建条件 AND 逻辑。

点元字符

点元字符匹配除换行符以外的任何字符。

# matches character 'c', any character and then character 't'
>> 'tac tin c.t abc;tuv acute'.gsub(/c.t/, 'X')
=> "taXin X abXuv aXe"

# matches character 'r', any two characters and then character 'd'
>> 'breadth markedly reported overrides'.gsub(/r..d/) { |s| s.upcase }
=> "bREADth maRKEDly repoRTED oveRRIDes"

# matches character '2', any character and then character '3'
>> "42\t33".sub(/2.3/, '8')
=> "483"

分割法

本章将另外使用 split 方法来举例说明。该 split 方法根据给定的正则表达式(或字符串)分隔字符串并返回字符串数组。

# same as: 'apple-85-mango-70'.split('-')
>> 'apple-85-mango-70'.split(/-/)
=> ["apple", "85", "mango", "70"]

>> 'bus:3:car:5:van'.split(/:.:/)
=> ["bus", "car", "van"]

# optional limit can be specified as second argument
# when limit is positive, you get maximum of limit-1 splits
>> 'apple-85-mango-70'.split(/-/, 2)
=> ["apple", "85-mango-70"]

贪婪的量词

量词具有类似于字符串重复运算符和范围方法的功能。它们可以应用于字符和分组(以及更多内容,您将在后面的章节中看到)。除了能够指定精确的数量和有界范围外,这些还可以匹配无界变化的数量。如果输入字符串可以通过多种方式满足不同数量的模式,您可以在三种类型的量词中进行选择以缩小可能性。在本节中,涵盖了贪婪类型的量词。

首先?是量化要匹配的字符或组 0 或 1 时间的元字符。换句话说,您将该字符或组作为可选匹配的内容。与交替和分组相比,这导致更简洁的正则表达式。

# same as: /ear|ar/
>> 'far feat flare fear'.gsub(/e?ar/, 'X')
=> "fX feat flXe fX"

# same as: /\bpar(t|)\b/
>> 'par spare part party'.gsub(/\bpart?\b/, 'X')
=> "X spare X party"

# same as: /\b(re.d|red)\b/
>> words = %w[red read ready re;d road redo reed rod]
>> words.grep(/\bre.?d\b/)
=> ["red", "read", "re;d", "reed"]

# same as: /part|parrot/
>> 'par part parrot parent'.gsub(/par(ro)?t/, 'X')
=> "par X X parent"
# same as: /part|parrot|parent/
>> 'par part parrot parent'.gsub(/par(en|ro)?t/, 'X')
=> "par X X X"

该*元字符量化人物或团体比赛 0 或更多次。没有上限,更多细节将在本章后面讨论。

# match 't' followed by zero or more of 'a' followed by 'r'
>> 'tr tear tare steer sitaara'.gsub(/ta*r/, 'X')
=> "X tear Xe steer siXa"

# match 't' followed by zero or more of 'e' or 'a' followed by 'r'
>> 'tr tear tare steer sitaara'.gsub(/t(e|a)*r/, 'X')
=> "X X Xe sX siXa"

# match zero or more of '1' followed by '2'
>> '3111111111125111142'.gsub(/1*2/, 'X')
=> "3X511114X"

这里有一些示例 split 和相关方法。partition 在第一个匹配项上拆分输入字符串,并且正则表达式匹配的文本也出现在输出中。rpartition 就像 partition 但在最后一场比赛中分裂。

# note how '25' and '42' gets split, there is '1' zero times in between them
>> '3111111111125111142'.split(/1*/)
=> ["3", "2", "5", "4", "2"]
# there is '1' zero times at end of string as well, note the use of -1 for limit
>> '3111111111125111142'.split(/1*/, -1)
=> ["3", "2", "5", "4", "2", ""]

>> '3111111111125111142'.partition(/1*2/)
=> ["3", "11111111112", "5111142"]

# last element is empty because there is nothing after 2 at the end of string
>> '3111111111125111142'.rpartition(/1*2/)
=> ["311111111112511114", "2", ""]

该 + 元字符量化人物或团体比赛 1 或更多次。与*量词类似,没有上限。更重要的是,这没有像在模式之间或字符串末尾匹配空字符串这样的惊喜。

>> 'tr tear tare steer sitaara'.gsub(/ta+r/, 'X')
=> "tr tear Xe steer siXa"
>> 'tr tear tare steer sitaara'.gsub(/t(e|a)+r/, 'X')
=> "tr X Xe sX siXa"

>> '3111111111125111142'.gsub(/1+2/, 'X')
=> "3X5111142"
>> '3111111111125111142'.split(/1+/)
=> ["3", "25", "42"]

您可以使用{}元字符指定有界和无界整数范围。有四种使用此量词的方法,如下所示:

>> demo = %w[abc ac adc abbc xabbbcz bbb bc abbbbbc]

>> demo.grep(/ab{1,4}c/)
=> ["abc", "abbc", "xabbbcz"]
>> demo.grep(/ab{3,}c/)
=> ["xabbbcz", "abbbbbc"]
>> demo.grep(/ab{,2}c/)
=> ["abc", "ac", "abbc"]
>> demo.grep(/ab{3}c/)
=> ["xabbbcz"]

的{}元字符必须进行转义逐字匹配。然而,与 () 元字符不同的是,它们有更多的回旋余地。例如,{单独转义就足够了,或者如果它不严格符合上面列出的四种形式中的任何一种,则根本不需要转义。此外,如果您将{}量词应用于#字符,则需要转义#以覆盖插值。

AND 条件

接下来,如何使用点元字符和量词构造 AND 条件。

# match 'Error' followed by zero or more characters followed by 'valid'
>> 'Error: not a valid input'.match?(/Error.*valid/)
=> true

>> 'Error: key not found'.match?(/Error.*valid/)
=> false

要允许以任何顺序进行匹配,您还必须引入交替。对于 2 或 3 个模式,这在某种程度上是可以管理的。有关更简单的方法,请参阅 AND conditional with lookarounds 部分。

>> seq1, seq2 = ['cat and dog', 'dog and cat']
>> seq1.match?(/cat.*dog|dog.*cat/)
=> true
>> seq2.match?(/cat.*dog|dog.*cat/)
=> true

# if you just need true/false result, this would be a scalable approach
>> patterns = [/cat/, /dog/]
>> patterns.all? { |re| seq1.match?(re) }
=> true
>> patterns.all? { |re| seq2.match?(re) }
=> true

贪婪是什么意思?

当您使用?量词时,如果两个数量都能满足正则表达式,Ruby 如何决定匹配 0 或 1 时间?例如,考虑这个替换表达式’foot’.sub(/f.?o/, ‘X’)——应该 foo 替换还是 fo?它总是会替换,foo 因为这些是贪婪的量词,这意味着它们会尽可能地匹配。

>> 'foot'.sub(/f.?o/, 'X')
=> "Xt"

# a more practical example
# prefix '<' with '\' if it is not already prefixed
# both '<' and '\<' will get replaced with '\<'
>> puts 'blah \< foo < bar \< blah < baz'.gsub(/\\?</, '\<')
blah \< foo \< bar \< blah \< baz

# say goodbye to /handful|handy|hand/ shenanigans
>> 'hand handy handful'.gsub(/hand(y|ful)?/, 'X')
=> "X X X"

但是等等,那么/Error.*valid/示例是如何工作的?之后不应该。*消耗所有字符 Error 吗?好问题。正则表达式引擎实际上确实消耗了所有字符。然后意识到正则表达式失败,它从字符串的末尾返回一个字符并再次检查整个正则表达式是否满足。重复此过程,直到找到匹配项或确认失败。在正则表达式中,这称为回溯。

>> sentence = 'that is quite a fabricated tale'

# /t.*a/ will always match from first '' to last 'a'
# also, note that 'sub' is being used here, not 'gsub'
>> sentence.sub(/t.*a/, 'X')
=> "Xle"
>> 'star'.sub(/t.*a/, 'X')
=> "sXr"

# matching first 't' to last 'a' for t.*a won't work for these cases
# the regexp engine backtracks until .*q matches and so on
>> sentence.sub(/t.*a.*q.*f/, 'X')
>> sentence.sub(/t.*a.*u/, 'X')
=> "Xite a fabricated tale"

非贪婪量词

顾名思义,这些量词将尝试尽可能少地匹配。也称为懒惰或不情愿的量词。将 a 附加?到贪婪的量词使它们成为非贪婪的。


>> 'foot'.sub(/f.??o/, 'X')
=> "Xot"
>> 'frost'.sub(/f.??o/, 'X')
=> "Xst"

>> '123456789'.sub(/.{2,5}?/, 'X')
=> "X3456789"

>> 'green:3.14:teal::brown:oh!:blue'.split(/:.*?:/)
=> ["green", "teal", "brown", "blue"]

像贪婪量词一样,懒惰量词会尝试满足整个正则表达式。


>> sentence = 'that is quite a fabricated tale'

# /t.*?a/ will always match from first 't' to first 'a'
>> sentence.sub(/t.*?a/, 'X')
=> "Xt is quite a fabricated tale"

# matching first 't' to first 'a' for t.*?a won't work for this case
# so, regexp engine will move forward until .*?f matches and so on
>> sentence.sub(/t.*?a.*?f/, 'X')
=> "Xabricated tale"
# this matches last 'e' after 'q' to satisfy the anchor requirement
>> sentence.sub(/q.*?e$/, 'X')
=> "that is X"

所有格量词

将 a 附加 + 到贪婪量词使它们成为所有格量词。这些就像贪婪的量词,但没有回溯。所以,像这样的东西/Error.+valid/永远不会匹配,因为。+ 会消耗所有剩余的字符。如果贪婪量词和所有格量词版本在功能上是等效的,那么所有格是首选,因为它在不匹配的情况下会失败得更快。


# functionally equivalent greedy and possessive versions
>> %w[abc ac adc abbc xabbbcz bbb bc abbbbbc].grep(/ab*c/)
=> ["abc", "ac", "abbc", "xabbbcz", "abbbbbc"]
>> %w[abc ac adc abbc xabbbcz bbb bc abbbbbc].grep(/ab*+c/)
=> ["abc", "ac", "abbc", "xabbbcz", "abbbbbc"]

# different results
# numbers >= 100 if there are leading zeros
# \d will be discussed in a later chapter, it matches all digit characters
>> '0501 035 154 12 26 98234'.gsub(/\b0*\d{3,}\b/, 'X')
=> "X X X 12 26 X"
>> '0501 035 154 12 26 98234'.gsub(/\b0*+\d{3,}\b/, 'X')
=> "X 035 X 12 26 X"

所有格量词的效果也可以用原子分组来表示。语法是 (?>pat),其中 pat 是正则表达式模式的一部分的缩写。在后面的章节中,您将看到更多这样的特殊分组。

# same as: /(b|o)++/
>> 'abbbc foooooot'.gsub(/(?>(b|o)+)/, 'X')
=> "aXc fXt"

# same as: /\b0*+\d{3,}\b/
>> '0501 035 154 12 26 98234'.gsub(/\b(?>0*)\d{3,}\b/, 'X')
=> "X 035 X 12 26 X"

调试和可视化工具

随着您的正则表达式变得复杂,如果您遇到问题,调试就会变得困难。从头开始逐步构建正则表达式并针对输入字符串进行测试将对纠正问题大有帮助。为了帮助完成这样的过程,您可以使用各种在线工具。

rubular 是一个在线 Ruby 正则表达式编辑器(基于 Ruby 2.5.7),用于直观地测试您的正则表达式。您需要添加正则表达式、输入字符串和可选修饰符。匹配的部分将突出显示。

另一个有用的工具是 debuggex,它将您的正则表达式转换为铁路图,从而为理解模式提供视觉帮助。这不支持 Ruby,所以选择 JavaScript 风格。

对于练习,通常建议使用 regexcrossword。它只支持 JavaScript,所以有些谜题可能与 Ruby 语法不同。请参阅 regexcrossword: howtoplay 寻求帮助。

总结

正则表达式是需要一本厚厚的书才能讲完的工具,感兴趣的读者可以搜索相关主题进行深入学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

朋朋dev

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值