正则表达式

正则表达式,又称正规表示式、正规表示法、正规表达式、规则表达式、常规表示法(英语:Regular Expression,在代码中常简写为 regex、regexp 或 RE),是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些匹配某个模式的文本。
许多程序设计语言都支持利用正则表达式进行字符串操作。例如,在 Perl 中就内建了一个功能强大的正则表达式引擎。正则表达式这个概念最初是由 Unix 中的工具软件(例如 sed 和 grep)普及开的。正则表达式通常缩写成“regex”,单数有 regexp、regex,复数有 regexps、regexes、regexen。

非教程,仅为个人笔记,方便日后查表。

预备知识

正则是什么
正则表达式不是编程语言,而是实际字符序列的抽象表示方法,比如字符串www,三个连续的w字符,因此我们可以抽象的表示为w{3},正则表达式就是这么演变而来的。

正则派别
相信大家对于正则表达式都不陌生,在文本处理中或多或少的都会使用到它。但是,我们在使用 linux 下的文本处理工具如 awk、sed 时,正则表达式的语法貌似还不一样,在 awk 中能正常工作的正则,在 sed 中总是不起作用,这是为什么呢?这个问题产生的缘由是因为正则表达式不断演变的结果,目前的主要”派别”有 3 个:

BRE、ERE 与 PCRE 并不存在明显的语法差异,从总体上讲,BRE/ERE 属于 PCRE 的子集。因此本文主要讨论 PCRE 正则,因为它是最强大、最灵活的一个正则派别,并且大多数编程语言的正则库都是 PCRE 风格的。

正则引擎
正则表达式引擎有两种:非确定型有穷自动机(NFA)确定型有穷自动机(DFA)

  • DFA:文本主导确保获得最长的匹配文本预编译复杂匹配简单快速特性较少
  • 传统型NFA:模式主导找到匹配后丢弃其它分支预编译简单匹配复杂特性多
  • POSIX NFA:与传统型NFA差别不大,最大的区别是它会尝试所有分支,确保获得最长的匹配文本,速度略慢

大多数编程语言都是使用传统型NFA引擎,如 Java、Perl、PHP、Python、.NET;而 grep、awk 则使用 DFA。

两类正则引擎要顺利工作,都必须有一个正则式和一个文本串,一个捏在手里,一个吃下去:

  • DFA 捏着文本串去比较正则式,看到一个子正则式,就把可能的匹配串全标注出来,然后再看正则式的下一个部分,根据新的匹配结果更新标注。
  • NFA 则捏着正则式去比文本,吃掉一个字符,就把它跟正则式比较,匹配就记下来:“某年某月某日在某处匹配上了!”,然后接着往下干。一旦不匹配,就把刚吃的这个字符吐出来,一个个的吐,直到回到上一次匹配的地方。

DFA 与 NFA 机制上的不同带来 5 个影响:

  1. DFA 对于文本串里的每一个字符只需扫描一次,比较快,但特性较少;NFA 要翻来覆去吃字符、吐字符,速度慢,但是特性丰富,因此应用广泛,当今主要的正则表达式引擎,如 Perl、Ruby、Python、Java 和 .NET 的 regex 库,都是 NFA 的。
  2. 只有 NFA 才支持 lazy(懒惰量词)和 backreference(反向引用)等特性。
  3. NFA 急于邀功请赏,所以最左子正则式优先匹配成功,因此偶尔会错过最佳匹配结果;DFA 则是最长的左子正则式优先匹配成功。
  4. NFA 缺省采用 greedy(贪婪量词),使用相应的修饰符可将贪婪量词转换为懒惰量词、占有量词。
  5. NFA 可能会陷入递归调用的陷阱而表现得性能极差。

输入序列的组成
对于字符串"abc"来说,除了存在三个字符外,它还存在四个位置,即"0a1b2c3";这四个位置分别为:a 的左边、a 的右边、b 的右边、c 的右边,也就是每个字符的边界位置。

