正则
正则
https://www.kancloud.cn/imxieke/ruby-base/107304
Ruby 的特点是“万物皆对象”,正则表达式也不例外。正则表达式对象所属的类就是接下来我们将要介绍的 Regexp 类。
关于正则表达式
下面我们开始介绍有关正则表达式的一些概念及用法。
正则表达式的写法与用法
正则表达式描述的是一种“模式”,该模式被用于匹配字符串。一般情况下,我们把正则表达式模式的对象(Regexp
类对象)称为“正则表达式对象”,或直接称为“正则表达式”。
到目前为止,我们都是使用纯文本文字作为模式,而实际上还有更复杂的模式。例如,通过模式可以很简单地匹配“首字符为 A 到 D 中的某个字母,从第 2 个字符开始为数字”这样的字符串(这个模式可写为 /[A-D]\d+/
)。
但模式也并非是万能的,例如像“与 Ruby 类似的字符串”这种含糊的模式就无法书写。模式说明的东西应该更具体一些,例如“以 R 开头,以 y 结尾,由 4 个字母组成”(这个模式可写为 /R..y/
)。
正则表达式对象的创建方法
在程序中,通过用 //
将表示正则表达式模式的字符串括起来,就可以非常简单地创建出正则表达式。
另外,我们也可以使用类方法 Regexp.new(str)
来创建对象。当程序中已经定义了字符串对象 str
,且希望根据这个字符串来创建正则表达式时,用这个方法会比较好。
除上述两种方法外,与数组、字符串一样,我们也可以通过使用 %
的特殊语法来创建。正则表达式的情况下使用的是 %r
,如果正则模式中包含 /
,用这种方法会比较方便。语法如下所示:
%r
(模式)%r
<模式>%r
|模式|%r!
模式!
正则表达式的模式与匹配
了解正则表达式的创建方法后,接下来讨论一下模式。=~
方法是正则表达式中常用的方法,可以用来判断正则表达式与指定字符串是否匹配。
无法匹配时返回 nil
,匹配成功则返回该字符串起始字符的位置。
Ruby 会将 nil
与 false
解析为“假”,将除此以外的值解析为“真”,因此,如果要根据匹配结果执行不同的处理,则可以像下面这样写。
我们还可以使用 !~
来颠倒“真”与“假”。
匹配普通字符
们首先来看看如何通过模式进行简单的匹配。当模式中只写有英文、数字时,正则表达式会单纯地根据目标字符串中是否包含该模式中的字符来判断是否匹配
匹配部分都用▶匹配部分◀来表示
模式 | 字符串 | 匹配部分
| - | - /ABC/ | "ABC" | "▶ABC◀" /ABC/ | "ABCDEF" | "▶ABC◀DEF" /ABC/ | "123ABC" | "123▶ABC◀" /ABC/ | "A1B2C3" | (不匹配) /ABC/ | "AB" | (不匹配) /AB/ | "abc" | (不匹配)
匹配行首与行尾
在上面的例子中,/ABC/
模式的情况下,只要是包含 ABC
的字符串就都可以匹配。但如果我们只想匹配 ABC 这一字符串,也就是说只匹配 "ABC"
,而不匹配 "012ABC"
、"ABCDEF"
等,这时的模式应该怎么写呢?这种情况下,我们可以使用模式 /^ABC$/
。
^
、$
是有特殊意义的字符。但它们并不用于匹配 ^
与 $
字符。像这样的特殊字符,我们称之为元字符(meta character)。
^
表示匹配行首,$
表示匹配行尾。也就是说,模式 /^ABC/
匹配行首为 ABC
的字符串,模式 /ABC$/
匹配行尾为 ABC
的字符串。
模式 | 字符串 | 匹配部分
| - | - /^ABC$/ | "ABC" | "▶ABC◀" /^ABC$/ | "ABCDEF" | (不匹配) /^ABC$/ | "123ABC" | (不匹配) /^ABC/ | "ABC" | "▶ABC◀" /^ABC/ | "ABCDEF" | "▶ABC◀DEF" /^ABC/ | "123ABC" | 不匹配 /ABC$/ | "ABC" | "▶ABC◀" /ABC$/ | "ABCDEF" | (不匹配) /ABC$/ | "123ABC" | "123▶ABC◀"
行首与行尾
^
、$
分别匹配“行首”、“行尾”,而不是“字符串的开头”、“字符串末尾”。匹配字符串的开头用元字符 \A
,匹配字符串的末尾用元字符 \z
。
这两种情况有什么不同呢? Ruby 的字符串,也就是 String
对象中,所谓的“行”就是用换行符 (\n)
间隔的字符串。因此模式 /^ABC/
也可以匹配字符串 "012\nABC"
。也就是说
像上面这种跨两行的字符串的情况下,由于第 2 行是以 ABC
开头的,因此也可以匹配。
那么为什么要将行首 /
行尾与字符串的开头 /
结尾分开定义呢?这是有历史原因的。
具体来说,原本正则表达式只能逐行匹配字符串,不能匹配多行字符串。因此就可以认为一个“字符串”就是一“行”。
但是,随着正则表达式的广泛使用,人们开始希望可以匹配多行字符串。而如果仍用 ^
、$
来匹配字符串的开头、结尾的话就很容易造成混乱,因此就另外定义了匹配字符串开头、结尾的元字符。
另外,还有一个与 \z
类似的表现,就是 \Z
,不过两者的作用有点不一样。\Z
虽然也是匹配字符串末尾的元字符,但它有一个特点,就是如果字符串末尾是换行符,则匹配换行符前一个字符。
我们一般常用 \z
,而很少使用 \Z
。
指定匹配字符的范围
时候我们会希望匹配“ABC 中的 1 个字符”。像这样,选择多个字符中的 1 个时,我们可以使用 []
。
[AB]
……A或B[ABC]
……A或B或C[CBA]
……同上(与[]中的顺序无关)[012ABC]
……0、1、2、A、B、C中的1个字符不过,如果按照这样的写法,那么匹配“从 A 到 Z 的全部英文字母”时就麻烦了。这种情况下,我们可以在
[]
中使用-
,来表示一定范围内的字符串。[A-Z]
……从A到Z的全部大写英 文字母[a-z]
……从a 到z 的全部小写英文字母[0-9]
……从0到9 的全部数字[A-Za-z]
……从A到Z与从a到z的全部英文字母[A-Za-z_]
……全部英 文字母与 _备注 字符的范围也称为“字符类”。请注意这里的“类”与面向对象中的“类”的意义是不一样的。
如果
-
是[]
中首个或者最后 1 个字符,那么就只是单纯地表示-
字符。反过来说,如果-
表示的不是字符类,而是单纯的字符-
,那么就必须写在模式的开头或者末尾。[A-Za-z0-9_-]
……全部英文字母、全部数字、_、-在
[]
的开头使用^
时,^
表示指定字符以外的字符。[^ABC]
……A、B、C 以外的字符[^a-zA-Z]
……a 到 z,A 到 Z(英文字母)以外的字符表为一些实际进行匹配的例子。另外,在 1 个模式中还可以使用多个
[]
模式 | 字符串 | 匹配部分
| - | - /[ABC]/ | "B" | "▶B◀" /[ABC]/ | "BCD" | "▶B◀CD" /[ABC]/ | "123" | (不匹配) /a[ABC]c/ | "aBc" | "▶aBc◀" /a[ABC]c/ | "1aBcDe" | "1▶aBc◀De" /a[ABC]c/ | "abc" | (不匹配) /[^ABC]/ | "1" | "▶1◀" /[^ABC]/ | "A" | (不匹配) /a[^ABC]c/ | "aBcabc" | "aBc▶abc◀" /[ABC][AB]/ | "AB" | "▶AB◀" /[ABC][AB]/ | "AA" | "▶AA◀" /[ABC][AB]/ | "CA" | "▶CA◀" /[ABC][AB]/ | "CCCCA" | "CCC▶CA◀" /[ABC][AB]/ | "xCBx" | "x▶CB◀x" /[ABC][AB]/ | "CC" | (不匹配) /[ABC][AB]/ | "CxAx" | (不匹配) /[ABC][AB]/ | "C" | (不匹配) /[0-9][A-Z]/ | "0A" | "▶0A◀" /[0-9][A-Z]/ | "000AAA" | "00▶0A◀AA" /[^A-Z][A-Z]/ | "1A2B3C" | "▶1A◀2B3C" /[^0-9][^A-Z]/ | "1A2B3C" | "1▶A2◀B3C"
匹配任意字符
有时候我们会希望定义这样的模式,即“不管是什么字符,只要匹配 1 个字符就行”。这种情况下,我们可以使用元字符 .
。
……
匹配任意字符模式 | 字符串 | 匹配部分
| - | - /A.C/ | "ABC" | "▶ABC◀" /A.C/ | "AxC" | "▶AxC◀" /A.C/ | "012A3C456" | "012▶A3C◀456" /A.C/ | "AC" | (不匹配) /A.C/ | "ABBC" | (不匹配) /A.C/ | "abc" | (不匹配) /aaa.../ | "00aaabcde" | "00▶aaabcd◀e" /aaa.../ | "aaabb" | (不匹配)
然而,可能有读者会问:“程序在什么时候会需要能够匹配任意字符的字符呢”。的确,任意字符都能匹配的话,也就没有必要特意指定了。
在下面两种情况下,一般会使用这个元字符。
在希望指定字符数时使用 /^...$/
这样的模式可以匹配字符数为 3 的行。与元字符 *
配合使用
使用反斜杠的模式
与字符串一样,我们也可以使用 +1 个英文字母这样的形式来表示换行、空白等特殊字符。
\s
表示空白符,匹配空格(0x20)、制表符(Tab)、换行符、换页符
模式 | 字符串 | 匹配部分
| - | - /ABC\sDEF/ | "ABC DEF" | "▶ABC DEF◀" /ABC\sDEF/ | "ABC\tDEF" | "▶ABC\tDEF◀" /ABC\sDEF/ | "ABCDEF" | (不匹配)
\d
匹配 0 到 9 的数字
模式 | 字符串 | 匹配部分
| - | - /\d\d\d-\d\d\d\d/ | "012-3456" | "▶012-3456◀" /\d\d\d-\d\d\d\d/ | "01234-012345" | "01▶234-0123◀45" /\d\d\d-\d\d\d\d/ | "ABC-DEFG" | (不匹配) /\d\d\d-\d\d\d\d/ | "012-21" | (不匹配)
\w
匹配英文字母与数字
模式 | 字符串 | 匹配部分
| - | - /\w\w\w/ | "ABC" | "▶ABC◀" /\w\w\w/ | "abc" | "▶abc◀" /\w\w\w/ | "012" | "▶012◀" /\w\w\w/ | "AB C" | (不匹配) /\w\w\w/ | "AB\nC" | (不匹配)
\A
匹配字符串的开头
模式 | 字符串 | 匹配部分
| - | - /\AABC/ | "ABC" | "▶ABC◀" /\AABC/ | "ABCDEF" | "▶ABC◀DEF" /\AABC/ | "012ABC" | (不匹配) /\AABC/ | "012\nABC" | (不匹配)
\z
匹配字符串的末尾
模式 | 字符串 | 匹配部分
| - | - /ABC\z/ | "ABC" | "▶ABC◀" /ABC\z/ | "012ABC" | "012▶ABC◀" /ABC\z/ | "ABCDEF" | (不匹配) /ABC\z/ | "012\nABC" | "012\n▶ABC◀" /ABC\z/ | "ABC\nDEF" | (不匹配)
元字符转义
我们还可以用
\
对元字符进行转义。在\
后添加^
、$
、[
等非字母数字的元字符后,该元字符就不再发挥元字符的功能,而是直接被作为元字符本身来匹配模式 | 字符串 | 匹配部分
| - | - /ABC[/ | "ABC[" | "▶ ABC[ ◀" /^ABC/ | "ABC" | (不匹配) /^ABC/ | "012^ABC" | "012▶^ABC◀"
重复
有时候,我们会需要重复匹配多次相同的字符。例如,匹配“"Subject:"
字符串后多个空白符,空白符后又有字符串这样的行”(这是匹配电子邮件的主题时使用的模式)。
正则表达式中用以下元字符来表示重复匹配的模式。
*** ……重复 0 次以上**
使用 * 的例子
模式 | 字符串 | 匹配部分
| - | - /A*/ | "A" | "▶A◀" /A*/ | "AAAAAA" | "▶AAAAAA◀" /A*/ | "" | "▶◀" /A*/ | "BBB" | "▶◀"BBB /AC/ | "AAAC" | "▶AAAC◀" /AC/ | "BC" | "B▶C◀" /AC/ | "AAAB" | (不匹配) /AAAC/ | "AAC" |"▶AAC◀" /AAAC/ | "AC" |(不匹配) /A.C/ | "AB012C" | "▶AB012C◀" /A.C/ | "AB CD" | "▶AB C◀D" /A.C/ | "ACDE" | "▶AC◀DE" /^Subject:\s.$/ | "Subject: foo" | "▶Subject: foo◀" /^Subject:\s.$/ | "Subject: Re: foo" | "▶Subject: Re: foo◀" /^Subject:\s*.$/ | "Subject:Re^2 foo" | "▶Subject:Re^2 foo◀" /^Subject:\s.*$/ | "in Subject:Re foo" | (不匹配)
+ ……重复 1 次以上
模式 | 字符串 | 匹配部分
| - | - /A+/ | "A" | "▶A◀" /A+/ | "AAAAAA" | "▶AAAAAA◀" /A+/ | "" | (不匹配) /A+/ | "BBB" | (不匹配) /A+C/ | "AAAC" | "▶AAAC◀" /A+C/ | "BC" | (不匹配) /A+C/ | "AAAB" | (不匹配) /AAA+C/ | "AAC" | (不匹配) /AAA+C/ | "AC" | (不匹配) /A.+C/ | "AB012C" | "▶AB012C◀" /A.+C/ | "AB CD" | "▶AB C◀D" /A.+C/ | "ACDE" | (不匹配)
? ……重复 0 次或 1 次
模式 | 字符串 | 匹配部分
| - | - /^A?$/ | "A" | "▶A◀" /^A?$/ | "" | "▶◀" /^A?$/ | "AAAAAA" | (不匹配) /^A?C/ | "AC" | "▶AC◀" /^A?C/ | "AAAC" | (不匹配) /^A?C/ | "BC" | (不匹配) /^A?C/ | "C" | "▶C◀" /AAA?C/ | "AAAC" | "▶AAAC◀" /AAA?C/ | "AAC" | "▶AAC◀" /AAA?C/ | "AC" | (不匹配) /A.?C/ | "ACDE" | "▶AC◀DE" /A.?C/ | "ABCDE" | "▶ABC◀DE" /A.?C/ | "AB012C" | (不匹配) /A.?C/ | "AB CD" | (不匹配)
最短匹配
匹配 0 次以上的 *
以及匹配 1 次以上的 +
会匹配尽可能多的字符 1。相反,匹配尽可能少的字符 2 时(重复后的模式首次出现的位置之前的部分),我们可以用以下元字符:
*? ……0 次以上的重复中最短的部分
+? ……1 次以上的重复中最短的部分
模式 | 字符串 | 匹配部分
| - | - /A.B/ | "ABCDABCDABCD" | "▶ABCDABCDAB◀CD" /A.C/ | "ABCDABCDABCD" | "▶ABCDABCDABC◀D" /A.?B/ | "ABCDABCDABCD" | "▶AB◀CDABCDABCD" /A.?C/ | "ABCDABCDABCD" | "▶ABC◀DABCDABCD" /A.+B/ | "ABCDABCDABCD" | "▶ABCDABCDAB◀CD" /A.+C/ | "ABCDABCDABCD" | "▶ABCDABCDABC◀D" /A.+?B/ | "ABCDABCDABCD" | "▶ABCDAB◀CDABCD" /A.+?C/ | "ABCDABCDABCD" | "▶ABC◀DABCDABCD"
() 与重复
在刚才的例子中,我们只是重复匹配了 1 个字符,而通过使用 ()
,我们还可以重复匹配多个字符
模式 | 字符串 | 匹配部分
| - | - /^(ABC)$/ | "ABC" | "▶ABC◀" /^(ABC)$/ | "" | "▶◀" /^(ABC)$/ | "ABCABC" | "▶ABCABC◀" /^(ABC)$/ | "ABCABCAB" | (不匹配) /^(ABC)+$/ | "ABC" | "▶ABC◀" /^(ABC)+$/ | "" | (不匹配) /^(ABC)+$/ | "ABCABC" | "▶ABCABC◀" /^(ABC)+$/ | "ABCABCAB" | (不匹配) /^(ABC)?$/ | "ABC" | "▶ABC◀" /^(ABC)?$/ | "" | "▶◀" /^(ABC)?$/ | "ABCABC" | (不匹配) /^(ABC)?$/ | "ABCABCAB" | (不匹配)
选择
我们可以用 |
在几个候补模式中匹配任意一个
模式 | 字符串 | 匹配部分
| - | - /^(ABC|DEF)$/ | "ABC" | "▶ABC◀" /^(ABC|DEF)$/ | "DEF" | "▶DEF◀" /^(ABC|DEF)$/ | "AB" | (不匹配) /^(ABC|DEF)$/ | "ABCDEF" | (不匹配) /^(AB|CD)+$/ | "ABCD" | "▶ABCD◀" /^(AB|CD)+$/ | "" | (不匹配) /^(AB|CD)+$/ | "ABCABC" | (不匹配) /^(AB|CD)+$/ | "ABCABCAB" | (不匹配)
使用 quote 方法的正则表达式
有时候我们可能会希望转义(escape)正则表达式中的所有元字符。而 quote
方法就可以帮我们实现这个想法。quote
方法会返回转义了元字符后的正则表达式字符串,然后再结合 new
方法,就可以生成新的正则表达式对象了。
quote
方法的问题在于不能以元字符的格式写元字符。因此,在写一些复杂的正则表达式时,建议不要使用 quote
方法,而是乖乖地对元字符进行转义。
正则表达式的选项
正则表达式中还有选项,使用选项可以改变正则表达式的一些默认效果。
设定正则表达式的选项时,只需在 /…/
的后面指定即可,如 /… /im
,这里的 i
以及 m
就是正则表达式的选项。
i
忽略英文字母大小写的选项。指定这个选项后,无论字符串中的字母是大写还是小写都会被匹配。
x
忽略正则表达式中的空白字符以及
#
后面的字符的选项。指定这个选项后,我们就可以使用#
在正则表达式中写注释了。m
指定这个选项后,就可以使用 . 匹配换行符了。
表中总结了几种常用的选项。
选项 | 选项常量 | 意义
| - | - i | Regexp::IGNORECASE | 不区分大小写 x | Regexp::EXTENDED | 忽略模式中的空白字符 m | R egexp::MULTILINE | 匹配多行 o | (无) | 只使用一次内嵌表达式
Regexp.new
方法中的第 2 个参数可用于指定选项常量。只需要 1 个参数时,可不指定第 2 个参数或者直接指定 nil
。
例如,/Ruby
脚本 /i
这一正则表达式,可以像下面那样写:
另外,我们还可以用 |
指定多个选项。这时,/Ruby
脚本 /im
这一正则表达式就变成了下面这样:
捕获
除了检查字符是否匹配外,正则表达式还有另外一个常用功能,甚至可以说是比匹配更加重要的功能——捕获(后向引用)。
所谓捕获,就是从正则表达式的匹配部分中提取其中的某部分。通过“$ 数字”这种形式的变量,就可以获取匹配了正则表达式中的用 ()
括住的部分的字符串。
在进行匹配的时候,我们只知道是否匹配、匹配第几个字符之类的信息。而使用捕获后,我们就可以知道哪部分被匹配了。因此,通过这个功能,我们就可以非常方便地对字符串进行分析。
我们提到了 ()
也被用于将多个模式整理为一个。在修改程序中的正则表达式时,如果改变了 ()
的数量,那么将要引用的部分的索引也会随之改变,有时就会带来不便。这种情况下,我们可以使用 (?: )
过滤不需要捕获的模式。
除了“$
数字”这种形式以外,保存匹配结果的变量还有 $
、$&
、$
,分表代表匹配部分前的字符串、匹配部分的字符串、匹配部分后的字符串。为了方便大家快速理解这 3 个变量的含义,我们来看看下面这个例子:
这样一来,我们就可以将字符串整体分为匹配部分与非匹配部分,并将其分别保存在 3 个不同的变量中。
使用正则表达式的方法
字符串相关的方法中有一些使用了正则表达式,接下来我们就来介绍一下其中的 sub
方法、gsub
方法、scan
方法。
sub 方法与 gsub 方法
sub
方法与 gsub
方法的作用是用指定的字符置换字符串中的某部分字符。
sub
方法与 gsub
方法都有两个参数。第 1 个参数用于指定希望匹配的正则表达式的模式,第 2 个参数用于指定与匹配部分置换的字符。sub
方法只置换首次匹配的部分,而 gsub
方法则会置换所有匹配的部分。
/\s+/
是用于匹配 1 个以上的空白字符的模式。因此在本例中,sub
方法与 gsub
方法会将匹配的空白部分置换为 1 个空白。sub
方法只会置换 abc
与 def
间的空白,而 gsub
方法则会将字符串后面匹配的空白部分全部置换。
sub
方法与 gsub
方法还可以使用块。这时,程序会将字符串中匹配的部分传递给块,并在块中使用该字符串进行处理。这样一来,块中返回的字符串就会置换字符串中匹配的部分。
在本例中,程序会将字符串 a
以及 a
之前的字母转换为大写,并用 <>
将其括起来。
sub
方法与 gsub
方法也有带 !
的方法。sub!
方法与 gusb!
方法会直接将作为接受者的对象变换为置换后的字符串。
scan 方法
scan
方法能像 gsub
方法那样获取匹配部分的字符,但不能做置换操作。因此,当需要对匹配部分做某种处理时,可以使用该方法
执行示例
在正则表达式中使用 ()
时,匹配部分会以数组的形式返回
执行示例
另外,如果指定与 ()
相等数量的块参数,则返回的结果就不是数组,而是各个元素。
执行示例
如果没有指定块,则直接返回匹配的字符串数组。
正则表达式的例子
接下来我们来看看用正则表达式匹配 URL 的例子。
首先我们需要“找出包含 URL 的行”。创建表示完整的 URL 的正则表达式会非常复杂,不过我们可以稍微变通一下,把目标改为“找出类似于 URL 的字符串”,这时,就可以用如下模式来进行匹配。
这个匹配模式的好处在于便于操作,而且也的确可以匹配 URL。
在此基础上,我们还可以进一步写出“获取类似于 URL 的字符串中的某部分”的正则表达式。例如,获取 HTTP 的 URL 中的服务器地址的模式时,可以像下面这样书写。
[^\/]*
表示匹配不含 /
的连续字符串。
上述例子中使用了较多 /
,不便于阅读,这种情况下我们可以使用 %r
将其改写成像下面那样:
执行示例
可以发现,的确可以获取服务器地址。
然后,我们再看看获取服务器地址以外部分的正则表达式。
这是在 RFC2396“Uniform Resource Identifiers(URI)”这个定义 URI 语法的文件中使用的正则表达式。
这个正则表达式可以被原封不动地用在 Ruby 中。如果用这个正则表达式进行匹配,则 HTTP 等协议名会被保存在 $2
中,服务器地址等会被保存在 $4
中,路径名会被保存在 $5
中,请求部分会被保存在 $7
中,片段(fragment)会被保存在 $9
中。
例如,http://www.example.co.jp/foo/?name=bar#bar
这个 URI 的情况下,http
为通信协议名,www.example.co.jp
为服务器地址,/foo/
为路径名,name=bar
为请求名,baz
为片段。
然而,写到这种程度,正则表达式已经变得非常复杂了。如果把正则表达式写成在任何情况下都能匹配的万能模式,就会使得正则表达式变得难以读懂,增加程序的维护成本。相比之下,只满足当前需求的正确易懂的正则表达式则往往更有效率。
例如,匹配邮政编号的正则表达式,可以写成下面这样:
这样,就不会匹配只有 3 位数字,或者没有 -
的邮政编码了。
在不需要太过严格的输入检查时,直接用 /\d+-?\d*/
匹配就可以了。