class Regexp

一个正则表达式(也称为regexp)是一个匹配模式(也简称为模式)。

regexp 的一种常见表示法是使用封闭的斜杠字符

/foo/

regexp 可以应用于一个目标字符串;字符串中与模式匹配的部分(如果有)称为匹配,可以称为匹配

re = /red/
re.match?('redirect') # => true   # Match at beginning of target.
re.match?('bored')    # => true   # Match at end of target.
re.match?('credit')   # => true   # Match within target.
re.match?('foo')      # => false  # No match.

Regexp 的用法

Regexp 可用于

Regexp 对象

Regexp 对象具有

创建 Regexp

正则表达式可以通过以下方式创建:

方法 match

方法 Regexp#matchString#matchSymbol#match 中的每一种都会在找到匹配时返回一个 MatchData 对象,否则返回 nil;每种方法还会设置全局变量

'food'.match(/foo/) # => #<MatchData "foo">
'food'.match(/bar/) # => nil

运算符 =~

运算符 Regexp#=~String#=~Symbol#=~ 中的每一种都会在找到匹配时返回一个整数偏移量,否则返回 nil;每种方法还会设置全局变量

/bar/ =~ 'foo bar' # => 4
'foo bar' =~ /bar/ # => 4
/baz/ =~ 'foo bar' # => nil

方法 match?

方法 Regexp#match?String#match?Symbol#match? 中的每一种都会在找到匹配时返回 true,否则返回 false;没有任何一种会设置全局变量

'food'.match?(/foo/) # => true
'food'.match?(/bar/) # => false

全局变量

某些面向 regexp 的方法会将值赋给全局变量。

受影响的全局变量是:

这些变量(除了 $~)是 $~ 方法的简写。请参阅MatchData 中的全局变量等效性

示例

# Matched string, but no matched groups.
'foo bar bar baz'.match('bar')
$~ # => #<MatchData "bar">
$& # => "bar"
$` # => "foo "
$' # => " bar baz"
$+ # => nil
$1 # => nil

# Matched groups.
/s(\w{2}).*(c)/.match('haystack')
$~ # => #<MatchData "stac" 1:"ta" 2:"c">
$& # => "stac"
$` # => "hay"
$' # => "k"
$+ # => "c"
$1 # => "ta"
$2 # => "c"
$3 # => nil