不过我们通常都会使用一个数字来表示一个位置,它们的表示规则为"0a1b2c3"。位置总是从 0 开始,最大值为字符串的长度,而每个位置的数值其实就是它后面的字符的索引值。如位置 0 就是字符 a 的索引值,位置 2 就是字符 c 的索引值。

正则模式的组成
对于模式www\d+.*+来说,存在 5 个子表达式(模式的最小单位称为子表达式),它们分别为:www\d+.*+一个子表达式是不可再分的最小单位

占有字符和零宽度
如果一个子表达式匹配的是字符,并且所匹配的字符会保存到最终结果中,那么该子表达式就是占有字符的。
如果一个子表达式匹配的是位置,或者匹配的字符不会保存到最终结果中,那么该子表达式就是零宽度的。

占有字符的表达式称为占宽表达式零宽度的表达式称为零宽表达式占宽表达式是互斥的,零宽表达式是非互斥的。也就是说:同一个字符在同一时间只能被一个占宽表达式匹配;而同一个位置在同一时间却能被多个零宽表达式匹配。

在 Java 的正则中,只有边界匹配符顺序环视逆序环视是零宽表达式,其它的都是非零宽(占宽)表达式。

匹配过程

控制权
如果正则引擎当前所执行的子表达式为 A,那么我们说子表达式 A 取得控制权。A 执行完后,它会将控制权转交给下一个子表达式 B,子表达式 B 则从 A 匹配成功的结束位置开始进行匹配,以此类推。

对于正则模式ABCDEF来说,控制权总是先交给 A,然后再交给 B,最后交给 F。即按照从左到右的顺序依次传递控制权(逆序环视中的子表达式例外,它是从右到左的)。

匹配细节
除了逆序环视外,其它的所有子表达式都是匹配当前位置右侧的字符序列。比如,字符序列"ABCDEFG",假设子表达式 A 从位置 3 开始匹配(即字符 D 前面),它只会从位置 3 往后看,看看能不能匹配后面的 DEFG 字符序列。
而对于逆序环视子表达式 B 来说,假设它从位置 3 开始匹配,它只会从位置 3 往前看,看看能不能匹配前面的 ABC 字符序列。

分支与回溯
对于模式(?:g|f)ood来说,它存在两条分支(或者叫做执行路径),一条是good,另一条是food。而正则引擎总是按照从左到右的顺序选择分支的,因此good分支首先被执行,只有当该分支在某一位置无法继续匹配时才会进入下一个分支food。这个进入下一个分支的行为叫做回溯(backtrack),也就是说,当前这条路已经走不通了,只能尝试下一条路了。

分支是可以有多个的,比如模式(?:a|b)X(?:c|d)Y,第一个岔路口a|b,第二个岔路口c|d,因此它有四条分支:aXcYaXdYbXcYbXdY。正则引擎会先选择 aXcY 分支,如果该分支可以将整个正则模式匹配完毕(即整个模式匹配成功),那么其它的所有分支将被丢弃,开始进入下一轮匹配(如果启用了 global 标志位)或者结束匹配并报告该轮匹配成功;如果该分支走到某个地方匹配失败了,则进行回溯,开始进入离它最近的下一个分支,即 aXdY 分支。以此类推。

量词与回溯
对于模式\d{1,3}www来说,它存在三条分支,即\d\d\dwww\d\dwww\dwww。为什么是倒着排呢?因为量词默认都是贪婪的,它总是会先尝试匹配 max 次(在这里就是 3 次咯),而如果给量词加上修饰符 ? 即\d{1,3}?,则三条分支依次为:\dwww\d\dwww\d\d\dwww,加上 ? 后贪婪量词就会变成懒惰量词,而懒惰量词总是先尝试匹配 min 次(在这里就是 1 次咯)。具体的回溯过程就不再复述了,在上面的”分支与回溯”中已进行讲解。

向前传动
如果当前选择的分支匹配失败了,则正则引擎会进行回溯,进入下一条分支再次尝试,但是如果当前的所有分支都匹配失败了或者当前根本就没有任何分支可用的情况下会怎么样呢?答案是进行向前传动。

