共计 7237 个字符,预计需要花费 19 分钟才能阅读完成。
正则表达式(英语:Regular Expression,常简称为 regex、regexp 或 RE)是使用单个字符串来描述、匹配一系列某个句法规则的字符串,其通常被用来检索、替换匹配某个模式的文本。由于强大的文本处理能力,使得不少编程语言都自带正则表达式相关的库,可以说熟练掌握正则表达式是程序员的基本功之一,正是基于此,笔者这里对正则表达式进行一个小结。
规则
一个正则表达式通常被称为一条模式(pattern),如 [1-9]
这个模式可表示 1 到 9 之间的数字。模式的语法规则如下。
元字符
字符 | 说明 | 示例 |
---|---|---|
. |
匹配除换行符(即 \r 、\n )以外的任意单个字符。若要匹配所有字符,可使用类似于 (.\|\r\|\n) 的模式 |
.? 匹配 0 到 1 个除换行符外的任意字符 |
^ |
匹配字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也可以匹配 \n 和 \r 之后的位置 |
^te 能匹配 test,但不匹配 interesting |
$ |
匹配字符串的结束位置。如果设置了 RegExp 对象的 Multiline 属性,$ 也可以匹配 \n 和 \r 之前的位置 |
st$ 能匹配 test,但不匹配 interesting |
* |
匹配前面的子字符(分组)零次或多次,等价于 {0,} |
zo* 能匹配 z、zo、zoo 和 zooooo 等字符串 |
+ |
匹配前面的子字符(分组)一次或多次,等价于 {1,} |
zo+ 能匹配 zo、zoo 和 zooooo 等字符串 |
? |
匹配前面的子字符(分组)零次或一次,等价于 {0,1} |
zo? 能匹配 z、zo |
{n} |
匹配确定的 n 次,n 为一个非负整数 | zo{2} 匹配 zoo |
{n,} |
至少匹配 n 次,n 为一个非负整数 | zo{1,} 匹配 zo, zoo 和 zooo 等字符串 |
{n,m} |
最少匹配 n 次且最多匹配 m 次,n, m 均为非负整数且 n<=m |
zo{1,3} 匹配 zo, zoo 和 zooo |
\| |
分枝条件 | gray\|grey 可以匹配 grey 或 gray |
[] |
指定匹配的字符范围
|
|
() |
定义了匹配字符的范围,会对匹配到的文本进行分组,可带数量后缀。更多高级用法见分组与捕获 |
|
\ |
转义。将下一个字符标记为特殊字符、原义字符、后向引用 、八/十六进制字符或 Unicode 字符串 |
|
\b |
匹配单词的边界 | er\b 能匹配 never 中的 er,但不能匹配 verb 中的 er |
\B |
匹配非单词的边界 | er\B 能匹配 verb 中的 er,但不能匹配 never 中的 er |
\d |
匹配一个数字字符,等价于 [0-9] |
\d 匹配 0-9 之间的数字 |
\D |
匹配一个非数字字符,等价于 [^0-9] |
\D 匹配非数字字符 |
\w |
匹配字母、数字、下划线和 Unicode 字符,等价于 [A-Za-z0-9_] |
|
\W |
匹配非字母、非数字、非下划线和非 Unicode 字符。等价于 [^A-Za-z0-9_] |
|
\f |
匹配一个换页符,等价于 \x0c 和 \cL |
|
\n |
匹配一个换行符,等价于 \x0a 和 \cJ |
|
\r |
匹配一个回车符,等价于 \x0d 和 \cM |
|
\t |
匹配一个制表符,等价于 \x09 和 \cI |
|
\h |
匹配一个水平空白符,等价于 [ \t\xA0\u1680\u180e\u2000-\u200a\u202f\u205f\u3000] |
|
\H |
匹配一个非水平空白符,等价于 [^\] |
|
\v |
匹配一个垂直空白符,等价于 [\n\x0B\f\r\x85\u2028\u2029] |
|
\V |
匹配一个非垂直空白符,等价于 [^\v] |
|
\s |
匹配任意空白符,包括空格、制表符、换页符等,等价于 [\f\n\r\t\v] |
|
\S |
匹配任何非空白字符,等价于 [^\f\n\r\t\v] |
|
\cx |
匹配由 x 指明的控制字符,其中 x 的值必须为 [A-Za-z] 之中的字符,否则将 c 视为一个原义 c 字符 |
\cM 表示 Ctrl-M 或回车符;\cC 表示 Ctrl-C |
\x |
十六进制转义字符串,格式为 \xnn ,匹配两个十六进制数字 nn 表示的字符 |
\x41 匹配 A |
\n |
其中 n 为一个非负整数,而不是字符 n,根据情况表示不同含义:
|
|
\u |
Unicode 转义字符串,格式为 \unnnn ,其中 n 为非负整数 |
\u0022 匹配 " ;\u002A 匹配 * |
表 1:正则表达式元字符表
POSIX 字符类
字符 | 说明 |
---|---|
\p{Lower} |
等价于 [a-z] |
\p{Upper} |
等价于 [A-Z] |
\p{Alpha} |
等价于 [\p{Lower}\p{Upper}] |
\p{Digit} |
等价于 [0-9] |
\p{Alnum} |
等价于 [\p{Alpha}\p{Digit}] |
\p{ASCII} |
等价于 [\x00-\x7F] |
\p{Blank} |
空格或 tab,等价于 [ \t] |
\p{Space} |
空白符,等价于 [ \t\n\x0B\f\r] |
\p{Punct} |
标点符号,!"#$%&'()*+,-./:;<=>?@[\]^_`{\|}~ 中的一个 |
\p{Graph} |
可见字符,等价于 [\p{Alnum}\p{Punct}] |
\p{Print} |
可打印符号,等价于 [\p{Graph}\x20] |
\p{Cntrl} |
等价于 [\x00-\x1F\x7F] |
\p{XDigit} |
十六进制数,即 [0-9a-fA-F] |
字符优先级
各元字符的优先级从高到低如下:
\
()
、(?:)
、(?=)
、[]
*
、+
、?
、{n}
、{n,}
、{,m}
、{n,m}
^
、$
、\
转义的字符|
同一优化级的计算从左到右进行,不同优化级的计算从高到低进行。
匹配次数
贪婪匹配
表 1 中有 ?
、+
、*
、{n}
、{n,}
、{n,m}
这 6 种表示匹配次数的模式,这里有个问题,除了 {n}
指定了确定的匹配次数外,其它五种模式到底是匹配多少次呢?以字符串 aababc 而例,ab*
是匹配 ab 还是 abab 呢?
在默认情况下,正则匹配是贪婪匹配,即若正则表达式中包含能接受重复的限定符时,默认会匹配尽可能多的字符。以 ab*
为例,对于 aababc
而言,它将匹配 abab。
懒惰匹配
有时我们需要匹配尽可能少的字符,那该怎么办呢?只需要在限定符后面加上一个问号 ?
或 +
即可,这五种模式都可以转化为懒惰匹配模式。以 a.*?b
(或 a.*+b
,下文不再赘述)为例,对于 aababc 而言,它将匹配 aab。
据此可得出如下懒惰限定表:
表达式 | 说明 |
---|---|
?? |
匹配 0 次或 1 次,但尽可能少的重复 |
+? |
至少匹配 1 次,但尽可能少的重复 |
*? |
重复任意次,但尽可能少的重复 |
{n,}? |
至少匹配 n 次,但尽可能少的重复 |
{m, n}? |
匹配 m 到 n 次,但尽可能少的重复 |
表 2:懒惰限定表
分组与捕获
在表 1 中提到 (pattern)
会对匹配到的文本进行分组,实际上,每个匹配到的分组默认自动拥有一个组号,组号从 1 开始,下个分组组号在上个分组组号基础上加 1,即左到右分组组号依次为 1,2, 3…,组号 0 表示整个匹配到的文本。
我们经常会利用分组来获取文本中的指定字段,以默认的 nginx log 为例:
185.165.190.34 – – [26/Apr/2022:18:31:43 +0800] “GET / HTTP/1.1” 200 612 “-” “Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36”
如果想解析出上述日志中 Chrome 的版本,可通过 (.*) Chrome\/([0-9\.]*) (.*)
这样的正则表达式来匹配日志文本,组号为 2 的分组即为 Chrome 版本(41.0.2228.0)。
用组号获取分组信息虽然简单,但每次使用时我们都会数下组号,如果分组太多就很容易数错了,而且很多时候我们只关心少数几个分组,对其他多数分组并不关心。此时我们可以通过 (?<name>pattern)
或 (?'name'pattern)
显式指定分组名,然后使用名称为 name 的分组即可。
:::tip
Python/Golang 等部分编程语言使用 (?P<name>pattern)
指定分组名。
:::
根据是否捕获(capturing)匹配的文本以供使用,可分为捕获匹配和非捕获匹配。(pattern)
和 (?<name>pattern)
都可以使用匹配后的文本,因此是一种捕获(capturing)匹配。若只想匹配模式,但不需要使用对应匹配的文本,也不用给分组分配组号,可使用 (?:pattern)
表示 ,该模式是一种非捕获匹配。
后向引用
后向引用(backreferences)用于搜索前面某个分组匹配的文本,用来避免书写相同的分组表达式。后向引用通过 \n
来引用前面的分组,其中 n 为从 1 开始的正整数。如:
(.)\1
:匹配两个连续的相同字符\b(\w+)\b\s+\1\b
:\1
表示分组1
匹配的字符,即(\w+)
如果要引用指定名称的分组,使用 \k<name>
引用该分组,例如 (?<word>\w+)
将 \w+
的组名指定为 word,然后通过 \k<word>
引用该分组。下面两个正则表达式的效果和使用上面使用分组号的两个表达式的效果一样:
(?<any>.)\k<any>
\b(?<word>\w+)\b\s+\k<word>\b
零宽断言
在程序设计中,为了验证程序运行结果和开发者预期是否一致,常常会使用断言(assertion)这种技术,当程序执行到断言的位置时,该断言应该为真,否则程序会中止执行,并给出错误消息。
正则表达式中提供了一类名为零宽断言(zero-width assertions)的模式,该模式自身不消费任何字符(即零个字符,故称零宽),仅测试文本是否满足某种条件(即断言)。零宽断言和 \b
、 $
、^
一样,匹配一个位置,且匹配到的文本不会保存到匹配结果中,是一种非捕获匹配。
根据匹配方向(下表中的 Lookbehind 和 Lookahead)和是否匹配(下表中的 Positive 和 Negative)有如下四种零宽断言模式:
Assertion | Lookahead | Lookbehind |
---|---|---|
Positive | (?=pattern) |
(?<=pattern) |
Negative | (?!pattern) |
(?<!pattern) |
表 3:零宽断言
可以将 Lookbehind 中的 <
看成文本检测方向来方便记忆
Positive Lookahead Assertion
Positive Lookahead Assertion 表示方法为 (?=pattern)
,在任何匹配 pattern 的起始处检测文本,如 iPhone(?=11|12|13)
能匹配 iPhone13 中的 iPhone,但不会匹配 iPhone SE 中的 iPhone。
Negative Lookahead Assertion
Negative Lookahead Assertion 的表示方法为 (?!pattern)
,在任何不匹配 pattern 的起始处检测文本,如 iPhone(?!11|12|13)
能匹配 iPhoneSE 中的 iPhone,但不会匹配 iPhone13 中的 iPhone。
Positive Lookbehind Assertion
Positive Lookbehind Assertion 的表示方法为 (?<=pattern)
,在任何匹配 pattern 的起始处反向检测文本,如 (?<=11|12|13)iPhone
能匹配 13iPhone 中的 iPhone,但不会匹配 SE iPhone 中的 iPhone。
Negative Lookbehind Assertion
Lookbehind Negative Assertion 的表示方法为 (?<!pattern)
,在任何不匹配 pattern 的起始处反向检测文本,如 (?<!11|12|13)iPhone
能匹配 SE iPhone 中的 iPhone,但不会匹配 13iPhone 中的 iPhone。
可能看到这里,有些读者朋友还是不太明白零宽断言的用处,何时使用零宽断言呢?笔者这里举例说明其用处,以如下文本为例:
>
- Hi guys
- James has sold his car
- Do you see it
- The main road
如果要找出所有包含字母 i 且紧跟其后的字母非 t 的单词,即 his 和 main 这两个单词。我们可能会使用 \b\w*i[^t]\w+\b
来匹配文本,但该表达式会将 Hi guys 也匹配出来,原因是 [^t]
会匹配一个非 t 字符,该字符可能为 a-s 或 u-z 中一个,可能为空格、句号、逗号等符号,其后的 \w+\b
再匹配下一个单词,导致将 Hi guys 错误的输出出来。而零宽断言不会消费字符,仅仅匹配一个位置,这里使用 Lookahead Negative Assertion \b\w*i(?!t)\w+\b
即可解决问题。
注释
(?#comment)
表示注释,对正则表达式不会产生影响。
总结如下图:
模式修饰符
正则表达式的修饰符可能会影响结果,而不同编程语言对此的定义并不完全相同。
Java
修饰符(Pattern.Modifier ) |
表达式 | 说明 | 示例 |
---|---|---|---|
UNIX_LINES | (?d) |
开启 Unix 行模式,即 \n 结束符仅在 . 、^ 和 $ 处才会被识别 |
|
CASE_INSENSITIVE | (?i) |
Case insensitive,忽略大小写 | te(?i)st 匹配 test 和 teST 等,但不匹配 TEst 和 TEST 等 |
COMMENTS | (?x) |
开启注释模式,此模式下会忽略空格,文本中的行从 # 开始到末尾的字符都会被当成注释忽略 |
(?x)a#b 匹配 a |
MULTILINE | (?m) |
开启多行模式,此时使用 ^ 和 $ 来匹配文本中每行的开始和结尾 |
(?m)^. 匹配 te\n\nst 中的 t 和 s |
LITERAL | 开启后元字符和转义符 \ 都无特殊含义,仅表示各自字面意思 |
||
DOTALL | (?s) |
开启 dotall(即单行)模式,此时 . 匹配所有字符(包括换行符) |
(?s).* 匹配 te\r\nst 中的 te\r\nst |
UNICODE_CASE | (?u) |
||
CANON_EQ | |||
UNICODE_CHARACTER_CLASS | (?U) |
可使用 (?-flag)
来关闭上述这些模式,如 te(?i)s(?-i)t
能匹配 test 和 teSt,但不会匹配 teST。
更多内容参考 Pattern。
Python
修饰符 | 表达式 | 说明 |
---|---|---|
re.A |
(?a) |
仅匹配 ASCII 字符 |
re.I |
(?i) |
忽略大小写 |
re.L |
(?L) |
|
re.M |
(?m) |
开启多行模式 |
re.S |
(?s) |
开启 dotall 模式,此时 . 匹配所有字符(包括换行符) |
re.U |
(?u) |
匹配 Unicode 字符 |
re.X |
(?x) |
开启 verbose 模式 |
其中 i
、m
、s
、x
这四种修饰符可通过 (?-flag)
的形式来关闭模式,如 (?-i)
关闭忽略大小写,而 a
、L
和 u
由于互斥,所以不支持以(?-flag)
的形式来关闭。
更多内容参考 re#Module Contents。
Golang
const (
FoldCase Flags = 1 << [iota] // case-insensitive match
Literal // treat pattern as literal string
ClassNL // allow character classes like [^a-z] and [[:space:]] to match newline
DotNL // allow . to match newline
OneLine // treat ^ and as only matching at beginning and end of text
NonGreedy // make repetition operators default to non-greedy
PerlX // allow Perl extensions
UnicodeGroups // allow \p{Han}, \P{Han} for Unicode group and negation
WasDollar // regexp OpEndText was, not \z
Simple // regexp contains no counted repetition
MatchNL = ClassNL | DotNL
Perl = ClassNL | OneLine | PerlX | UnicodeGroups // as close to Perl as possible
POSIX Flags = 0 // POSIX syntax
)
更多内容参考 regexp。
常用正则表达式
- ip:
(\d{1,3}\.){3}\d{1,3}
- Email 地址:
\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*
使用
Java
在其他语言中,\\
表示在正则表达式中插入一个普通(字面上)的反斜杠,请不要给它任何特殊的意义。而在 Java 中,\\
表示插入一个正则表达式的反斜杠,反斜杠后面的字符具有特殊的意义。
所以在其他的语言中(如 Perl),一个 \
就具有转义的作用,而在 Java 的正则表达式中需要使用两个反斜杠才能被解析为其他语言中的转义作用。简单的理解,Java 的正则表达式使用两个 \\
代表其他语言中的一个 \
来转义字符。如果想匹配一个 \
,那么应该使用 \\\\
来匹配。
Swift
参考 Swift Regex: Beyond the basics。