字符

元字符:$()*+.?[\^{|。注意这个列表中并不包含右括号 ]} 和连字符-。前两个符号只有在它们位于一个没有转移的 [{ 之后才成为元字符,在任何时候都没有必要} 进行转义。

\Q 会抑制所有元字符的含义,直到 \E,如 \Q!"#$%^&*()-+\E

模式修饰符 (?i) 设置后面部分不区分大小写,直到 (?-i)(不是所有语言都支持,如 JS 是 /.../i)。这样的模式切换不会影响到捕获组。也可以使用 abc(?ism:abc|def)ABC(?i-sm)这样的格式。

单行模式下,. 匹配换行符外的任意字符,[\s\S] 匹配包含换行符在内的任意字符,类似的 [\w\W] 也有同样效果。部分语言使用 (?s) 开启点号匹配换行符。

^ $ \A \Z 匹配一行的首尾,前两者在开启“^和$匹配换行处(多行模式)”后可以匹配换行,一般默认关闭,Ruby强制开启,JS 不支持 \Z \z$\Z 可以匹配最后一个换行符之前的位置(即可忽略最后一个换行符),\z 则只匹配目标文本最末尾处。

\b 只在单词字符边界有效,如 x\bx!\b! 永远不会匹配任何位置。如果换行符紧跟在一个单词字符后面,那么\b会匹配换行符后面的位置,它同样也会匹配单词字符之前的换行符,这样一个占据了一整行的单词也可以通过一个“只匹配完整单词”的查找来发现。

还没参与匹配的分组,并不等同于捕获到长度为 0 的分组,如 \1(\d) 永远不会成功(JS除外),但 (^)\1 则总是成功。

流派相关特性

.NET 字符差:[a-zA-Z0-9-[g-zG-Z]] 表示一个不是 g~z 的字符或数字,采用嵌套类的方式,嵌套类必须出现在基类的最后,紧跟在一个连字符后面:[class-[subclass]][\p{IsThai}-[\P{N}]] 匹配任意的10个泰语数字字符。

Java 并集(union):[a-f[A-F][0-9]],交集(intersection):[\w&&[a-fA-F0-9\s]] 相当于 [a-zA-Z0-9&&[^g-zG-Z]],匹配十六进制数字。如果 subclass 是否定类,则是作差,即 [class&&[^substract]],如[\p{InThai}&&[\P{N}]] 匹配任意的10个泰语数字字符。

元字符 含义
\p{L} 所有字母
\p{N} 所有数字,类似于 \d
[\p{N}\p{L}] 所有数字和所有字母,类似于 \w
\P{L} 不是字母,等价于 [^\p{L}]
\P{N} 不是数字,等价于 [^\p{N}]

分组

使用 \1\2 来捕获 () 包含的分组

命名捕获:(?P<name>regex)(Python)、(?<name>regex)(?'name'regex)(.NET)

命名反向应用:(?P=name)\k<name>\k'name'

条件判断

使用 (?(1)then|else)(?(name)then|else)(命名仅 .NET 支持) 来判断指定分组有没有产生匹配(匹配空字符串也算产生匹配),其中 then 或 else 可为空(相当于长度为 0 的匹配)。如果里面想要使用多选结构,需要用分组包起来,条件判断中只允许直接出现一个竖线。

Java 和 Ruby 不支持条件判断

示例:匹配字符串中右逗号分隔的 one two three 三个单词,且每个单词至少出现一次:

1
\b(?:(?:(one)|(two)|(three))(?:,|\b)){3,}(?(1)|(?!))(?(2)|(?!))(?(3)|(?!)))

一个空的否定型顺序环视 (?!) 被用在了 else 部分,因为空的正则表达式总是会产生匹配,所有包含空正则表达式的否定型顺序环视则总是会匹配失败。因此,当第一个捕获分组没有匹配到任何东西的时候,条件判断 (?(1)|(?!)) 总是会失败。

示例:(a)?b(?(1)c|d) 等价于 abc|bd。如果是 (a?),则总是会匹配成功。

在 .NET、PCRE、Perl 中,条件判断还可以使用环视:(?(?=if)then|else)。不推荐使用否定型环视,因为它会把 then 和 else 的人含义反转。

懒惰模式

懒惰模式并不会影响匹配的成功和失败。

贪心量词?\d+?\w\b 能匹配到 1234X,结果是 1234X 而不是 4X

占有量词:永远不会回退,*+++?+{7,42}+

\w++\d++(?>\w+)(?>\d+) 一样,和 (?>\w+\d+) 不同,后者有两个贪心量词,回溯的位置只有当引擎退出整个分组的时候才会被丢弃

明确消除不必要的回溯

\b\d+\b\b\d+?\b\b\d++\b\b(?>\d+)\b 都会匹配一个整数。在匹配“123abc 456” 时,前两者在 123a 处匹配失败,会没必要地回溯尝试 23a3a 并依旧失败;而后两者占有量词会把这些回溯的位置丢弃,往后去匹配 456

匹配HTML标签

使用一个正则表达式来匹配一个完整的 HTML 文件,并检查其中的几个标签是否进行了正确的嵌套。(打开多行模式和忽略大小写)

1
<html>.*?<head>.*?<title>.*?</title>.*?</head>.*?<body[^>]*>.*?</body>.*?</html>

当一个正确的 HTML 文件上测试的时候,它会完全正常地运行。最坏的情况是当 </html> 缺失的时候,最后一个 .*? 以及另外 6 个 .*? 都记住了一个回溯位置,逐步匹配并逐渐扩展到文件结尾,直到无法再进行扩展,此时复杂度是 O(n^7^),这种情形称作灾难性回溯,会毁掉程序的性能。

优化后的版本:

1
<html>(?>.*?<head>)(?>.*?<title>)(?>.*?</title>)(?>.*?</head>)(?>.*?<body[^>]*>)(?>.*?</body>).*?</html>

环视

  • 逆序环视(前):(?<=)(?<!)不支持无限长度量词(*、+、{})

    一些引擎(JS、Ruby1.8)不支持逆序环视,且效率一般,建议使用捕获组或者匹配两次代替

  • 顺序环视(后):(?=)(?!)

慎用环视

  • (?=(\d+))\w+\1 无法匹配 123x12

    (?=\d+) 会首先匹配 123,存到第一个捕获分组中,并丢弃由贪心的加号所记住的回溯位置。\w+ 会贪心地匹配 123x12,发现失败于是挨个回退,直到最后第一个 1 并同样失败。

    如果正则引擎能返回到顺序环视中,放弃 123 而选择 12,那么则可以匹配,但它并不会这么做。\w+ 已经回退到头了,但 \d+ 把它的回溯位置丢掉了,因此匹配宣告失败。

  • (?<=(\d+))\w+\1 匹配 123x12 结果:(23x1, 1) 而不是 (3x12, 12)

    可能因为环视里面的 + * 不是贪婪的,表达式末尾加上 $ 则可以匹配全部

注释

宽松排列模式

1
2
3
4
5
(\d{4})   #year
-
(\d{2}) #month
-
(\d{2}) #day
1
(?#Year)\d{4}-(?#Month)\d{2}-(?#Day)\d{2}

此模式会忽略所有空白符和 #,若要使用空格得用以下方法替换:

  • 字符类 [ ][#]
  • 转义 \\#
  • \x20
  • \u0020
  • \x{0020}

Java / JavaScript中会忽略所有空白符,因此 [ ] 方法无效。