我们将上面的这种无分支可用或全部分支都匹配失败的情况称为本轮匹配失败,那么就要进入”下一轮匹配”。那么要怎么进入下一轮呢?

别急,首先,正则引擎第一轮匹配总是从输入序列的位置 0 开始的,因此,进入第二轮就是说从输入序列的位置 1 开始重新匹配整个模式,而进入第三轮就是从输入序列的位置 2 开始重新匹配整个模式,以此类推,直到输入序列的最后一个位置 n,如果到了最后一轮还是失败,那么正则引擎将报告模式匹配失败。这个进入下一轮的过程就称为向前传动,简称传动。也就是说,将输入序列往前挪动一个字符长度,最前面的一个字符将会被丢弃,后续匹配不会再考虑被丢弃的字符序列。

最后再说一下 global 匹配的过程,默认情况下,正则引擎匹配成功了一次之后,就会返回,不会再管剩下的字符序列;但如果启用了 global 全局匹配标志,则匹配成功一次后,不会立即返回,而是接着从本次匹配成功的结束位置开始重新匹配整个模式,即:将本次匹配成功的结束位置前的字符都丢弃掉(向前传动),然后重新开始匹配整个模式,直到整个字符序列都被传动完毕(消耗完毕,导致没有字符可匹配)为止,引擎才会返回。

正则匹配过程总结
每开始新的一轮匹配,控制权都是先交给正则模式中的第一个子表达式,当遇到分支结构时,如X|Y,正则引擎会记录下 Y 分支,并首先进入 X 分支,如果在 X 分支中走不动了,就会进行回溯,即回到X|Y处,选择 Y 分支继续尝试匹配,如果这条路走通了,则正则引擎报告第一次匹配成功并返回(如果启用了全局标志,则还会继续搜索);如果这条路依旧失败了,则进行向前传动,即丢弃当前字符序列的头一个字符,重新开始匹配整个正则模式,以此类推,直到字符序列全部被消耗完毕。

字符

宽度为 1

表达式 解释说明
x 匹配字符x
\\ 匹配字符\
\0n 匹配八进制值为0n的字符 (0 <= n <= 7)
\0nn 匹配八进制值为0nn的字符 (0 <= n <= 7)
\0mnn 匹配八进制值为0mnn的字符 (0 <= m <= 3, 0 <= n <= 7)
\xhh 匹配十六进制值为0xhh的字符
\uhhhh 匹配十六进制值为0xhhhh的字符
\x{h...h} 匹配十六进制值为0xh...h的字符 (0x0000 <= 0xh…h <= 0x10FFFF)
\t 匹配水平制表符 HT ('\u0009')
\v 匹配垂直制表符 VT ('\u000B')
\r 匹配回车符 CR ('\u000D')
\n 匹配换行符 LF ('\u000A')
\f 匹配换页符 FF ('\u000C')
\a 匹配警铃符 BEL ('\u0007')
\e 匹配 ESC 符 ('\u001B')
\cx 匹配控制字符 x,如 \cM 匹配 Control-M

自定义字符集合

宽度为 1

表达式 解释说明
[abc] 匹配集合中的单个字符 (枚举)
[a-z] 匹配集合中的单个字符 (范围)
[^abc] [逻辑非] 匹配不在集合中的单个字符 (枚举)
[^a-z] [逻辑非] 匹配不在集合中的单个字符 (范围)
[a-z[A-Z]] [逻辑或] 表达式 a-z 和表达式 A-Z 只要有一个匹配则整个表达式匹配
[a-zA-Z] [逻辑或] 可以省略嵌套中括号,但如果后一个表达式为否定则不能省略
[a-z[^0-9]] [逻辑或] 表达式 a-z 和表达式 ^0-9 只要有一个匹配则整个表达式匹配
[a-z&&[def]] [逻辑与] 表达式 a-z 和表达式 def 只有两个都匹配时整个表达式才匹配
[a-z&&[^bc]] [逻辑与] 表达式 a-z 和表达式 ^bc 只有两个都匹配时整个表达式才匹配
[a-z&&[^m-p]] [逻辑与] 表达式 a-z 和表达式 ^m-p 只有两个都匹配时整个表达式才匹配

