类 Regexp

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

正则表达式的常见表示法是使用斜杠字符括起来

/foo/

正则表达式可以应用于目标字符串;字符串中匹配模式的部分(如果有)称为匹配,并且可以说匹配

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

可以使用以下方式创建正则表达式

方法 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

全局变量

某些面向正则表达式的方法会为全局变量赋值

受影响的全局变量是

示例

# 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?不设置全局变量。

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

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

可用的丰富子表达式集合赋予正则表达式强大的功能和灵活性

特殊字符

正则表达式的特殊字符,称为元字符,在某些上下文中具有特殊含义;根据上下文,这些有时是元字符

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

要按字面意思匹配元字符,请使用反斜杠转义

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

要按字面意思匹配反斜杠,请使用反斜杠转义

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

Method Regexp.escape返回转义后的字符串

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

源字面量

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

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

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

普通字符串字面量和源字面量之间存在差异;请参阅速记字符类

字符类

字符类用方括号分隔;它指定在目标字符串中的给定点匹配某些字符

# 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">

匹配重置锚点

选择符

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

两种选择

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 提供了一种方便的方式来构造具有选择符的正则表达式。

量词

一个简单的正则表达式匹配一个字符

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

添加的量词指定需要或允许的匹配次数

贪婪、懒惰或占有匹配

量词匹配可以是贪婪的、懒惰的或占有的

更多

组和捕获

一个简单的正则表达式(最多)有一个匹配项

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

添加一个或多个括号对,(子表达式),定义,这可能会导致多个匹配的子字符串,称为捕获

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">

反向引用

组匹配也可以在正则表达式本身中引用;这种引用称为反向引用

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

此表显示了正则表达式中每个子表达式如何匹配目标字符串中的子字符串

| 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'          |

一个正则表达式可以包含任意数量的组

命名捕获

如上所述,捕获可以通过其编号引用。捕获也可以有一个名称,前缀为 ?<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"

当正则表达式包含命名捕获时,没有未命名的捕获

/\$(?<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">

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

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

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

原子分组

可以使用 (?>子表达式) 将组设置为原子

这将导致子表达式独立于表达式的其余部分进行匹配,以便匹配的子字符串在匹配的其余部分中变为固定,除非必须放弃并随后重新访问整个子表达式。

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

示例(不使用原子分组)

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

分析

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

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

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

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

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

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

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

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

子表达式调用

如上所述,反向引用编号 (\n) 或名称 (\k<name>) 提供对捕获的子字符串的访问权限;相应的正则表达式子表达式也可以通过编号 (\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. 匹配字符串的结尾。

请参阅 子表达式调用

条件

条件构造采用 (?(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 属性 以获取基于众多属性的正则表达式。

一些常用属性对应于 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)括号表达式

注释

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

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

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

另请参阅扩展模式

模式

以下每个修饰符都为正则表达式设置一个模式

可以应用其中任意一个、全部或都不应用。

修饰符 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 返回一个整数,其值显示不区分大小写模式、多行模式和扩展模式的设置。

不区分大小写模式

默认情况下,正则表达式是区分大小写的

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

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

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

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

多行模式

Ruby 中的多行模式通常称为“点全部模式”

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

扩展模式

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

在扩展模式下,可以使用空格和注释来形成自文档化的正则表达式。

未处于扩展模式的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对象,并将其用于该文字正则表达式的所有未来评估。如果没有修饰符 o,则不会保存生成的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

请注意,如果文字正则表达式没有插值,则 o 行为为默认行为。

编码

默认情况下,仅包含 US-ASCII 字符的正则表达式具有 US-ASCII 编码

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

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

当满足以下任一条件时,正则表达式可以与目标字符串匹配

如果尝试在不兼容的编码之间进行匹配,则会引发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.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) 攻击。

正则表达式匹配可以应用优化来防止 ReDoS 攻击。应用优化后,匹配时间与输入大小成线性(不是多项式或指数级)增长,并且不可能发生 ReDoS 攻击。

如果模式符合以下条件,则应用此优化

可以使用方法 Regexp.linear_time? 来确定模式是否符合这些条件

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

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

参考

阅读(在线 PDF 书籍)

探索,测试(交互式在线编辑器)