# No match.
'foo'.match('bar')
$~ # => nil
$& # => nil
$` # => nil
$' # => nil
$+ # => nil
$1 # => nil

请注意,Regexp#match?String#match?Symbol#match? 不会设置全局变量。

如上所示,最简单的 regexp 使用字面量表达式作为其源。

re = /foo/              # => /foo/
re.match('food')        # => #<MatchData "foo">
re.match('good')        # => nil

丰富的可用子表达式集合使 regexp 具有强大的功能和灵活性。

特殊字符

Regexp 特殊字符,称为元字符,在某些上下文中具有特殊含义;取决于上下文,这些有时是元字符。

. ? - + * ^ \ | $ ( ) [ ] { }

要字面匹配一个元字符,请使用反斜杠转义它。

# Matches one or more 'o' characters.
/o+/.match('foo')  # => #<MatchData "oo">
# Would match 'o+'.
/o\+/.match('foo') # => nil

要字面匹配一个反斜杠,请使用反斜杠转义它。

/\./.match('\.')  # => #<MatchData ".">
/\\./.match('\.') # => #<MatchData "\\.">

方法 Regexp.escape 返回一个已转义的字符串。

Regexp.escape('.?-+*^\|$()[]{}')
# => "\\.\\?\\-\\+\\*\\^\\\\\\|\\$\\(\\)\\[\\]\\{\\}"

源字面量

源字面量在很大程度上类似于双引号字符串;请参阅Double-Quoted String Literals

特别地,源字面量可以包含插值表达式。

s = 'foo'         # => "foo"
/#{s}/            # => /foo/
/#{s.capitalize}/ # => /Foo/
/#{2 + 2}/        # => /4/

普通字符串字面量和源字面量之间存在差异;请参阅Shorthand Character Classes

字符类

一个字符类由方括号分隔;它指定在目标字符串的给定位置,某些字符匹配。

# This character class will match any vowel.
re = /B[aeiou]rd/
re.match('Bird') # => #<MatchData "Bird">
re.match('Bard') # => #<MatchData "Bard">
re.match('Byrd') # => nil

字符类可以包含连字符来指定字符范围。

# These regexps have the same effect.
/[abcdef]/.match('foo') # => #<MatchData "f">
/[a-f]/.match('foo')    # => #<MatchData "f">
/[a-cd-f]/.match('foo') # => #<MatchData "f">

当字符类的第一个字符是插入符号(^)时,类的含义被反转:它匹配指定字符以外的任何字符。

/[^a-eg-z]/.match('f') # => #<MatchData "f">

一个字符类可以包含另一个字符类。单独而言,这没什么用,因为 [a-z[0-9]] 描述的集合与 [a-z0-9] 相同。

然而,字符类还支持 && 运算符,它对参数执行集合交集。两者可以组合如下:

/[a-w&&[^c-g]z]/ # ([a-w] AND ([^c-g] OR z))

这等同于

/[abh-w]/

简写字符类

以下每个元字符都作为字符类的简写:

锚点

锚点是元序列,用于匹配目标字符串中字符之间的零宽度位置。

对于没有锚点的子表达式,匹配可以从目标字符串的任何位置开始。

/real/.match('surrealist') # => #<MatchData "real">

对于带有锚点的子表达式,匹配必须从匹配的锚点开始。

边界锚点

以下每个锚点都匹配一个边界。

环视锚点

前瞻锚点

后顾锚点

下面的模式使用正前瞻和正后顾来匹配出现在 标签中的文本,而不将标签包含在匹配中。

/(?<=<b>)\w+(?=<\/b>)/.match("Fortune favors the <b>bold</b>.")
# => #<MatchData "bold">

后顾中的模式必须是固定宽度的。但顶层替代项可以是不同长度的。例如。 (?<=a|bc) 是可以的。 (?<=aaa(?:b|cd)) 不允许。

匹配重置锚点

交替

竖线元字符(|)可用于括号内以表示交替:两个或多个子表达式,其中任何一个都可以匹配目标字符串。

两个替代项

re = /(a|b)/
re.match('foo') # => nil
re.match('bar') # => #<MatchData "b" 1:"b">

四个替代项

re = /(a|b|c|d)/
re.match('shazam') # => #<MatchData "a" 1:"a">
re.match('cold')   # => #<MatchData "c" 1:"c">

每个替代项都是一个子表达式,并且可以由其他子表达式组成。

re = /([a-c]|[x-z])/
re.match('bar') # => #<MatchData "b" 1:"b">
re.match('ooz') # => #<MatchData "z" 1:"z">

方法 Regexp.union 提供了一种方便的方式来构造具有替代项的 regexp。

量词

简单的 regexp 匹配一个字符。

/\w/.match('Hello')  # => #<MatchData "H">

添加的量词指定需要或允许多少次匹配。

贪婪、惰性或占有式匹配

量词匹配可以是贪婪的、惰性的或占有式的。

更多

分组和捕获

简单的 regexp 至多只有一个匹配。

re = /\d\d\d\d-\d\d-\d\d/
re.match('1943-02-04')      # => #<MatchData "1943-02-04">
re.match('1943-02-04').size # => 1
re.match('foo')             # => nil

添加一个或多个括号对 (subexpression) 定义了分组,这可能导致多个匹配的子字符串,称为捕获

re = /(\d\d\d\d)-(\d\d)-(\d\d)/
re.match('1943-02-04')      # => #<MatchData "1943-02-04" 1:"1943" 2:"02" 3:"04">
re.match('1943-02-04').size # => 4

第一个捕获是整个匹配的字符串;其他捕获是来自分组的匹配子字符串。

分组可以有一个量词

re = /July 4(th)?/
re.match('July 4')   # => #<MatchData "July 4" 1:nil>
re.match('July 4th') # => #<MatchData "July 4th" 1:"th">

re = /(foo)*/
re.match('')       # => #<MatchData "" 1:nil>
re.match('foo')    # => #<MatchData "foo" 1:"foo">
re.match('foofoo') # => #<MatchData "foofoo" 1:"foo">

re = /(foo)+/
re.match('')       # => nil
re.match('foo')    # => #<MatchData "foo" 1:"foo">
re.match('foofoo') # => #<MatchData "foofoo" 1:"foo">

返回的 MatchData 对象提供了对匹配子字符串的访问。

re = /(\d\d\d\d)-(\d\d)-(\d\d)/
md = re.match('1943-02-04')
# => #<MatchData "1943-02-04" 1:"1943" 2:"02" 3:"04">
md[0] # => "1943-02-04"
md[1] # => "1943"
md[2] # => "02"
md[3] # => "04"

非捕获分组

分组可以设置为非捕获;它仍然是一个分组(并且,例如,可以有一个量词),但其匹配的子字符串不包含在捕获中。

非捕获分组以 ?:(在括号内)开头。

# Don't capture the year.
re = /(?:\d\d\d\d)-(\d\d)-(\d\d)/
md = re.match('1943-02-04') # => #<MatchData "1943-02-04" 1:"02" 2:"04">

反向引用

分组匹配也可以在 regexp 本身内部引用;这种引用称为 backreference

/[csh](..) [csh]\1 in/.match('The cat sat in the hat')
# => #<MatchData "cat sat in" 1:"at">

下表显示了上面 regexp 中的每个子表达式如何匹配目标字符串中的子字符串。

| Subexpression in Regexp   | Matching Substring in Target String |
|---------------------------|-------------------------------------|
|       First '[csh]'       |            Character 'c'            |
|          '(..)'           |        First substring 'at'         |
|      First space ' '      |      First space character ' '      |
|       Second '[csh]'      |            Character 's'            |
| '\1' (backreference 'at') |        Second substring 'at'        |
|           ' in'           |            Substring ' in'          |

Regexp 可以包含任意数量的分组。

命名捕获

如上所示,可以通过数字引用捕获。捕获还可以有一个名称,前缀为 ?<name>?'name',并且名称(符号化)可以用作 MatchData[] 的索引。

md = /\$(?<dollars>\d+)\.(?'cents'\d+)/.match("$3.67")
# => #<MatchData "$3.67" dollars:"3" cents:"67">
md[:dollars]  # => "3"
md[:cents]    # => "67"
# The capture numbers are still valid.
md[2]         # => "67"

当 regexp 包含命名捕获时,没有未命名的捕获。

/\$(?<dollars>\d+)\.(\d+)/.match("$3.67")
# => #<MatchData "$3.67" dollars:"3">

命名分组可以作为 \k<name> 进行反向引用。

/(?<vowel>[aeiou]).\k<vowel>.\k<vowel>/.match('ototomy')
# => #<MatchData "ototo" vowel:"o">

当(仅当)regexp 包含命名捕获组并出现在 =~ 运算符之前时,捕获的子字符串会被分配给具有相应名称的局部变量。

/\$(?<dollars>\d+)\.(?<cents>\d+)/ =~ '$3.67'
dollars # => "3"
cents   # => "67"

方法 Regexp#named_captures 返回一个捕获名称和子字符串的哈希;方法 Regexp#names 返回一个捕获名称的数组。

原子分组

分组可以通过 (?>subexpression) 设为原子

这会导致子表达式独立于表达式的其余部分进行匹配,因此匹配的子字符串在剩余的匹配中是固定的,除非整个子表达式必须被放弃并随后重新访问。

通过这种方式,subexpression 被视为一个不可分割的整体。原子分组通常用于优化模式以防止不必要的回溯。

示例(无原子分组)

/".*"/.match('"Quote"') # => #<MatchData "\"Quote\"">

分析

  1. 模式中的前导子表达式 " 匹配目标字符串中的第一个字符 "

  2. 下一个子表达式 .* 匹配下一个子字符串 Quote"(包括尾随的双引号)。

  3. 现在目标字符串中没有剩余内容可以匹配模式中的尾随子表达式 ";这将导致整体匹配失败。

  4. 匹配的子字符串回溯一个位置:Quote

  5. 最后一个子表达式 " 现在匹配最后一个子字符串 ",并且整体匹配成功。

如果子表达式 .* 被原子分组,则回溯被禁用,并且整体匹配失败。

/"(?>.*)"/.match('"Quote"') # => nil

原子分组会影响性能;请参阅Atomic Group

子表达式调用

如上所示,反向引用编号(\n)或名称(\k<name>)可以访问捕获的子字符串;相应的 regexp子表达式也可以通过编号(\gn)或名称(\g<name>)访问。

/\A(?<paren>\(\g<paren>*\))*\z/.match('(())')
# ^1
#      ^2
#           ^3
#                 ^4
#      ^5
#           ^6
#                      ^7
#                       ^8
#                       ^9
#                           ^10

模式

  1. 匹配字符串的开头,即第一个字符之前。

  2. 进入命名分组 paren

  3. 匹配字符串中的第一个字符 '('

  4. 再次调用 paren 分组,即递归回第二步。

  5. 重新进入 paren 分组。

  6. 匹配字符串中的第二个字符 '('

  7. 尝试第三次调用 paren,但失败,因为这样做会阻止整体成功匹配。

  8. 匹配字符串中的第三个字符 ')';标记第二次递归调用的结束。

  9. 匹配字符串中的第四个字符 ')'

  10. 匹配字符串的末尾。

请参阅Subexpression calls

条件语句

条件结构的形式为 (?(cond)yes|no),其中:

示例

re = /\A(foo)?(?(1)(T)|(F))\z/
re.match('fooT') # => #<MatchData "fooT" 1:"foo" 2:"T" 3:nil>
re.match('F')    # => #<MatchData "F" 1:nil 2:nil 3:"F">
re.match('fooF') # => nil
re.match('T')    # => nil

re = /\A(?<xyzzy>foo)?(?(<xyzzy>)(T)|(F))\z/
re.match('fooT') # => #<MatchData "fooT" xyzzy:"foo">
re.match('F')    # => #<MatchData "F" xyzzy:nil>
re.match('fooF') # => nil
re.match('T')    # => nil

缺失运算符

缺失运算符是一种特殊的分组,它匹配任何匹配包含的子表达式的内容。

/(?~real)/.match('surrealist') # => #<MatchData "surrea">
/(?~real)ist/.match('surrealist') # => #<MatchData "ealist">
/sur(?~real)ist/.match('surrealist') # => nil

Unicode

Unicode 属性

/\p{property_name}/ 构造(小写 p)使用 Unicode 属性名称匹配字符,很像字符类;属性 Alpha 指定字母字符。

/\p{Alpha}/.match('a') # => #<MatchData "a">
/\p{Alpha}/.match('1') # => nil

可以通过在名称前加上插入符号字符(^)来反转属性。

/\p{^Alpha}/.match('1') # => #<MatchData "1">
/\p{^Alpha}/.match('a') # => nil

或者通过使用 \P(大写 P)。

/\P{Alpha}/.match('1') # => #<MatchData "1">
/\P{Alpha}/.match('a') # => nil

请参阅Unicode Properties,了解基于众多属性的 regexp。

一些常用的属性对应于 POSIX 方括号表达式。

这些也经常使用:

Unicode 字符类别

Unicode 字符类别名称

示例

/\p{lu}/                # => /\p{lu}/
/\p{LU}/                # => /\p{LU}/
/\p{Uppercase Letter}/  # => /\p{Uppercase Letter}/
/\p{Uppercase_Letter}/  # => /\p{Uppercase_Letter}/
/\p{UPPERCASE-LETTER}/  # => /\p{UPPERCASE-LETTER}/

以下是 Unicode 字符类别缩写和名称。每个类别的字符枚举可在链接中找到。

字母

标记

数字

标点符号

Unicode 脚本和块

Unicode 属性包括:

POSIX 方括号表达式

POSIX方括号表达式也类似于字符类。这些表达式提供了上述方法的便携式替代方案,并具有包含非 ASCII 字符的额外好处。

POSIX 方括号表达式

Ruby 还支持以下(非 POSIX)方括号表达式:

Comments

可以使用 (?#comment) 构造在 regexp 模式中包含注释,其中 comment 是要忽略的子字符串。regexp 引擎会忽略任意文本。

/foo(?#Ignore me)bar/.match('foobar') # => #<MatchData "foobar">

注释不能包含未转义的终止符字符。

另请参阅Extended Mode

模式

以下每个修饰符都会为 regexp 设置一个模式。

这些可以全部、部分或都不应用。

修饰符 imx 可以应用于子表达式。

示例

re = /(?i)te(?-i)st/
re.match('test') # => #<MatchData "test">
re.match('TEst') # => #<MatchData "TEst">
re.match('TEST') # => nil
re.match('teST') # => nil

re = /t(?i:e)st/
re.match('test') # => #<MatchData "test">
re.match('tEst') # => #<MatchData "tEst">
re.match('tEST') # => nil

方法 Regexp#options 返回一个整数,其值显示不区分大小写模式、多行模式和扩展模式的设置。

不区分大小写模式

默认情况下,regexp 是区分大小写的。

/foo/.match('FOO')  # => nil

修饰符 i 启用不区分大小写模式。

/foo/i.match('FOO')
# => #<MatchData "FOO">

方法 Regexp#casefold? 返回该模式是否不区分大小写。

多行模式

Ruby 中的多行模式就是通常所说的“dot-all 模式”。

与其他语言不同,修饰符 m 不影响锚点 ^$。在 Ruby 中,这些锚点始终在行边界处匹配。

扩展模式

修饰符 x 启用扩展模式,这意味着:

在扩展模式下,可以使用空白和注释来形成自文档化的 regexp。

未处于扩展模式的Regexp(匹配某些罗马数字)。

pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
re = /#{pattern}/
re.match('MCMXLIII') # => #<MatchData "MCMXLIII" 1:"CM" 2:"XL" 3:"III">

处于扩展模式的Regexp

pattern = <<-EOT
  ^                   # beginning of string
  M{0,3}              # thousands - 0 to 3 Ms
  (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),
                      #            or 500-800 (D, followed by 0 to 3 Cs)
  (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs),
                      #        or 50-80 (L, followed by 0 to 3 Xs)
  (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is),
                      #        or 5-8 (V, followed by 0 to 3 Is)
  $                   # end of string
EOT
re = /#{pattern}/x
re.match('MCMXLIII') # => #<MatchData "MCMXLIII" 1:"CM" 2:"XL" 3:"III">

插值模式

修饰符 o 表示,当第一次遇到带有插值的字面量 regexp 时,生成的 Regexp 对象会被保存并用于该字面量 regexp 的所有未来评估。没有修饰符 o,生成的 Regexp 不会被保存,因此每次对字面量 regexp 的评估都会生成一个新的 Regexp 对象。

没有修饰符 o

def letters; sleep 5; /[A-Z][a-z]/; end
words = %w[abc def xyz]
start = Time.now
words.each {|word| word.match(/\A[#{letters}]+\z/) }
Time.now - start # => 15.0174892

带有修饰符 o

start = Time.now
words.each {|word| word.match(/\A[#{letters}]+\z/o) }
Time.now - start # => 5.0010866

请注意,如果字面量 regexp 没有插值,则 o 的行为是默认行为。

编码

默认情况下,仅包含 US-ASCII 字符的 regexp 具有 US-ASCII 编码。

re = /foo/
re.source.encoding # => #<Encoding:US-ASCII>
re.encoding        # => #<Encoding:US-ASCII>

包含非 US-ASCII 字符的正则表达式假定使用源编码。这可以用以下修饰符之一覆盖。

当以下任一条件满足时,regexp 可以与目标字符串匹配:

如果尝试不兼容编码的匹配,则会引发 Encoding::CompatibilityError 异常。

示例

re = eval("# encoding: ISO-8859-1\n/foo\\xff?/")
re.encoding                 # => #<Encoding:ISO-8859-1>
re =~ "foo".encode("UTF-8") # => 0
re =~ "foo\u0100"           # Raises Encoding::CompatibilityError

可以通过在 Regexp.new 的第二个参数中包含 Regexp::FIXEDENCODING 来显式固定编码。

# Regexp with encoding ISO-8859-1.
re = Regexp.new("a".force_encoding('iso-8859-1'), Regexp::FIXEDENCODING)
re.encoding  # => #<Encoding:ISO-8859-1>
# Target string with encoding UTF-8.
s = "a\u3042"
s.encoding   # => #<Encoding:UTF-8>
re.match(s)  # Raises Encoding::CompatibilityError.

超时

当 regexp 源或目标字符串来自不受信任的输入时,恶意值可能导致拒绝服务攻击;为防止此类攻击,设置超时是明智的。

Regexp 有两个超时值:

当 regexp.timeout 为 nil 时,超时会“贯穿”到 Regexp.timeout;当 regexp.timeout 非 nil 时,该值控制超时。

| regexp.timeout Value | Regexp.timeout Value |            Result           |
|----------------------|----------------------|-----------------------------|
|         nil          |          nil         |       Never times out.      |
|         nil          |         Float        | Times out in Float seconds. |
|        Float         |          Any         | Times out in Float seconds. |

优化

对于模式和目标字符串的某些值,匹配时间会相对于输入大小呈多项式或指数级增长;由此产生的潜在漏洞是正则表达式拒绝服务(ReDoS)攻击。

Regexp 匹配可以应用优化来防止 ReDoS 攻击。当应用优化时,匹配时间相对于输入大小呈线性增长(而不是多项式或指数级),并且不会发生 ReDoS 攻击。

如果模式满足以下条件,则应用此优化:

您可以使用方法 Regexp.linear_time? 来确定模式是否满足这些条件。

Regexp.linear_time?(/a*/)     # => true
Regexp.linear_time?('a*')     # => true
Regexp.linear_time?(/(a*)\1/) # => false

但是,即使该方法返回 true,不受信任的源也可能不安全,因为优化使用了记忆化(这可能会导致大量内存消耗)。

参考

阅读

探索,测试