[a-z[^0-9]][a-z&&[def]]等语法只有 Java 支持,大家可以忽略。

自定义字符集合的[]方括号中的特殊字符有 6 个:

  1. [:表示一个字符集合的开始,如果需要匹配 [ 本身,请使用\[
  2. ]:表示一个字符集合的结束,如果需要匹配 ] 本身,请使用\]
  3. ^:只有位于字符集合的起始位置才有特殊意义,表示取反操作、排除语义
  4. -:只有位于字符集合的非边界位置才有特殊意义,表示一个字符范围(从小到大)
  5. &:用于连接前后两个字符集合(逻辑与),并且必须是两个连续的 & 才有特殊意义
  6. \:表示一个转义序列的开始,本身不匹配任何字符,如果要匹配 \ 本身,请使用\\

除了这六个字符外,其它字符都是普通字符,比如 .?*+$|(){} 等都属于普通字面意义字符。

预定义字符集合

宽度为 1

表达式 解释说明
. 匹配除行结束符外的任意字符(单行模式匹配任意字符)
\d 匹配数字[0-9]
\D 匹配非数字[^0-9]
\w 匹配单词字符[a-zA-Z_0-9]
\W 匹配非单词字符[^\w]
\s 匹配空白符[ \t\n\x0B\f\r]
\S 匹配非空白符[^\s]
\h 匹配水平空白符[ \t\xA0\u1680\u180e\u2000-\u200a\u202f\u205f\u3000]
\H 匹配非水平空白符[^\h]
\v 匹配垂直空白符[\n\x0B\f\r\x85\u2028\u2029]
\V 匹配非垂直空白符[^\v]

POSIX 字符集合

宽度为 1、ASCII only

表达式 解释说明
\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{Punct} 匹配标点符号!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
\p{Graph} 匹配可见字符[\p{Alnum}\p{Punct}]
\p{Print} 匹配可打印字符[\p{Graph}\x20]
\p{Cntrl} 匹配控制字符[\x00-\x1F\x7F]
\p{Blank} 匹配空格符[ \t]
\p{Space} 匹配空白符[ \t\n\x0B\f\r]
\p{XDigit} 匹配十六进制数字[0-9a-fA-F]
\p{ASCII} 匹配所有 ASCII 字符[\x00-\x7F]

Java 字符集合

宽度为 1

表达式 解释说明
\p{javaLowerCase} 等价于 Character.isLowerCase()
\p{javaUpperCase} 等价于 Character.isUpperCase()
\p{javaWhitespace} 等价于 Character.isWhitespace()
\p{javaMirrored} 等价于 Character.isMirrored() 镜像字符,如 () [] {}

Unicode 字符集合

宽度为 1,Unicode 脚本/块/分类/二进制属性

表达式 解释说明
\p{IsLatin} 拉丁字符 (script)
\p{InGreek} 希腊字符 (block)
\p{Lu} 大写字母 (category)
\p{IsAlphabetic} 字母字符 (binary property)
\p{Sc} 货币符号
\P{InGreek} 除希腊字符外的任意字符 (negation)
[\p{L}&&[^\p{Lu}]] 除大写字母外的任意字母 (subtraction)
\R (宽度为一或二)任何 Unicode 行结束序列,如\r\n\n\f,它等同于\u000D\u000A|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029]

边界匹配符

宽度为 0,零宽断言,匹配位置而非字符

表达式 解释说明
^ 匹配输入序列的起始位置(默认模式),如果为多行模式,同时还会匹配行结束符之后的位置
$ 匹配输入序列的结束位置(默认模式),如果为多行模式,同时还会匹配行结束符之前的位置
\b 匹配单词的边界
\B 匹配非单词边界
\A 匹配输入序列的起始位置
\G 匹配前一匹配处的结束位置
\z 匹配输入序列的结束位置
\Z 匹配输入序列的结束位置,在 Java 中同\z,在 Python 中,它同时还会匹配行结束符之前的位置

\b单词边界
很多人都知道\b是匹配单词边界,但是关于”单词”的范围,却很少提及。正则中所指的单词就是\w定义的字符组成的序列。
\w是一个预定义字符集合,它等价于[a-zA-Z0-9_],即字母数字下划线组成的字符序列被称为单词。如果启用了 Unicode 字符支持,则\w还会匹配 Unicode 中定义的单词字符,如汉字、全角数字。

\b是一个零宽子表达式,它只匹配位置,因此单词边界就是一侧是\w而另一侧不是\w的这样一个位置,因此\b等价于(?<=\w)(?!\w)|(?<!\w)(?=\w)
\B则表示非单词边界,所谓的非单词边界就是指一侧是\w且另一侧也是\w的这样一个位置,因此\B等价于(?<=\w)(?=\w)

最后还有一点要说明,位于字符集合中的\b(如[0-9\b])不是表示单词边界,而是代表退格键

贪婪量词

greedy、贪婪量词会尽量将所修饰的子表达式应用 max 次、贪婪量词会进行回溯、量词默认都是贪婪的

表达式 解释说明
X? 匹配模式 X 0~1 次(含 0、1)
X* 匹配模式 X 0+ 次(含 0)
X+ 匹配模式 X 1+ 次(含 1)
X{n} 匹配模式 X n 次
X{n,} 匹配模式 X n+ 次(含 n)
X{n,m} 匹配模式 X n~m 次(含 n、m)

懒惰量词

reluctant、懒惰量词会尽量将所修饰的子表达式应用 min 次、懒惰量词会进行回溯

表达式 解释说明
X?? 匹配模式 X 0~1 次(含 0、1)
X*? 匹配模式 X 0+ 次(含 0)
X+? 匹配模式 X 1+ 次(含 1)
X{n}? 匹配模式 X n 次
X{n,}? 匹配模式 X n+ 次(含 n)
X{n,m}? 匹配模式 X n~m 次(含 n、m)

占有量词

possessive、占有量词会尽量将所修饰的子表达式应用 max 次、占有量词不进行回溯

表达式 解释说明
X?+ 匹配模式 X 0~1 次(含 0、1)
X*+ 匹配模式 X 0+ 次(含 0)
X++ 匹配模式 X 1+ 次(含 1)
X{n}+ 匹配模式 X n 次
X{n,}+ 匹配模式 X n+ 次(含 n)
X{n,m}+ 匹配模式 X n~m 次(含 n、m)

逻辑连接符

表达式 解释说明
XY [逻辑与] 最普通的一种形式
X|Y [逻辑或],比如g|food匹配gfood(g|f)ood匹配goodfood,不过为了不被当作捕获组,我一般都会使用(?:g|f)ood来消除这一副作用

捕获组

表达式 解释说明
(X) 将 X 匹配的序列存储起来,作为一个捕获组,后续可引用该捕获组的内容
(?<name>X) 定义命名捕获组,必须以字母开头,后面可接数字和字母。即使是命名捕获组,我们依旧可以根据组号 N 来引用它。给它定义名字只不过是为了方便引用而已

非捕获组

表达式 解释说明
(?:X) 逻辑上的括号,一般用于限定一个范围,或者将表达式 X 作为一个整体
(?>X) 原子组,原子组中的子表达式的任何回溯点都会被丢弃(即不保存任何分支)

非捕获组
比如(?:\d{3})+表示长度为 3N (N >= 1) 的连续数字序列,而\d{3}+的意义却完全不同了,我们来解析一下,这里存在三个元素\d{3}+,元素二和元素三都属于量词,但是在正则模式中,如果存在两个连续的量词,则会将 “贪婪量词” -> “懒惰量词”/“占有量词”,而两个以上的连续量词是不允许的,会导致语法错误!这一点要十分清楚,刚开始时我也稀里糊涂的:

  • .*:为 “贪婪属性”
  • .*?:为 “懒惰属性”
  • .*+:为 “占有属性”
  • .*??:非法,语法错误
  • .*++:非法,语法错误

原子组
原子组中的子表达式的任何回溯点都会被丢弃(即不保存任何分支),因此位于原子组中的贪婪量词懒惰量词分支语句都不会被执行回溯操作:

  • 贪婪量词:吃完了当前能够吃的所有字符后,它将不会再吐出来了;
  • 懒惰量词:吃完了 min 次能够吃掉的字符后,它将不会再吃进去了;
  • 分支语句:对于模式 (?>X|Y|Z) 只会执行 X 分支,其它分支被丢弃。

反向引用

替换模式中使用

表达式 解释说明
\n 引用捕获组 n (0 <= n <= 9),不过在 Java 中,不使用 \N 来引用,而是使用 $N 来引用,并且,N 的第一位数字总是会被解释为第 N 组,而不管模式中有没有这个组,当然如果模式中的捕获组有 10+ 以上,那么 $10、$11 之类的引用也是合法的,可以被 Java 正则引擎解释
\k<name> 引用捕获组 name,即引用具名捕获组,当然,即使是有名字的捕获组,我们也是可以根据组号来引用它,给它命名只不过为了好记而已

字面引用

宽度为 0

表达式 解释说明
\ 用于转义随后的字符,它本身不匹配任何内容,除非使用\\
\Q 用于表示一个普通字符序列的开始,它本身不匹配任何内容
\E 用于表示一个普通字符序列的结束,它本身不匹配任何内容

环视/预查

宽度为 0、零宽断言逆序环视中不允许存在不定长量词

表达式 解释说明
(?=X) 顺序肯定环视,表示所在位置的右侧能够匹配子表达式 X
(?!X) 顺序否定环视,表示所在位置的右侧不能匹配子表达式 X
(?<=X) 逆序肯定环视,表示所在位置的左侧能够匹配子表达式 X
(?<!X) 逆序否定环视,表示所在位置的左侧不能匹配子表达式 X

正则 flags

表达式 解释说明
(?idmsuxU-idmsuxU) 不匹配任何字符,而是打开或关闭给定的正则模式标志位,其作用范围是从该模式位置到模式结束位置,可以理解为全局。-前面的表示要打开的标志位,-后面的表示要关闭的标志位
(?idmsux-idmsux:X) 同上,但是作用范围仅限于圆括号内部,即仅针对表达式 X 设置

其它说明

环视常见用法

  1. 顺序肯定常规[a-z]+(?=;):字母序列后面跟着;
  2. 顺序肯定变种(?=[a-z]+$).+$:字母序列
  3. 顺序否定常规[a-z]+\b(?!;):不以;结尾的字母序列
  4. 顺序否定变种(?!.*?[lo0])\b[a-z0-9]+\b:不包含l/o/0的字母数字系列
  5. 逆序肯定常规(?<=:)[0-9]+:后面的数字序列
  6. 逆序肯定变种\b[0-9]\b(?<=[13579]):0~9 中的奇数
  7. 逆序否定常规(?<!age)=([0-9]+):参数名不为 age 的数据
  8. 逆序否定变种\b[a-z]+(?<!z)\b:不以 z 结尾的单词

正则测试工具

正则测试工具[在线工具] -> 正则表达式测试工具在线调试与分享 - Zjmainstay

使用图解 - 图片
正则在线测试

使用图解 - gif
正则在线测试

个人截图 - 千分位格式化
正则在线测试

个人截图 - 千分位格式化(正则匹配过程)
正则在线测试

正则测试工具[Windows版] -> Learn, Create, Understand, Test, Use and Save Regular Expressions with RegexBuddy

主界面详细介绍
主界面详细介绍

如何使用匹配功能
如何使用匹配功能

如何使用替换功能
如何使用替换功能

如何进行 debug 调试
如何进行 debug 调试

正则练习题

正则掌握程度测试题(Zjmainstay 学习笔记) -> 正则练习题