正则表达式入门

正则表达式学习笔记

正则表达式入门
匹配原理
实用技巧
打造高效正则表达式

正则表达式入门

行的起始和结束

^ 行的开头
$ 行的末尾

例如 ^cat 匹配的是以c作为一行的第一个字符,紧接一个a,紧接一个t的文本。

1
2
3
4
5
6
 ^cat$    文字意义:匹配的条件是,行开头(显然,每一行都有开头),然后是字母cat,然后是行末尾。
          应用意义:只包含cat的行——没有多余的单词、空白字符……只有‘cat’。
 ^$       文字意义:匹配的条件是,行开头,然后就是行末尾。
          应用意义:空行(没有任何字符,包括空白字符)。
 ^        文字意义:匹配条件是行开头。
          应用意义:无意义!因为每一行都有开头,所以每一行都能匹配——空行也不例外。

字符组

gr[ea]y 的意思是:先找到g,跟着是一个r,然后是一个a或者e,最后是一个y。

字符组元字符 ‘-’(连字符)表示一个范围

1
[0-9]和[a-z] 是常用的匹配数字和小写字母的简便方式。多重范围也是容许的,例如[0123456789abcdefABCDEF]可以写作[0-9a-fA-F](或者也可以写作[A-Fa-f0-9],顺序无所谓)。这三个正则表达式非常适用于处理十六进制数字。

我们还可以随心所欲地把字符范围和普通文本结合起来:[0-9A-Z_!.?] 能够匹配一个数字、大写字母、下划线,惊叹号、点号或者是问号。

排除型字符组

用[^……]取代[……],这个字符组就会匹配任何未列出的字符。

例如,[\^1-6]匹配除了1到6以外的任何字符,这里的^表示“排除”。

在字符组外部,^表示一个行锚点,但是在字符组内部(而且必须是紧接在字符组的第一个方括号之后),它就是一个元字符。

例子:

1
2
3
4
5
6
7
在一堆单词中找到字母q后面的字母不是u的单词,用正则表示就是 q[^u]。

但是这里得不到Iraq和Qantas。

Qantas无法匹配的原因是,正则表达式要求小写q,而Qantas中的Q是大写的。如果我们改用Q[^u],就能匹配到Qantas,但是其他单词又不在结果中了,因为他们不包括大写Q。[Qq][^u]则能找到上面所有单词。

Iraq的例子有点迷惑人。正则表达式要求q之后紧跟一个u以外的字符,这就排除了q处在行尾的情况。通常说,文本行的结尾都有一个换行字符,但是egrep会在检查正则表达式之前把这些换行符去掉,所以在行尾的q之后,没有能够匹配u以外的字符。

排除型字符组表示“匹配一个未列出的字符”,而不是“不要匹配列出的字符”。

如果egrep保留换行符(许多其他软件会保留这些符号),或者Iraq后紧接着空格或者其他单词,这一行就能匹配。理解工具软件的细节虽然重要,但更重要的是认识到:一个字符组,及时是排除型字符组,也需要匹配一个字符。

用点号匹配任意字符

例如我们需要搜索03/19/76、03-19-76或者03.19.76,不怕麻烦的话用一个明确容许‘/’、‘-’、‘.’的字符组来构建正则表达式,例如03[-./]19[-./]76 也可以简单的尝试03.19.76.

注:如果连字符在字符组中不是在开头位置,如[.-/]就表示范围了,在本例中就是错误的用法了。

在03.19.76中,点号是元字符——它能够匹配任意字符(包括我们期望的连字符、句号和斜线)。不过,点号可以匹配任何字符,所以这个正则表达式也能够匹配下面的字符串:‘lottery numbers:19 203319 7639’(03319 76)

匹配任意子表达式

| 元字符,意思是“或(or)”。能够把不同的子表达式组合成一个总的表达式,而这个总的表达式又能够匹配任意的子表达式。子表达式成为“多选分支”。

gr[ea]y 的例子可以写作grey|gray,或者是gr(a|e)y。后者用括号来划定多选结构的范围(正常情况下,括号也是元字符)。请注意,gr[a|e]y 不符合我们的要求——在这里,‘|’只是一个和‘a’与‘e’一样的普通字符。

对表达式gr(a|e)y和gr[ea]y的例子可能会觉得多选结构和字符组没太大区别,但是不要混淆两个概念。一个字符组只能匹配目标文本中的单个字符,而每个多选结构自身可能是完整的正则表达式,都可以匹配任意长度的文本。

在一个包含多选结构的表达式中使用脱字符和美元符的时候要小心。

比较^From|Subject|Date:· 和 ^(From|Subject|Date):· 就会发现,匹配结果大不相同。前者由三个多选分支构成,所以它能匹配 ^From 或者Subject 或者 Date:· ,实用性不大。我们希望在每一个多选分支之前都有脱字符,之后又 :* 。所以应该用括号来限制这些多选分支:

1
^(From|Subject|Date):*

现在3个多选分支都受括号限制,所以这个正则表达式的意思是:匹配一行的起始位置,然后匹配 ^From 、 Subject 或者 Date中的任意一个,然后匹配 :· ,所以,它能够匹配的文本是:

  1. 行起始,然后是From,然后是 :·,
  2. 行起始,然后是Subject,然后是 :·,
  3. 行起始,然后是Date,然后是 :·。

简单说,就是匹配以 From:·、Subject:·或者Date:· 开头的文本行,在提取E-mail文件中的信息是这很有用。

忽略大小写

该功能不是正则表达式语言的一部分,但是许多工具软件提供了相关特性:在正则表达式之前写 -i

单词分界符

如果期望匹配的单词包含在另一个单词中,如果egrep支持“元字符序列 (< 和 >)”,就可以使用它们匹配单词分界的位置。

表达式 \<cat\>的意思是“匹配单词的开头位置,然后是cat三个字母,然后是单词的结束位置”。更直接点说就是“匹配cat这个单词”。

\<cat 和 cat\> 匹配以cat开头和结束的单词。

< 和 > 本身并不是元字符,只有当它们与斜线结合起来的时候,整个序列才具有特殊意义。

可选项元素

元字符 ? 代表可选项。把它加在一个字符的后面,就表示此处容许出现这个字符,不过他的出现并非匹配成功的必要条件。

如果需要匹配7月4日(July fourth)的文本,其中月份可能写作July或者Jul,而日期可能写作fourth、4th或者是4.显然,我们可以用

1
2
3
(July|Jul)*(fourth|4th|4);

July?·(fourth|4th|4)

可以继续简化为

1
July?(fourth|4(th)?)
其他量词

+(加号)和*(星号)的作用与问号类似。元字符 + 表示之前紧邻的元素出现一次货多次 * 表示之前紧邻的元素出现任意多次,或者不出现。

换种说法是

……* 表示 匹配尽可能多的次数,如果实在无法匹配,也不要紧。 ……+ 表示 匹配尽可能多的次数,但如果连一次匹配都无法完成,就报告失败。

与 ……? 一样,正则表达式中的 ……* 也是永远不会匹配失败的,区别只在于它们的匹配结果。而 ……+ 在无法进行任何一次匹配时,会报告匹配失败。

举例来说, ·? 能够匹配一个可能出现的空格,但是 ·* 能够匹配任意多个空格。

符号 次数下限 次数上限 含义
? 1 可以不出现,也可以只出现一次(单次可选)
* 可以出现无数次,也可以不出现(任意次数均可)
+ 1 可以出现无数次,但至少要出现一次(至少一次)
转义符号

如果需要匹配的某个字符本身就是元字符,例如检索互联网的主机名 eag.att.com ,使用 ega.att.com 可能得带 megawatt·computing的结果,.本身就是元字符,他可以匹配任何字符,包括空格。

真正匹配文本中点号的元序列应该是反斜线加上点号的组合: ega.att.com。 反斜线加元字符 即转义 不过在字符组内部无效

引号内的字符串

匹配引号内的字符串最简单的办法是使用这个表达式: “[^”]*” 。

两端的引号用来匹配字符串开头和结尾的引号。在这两个引号之间的文本可以包括双引号之外的任何字符。所以我们用 [^”] 来匹配除双引号之外的任何字符,用 * 来表示两个双引号之间可以存在任意数目的非双引号字符。

egrep的元字符总结
匹配单个字符的元字符
元字符 元字符名称 匹配对象
. 点号 匹配单个任意字符
[……] 字符组 匹配单个列出的字符
[^……] 排除型字符组 匹配单个未列出的字符
\char 转义字符 若char是元字符,或转义序列无特殊含义时,匹配char对应的普通字符
提供计数功能的元字符
元字符 元字符名称 匹配对象
? 问号 容许匹配一次,但非必须
* 星号 可以匹配任意多次,也可能不匹配
+ 加号 至少需要匹配一次,至多可能任意多次
[min,max] 区间量词 至少需要min次,至多容许max次
匹配位置的元字符
元字符 元字符名称 匹配对象
^ 脱字符 匹配一行的开头位置
$ 美元符 匹配一行的结束位置
< 单词分界符 匹配单词的开始位置
> 单词分界符 匹配单词的结束位置
其他元字符

元字符 | 元字符名称 | 匹配对象

  • | alternation 匹配任意分隔的表达式
    (……) 括号 限定多选结构的范围,标注量词作用的元素,为反向引用“捕获”文本
    \1,\2,.. 反向引用 匹配之前的第一、第二组括号内的字表达式匹配的文本

@并非所有版本的egrep都支持

讨论空白的问题时,我们最后使用的是 [\t]* 这样做没问题,但许多流派的正则表达式提供了一种简便的办法: \s 。 \s 看来来类似 \t , \t 代表制表符,而 \s 能表示所有表示“空白字符”的字符组,其中包括空格符、制表符、换行符和回车符。

在Perl中, $variable =~ m/regex/ 来判断一个正则表达式是否能匹配某个字符串。m 表示“匹配(match)”,而斜线用来标注正则表达式的边界(它们本身不属于正则表达式)。整个测试语句作为一个单元,返回 true 或者 false 值。

Perl 和其他流派的正则表达式提供了许多有用的简记法(shorthands)

简记 符号
\t 制表符
\n 换行符
\r 回车符
\s 任何“空白”字符(例如空格符、制表符、换行符)
\S 除 \s 之外的任何符号
\w [a-zA-Z0-9] (在 \w 中很有用,可以用来匹配一个单词)
\W 除 \w 之外的任何字符,也就是 [^a-zA-Z0-9]
\d [0-9],即数字
\D 除 \d 之外的任何字符,即 [^0-9]
/g 表示全局替换,在第一次替换完成后继续搜索更多的文本。

匹配原理

规则1:优先选择最左端的匹配结果

起始位置最靠左的匹配结果总是优先于其他可能的匹配结果。

这条规则并没有规定优先的匹配结果的长度,而只是规定,在所有可能的匹配结果中,优先选择开始位置最左端的。实际上,因为可能有多个匹配结果的起始位置都在最左端,也许我们应该把这条规则中的“某个匹配结果(a match)”改为“该匹配结果(the match)”

所以如果用 ORA 来匹配 FLORAL,从字符串左边开始第一轮尝试会失败(因为 ORA 不能匹配 FLO),第二轮尝试也会失败( ORA 同样不能匹配 LOR),从第三个字符开始的尝试能够成功,所以引擎会停下来,报告匹配结果 FLORAL。

如果不了解这条规则,有时候就不能理解匹配的结果。例如用 cat 来匹配:

1
The dragging belly indicates that your cat is too fat.

结果是 indicates,而不是后来出现的 cat。单词 cat 是能够被匹配的,但 indicates 中的 cat 出现的更早,所以得到的匹配是它。

规则2:标准量词是匹配优先的

标准匹配量词(?、*、+,以及{min,max})都是“匹配优先”的。

如果用这些量词来约束某个表达式,例如 (expr)* 中的 (expr),a? 中的 a 和 [0-9]+ 中的 [0-9],在匹配成功之前,进行尝试的次数是存在上限和下限的。规则2表明,这些尝试总是希望获得最长的匹配。

简而言之,标准匹配量词的结果“可能”并非所有可能中最长的,但它们总是尝试匹配尽可能多的字符,直到匹配上限为止。如果最终结果并非该表达式的所有可能中最长的,原因肯定是匹配字符过多导致匹配失败。举个简单的例子:用 \b\w+s\b 来匹配包含’s’的字符串,比如说’regexes’, \w+ 完全能够匹配整个单词,但如果用 \w+ 来匹配整个单词, s 就无法匹配了。为了完成匹配, \w+ 必须匹配 ‘\w+’必须匹配 ‘regexes’,把最后的 s\b 留出来。

匹配优先的性质可以非常有用(有时候也非常讨厌)。它可以用来解释 [0-9]+ 为什么能匹配 March·1998 中的所有数字。1匹配之后,实际上已经满足了成功的下限,但此正则表达式是匹配优先的,所以它不会停在此处,而会继续下去,继续匹配‘998’,直到这个字符串的末尾(因为 [0-9] 不能匹配字符串最后的空挡,所以会停下来)。

过度的匹配优先

^.*([0-9][0-9]) 或许是个有用的正则表达式,它能够匹配一行字符的最后两位数字,如果有的话,然后将它们存储在$1 中。

下面是匹配的过程:

.* 首先匹配整行,而 [0-9][0-9] 是必须匹配的,在尝试匹配行末的时候会失败,这样它会通知 .* 说:“嗨,你占得太多了,交出一些字符来吧,这样我没准能匹配。”匹配优先组件首先会匹配尽可能多的字符,但为了整个表达式的匹配,它们通常需要“释放”一些字符(抑制自己的天性)。当然,它们并不“愿意”这么做,只是不得已而为之。当然,“交还”决不能破坏匹配成立必须的条件,比如加号的第一次匹配。

再来看 ^.*([0-9][0-9]) 匹配 ‘about·24·characters·long’ 的过程。

.* 匹配整个字符串以后,第一个 [0-9] 的匹配要求 .* 释放一个字符 ‘g’(最后的字符)。但是这并不能让[0-9]匹配,所以 .* 必须“交还”字符,接下来交还的字符是‘n’。如此循环15次,直到 .* 最终释放‘4’为止。

不幸的是,即使第一个 [0-9] 能够匹配 ‘4’,第二个[0-9] 仍然不能匹配。为了匹配整个正则表达式, .* 必须再次释放一个字符,这次是‘2’,由第一个[0-9] 匹配。现在‘4’能够由第二个 [0-9] 匹配,所以整个表达式匹配的是‘about·24’,$1 的值是‘24’。

先来先服务

用 ^.*([0-9]+) 匹配 ‘copyright 2003.’ 会捕获到什么?

这个表达式的本意是捕获整个数字 2003,但结果并非如此。为了满足[0-9]+ 的匹配, .* 必须交还一些字符。在这个例子中,释放的字符是最后的‘3’和点号,之后‘3’能够由[0-9] 匹配。 [0-9] 由 + 量词修饰,所以现在还只做到了最小的匹配可能,现在它遇到了’.’,找不到其他可以匹配的字符。

与之前不同,此时没有“必须”匹配的元素,所以 .* 不会被迫交出0.否则, [0-9]+ 应当心存感激,接受匹配优先元素的馈赠。但请记住“先来先服务”原则。匹配优先的结构只会在被迫的情况下交还字符,所以最终 $1 的值是‘3’。

实用技巧

好的正则表达式必须在这些方面求得平衡:

  1. 只匹配期望的文本,排除不期望的文本
  2. 必须易于控制和理解
  3. 如果使用NFA引擎,必须保证效率(如果能够匹配,必须很快地返回匹配结果,如果不能匹配,应该在尽可能短的时间内报告匹配失败)。

处理文件名

处理文件名和路径,例如Unix下的 /usr/local/bin/Perl 或者 Windows下的 \Program Files\Yahoo!\Messenger

  1. 去掉文件名开头的路径

例如把 /usr/local/bin/gcc 变成 gcc

^.*/ 中的 .* 可以匹配一整行,然后回退(回溯)到最后的斜线,来完成匹配。

1
Unix文件名 f = "/usr/local/bin/gcc"
语言 代码
Perl $f =~ s{^.*/}{};
PHP $f = preg_replace(‘{^.*/}’,’’,$f);
java.util.regex f = f.replaceFirst(“^.*/”,””);
VB.NET f = Regex.Replace(f,”^.*/”,””);

Windows文件名 f = “\Program Files\Yahoo!\Messenger”,Windows中的分隔符是反斜线而不是斜线,所以要用正则表达式 ^.*\\ 。在正则表达式中,我们需要在反斜线前再加个反斜线,才能表示转义的反斜线,不过,在中间两段程序中添加的这个反斜线本身也需要转义:

语言 代码
Perl $f =~ s/^.*\//;
PHP $f = preg_replace(‘/\^.*\\\/’,’’,$f);
java.util.regex f = f.replaceFirst(“^.*\\\\”,””);
VB.NET f = Regex.Replace(f,”^.*\\”,””);

从路径中获取文件名

忽略路径,简单地匹配最后的文件名部分,最终的文件名就是从最后一个斜线开始的所有内容: [^/]*$

匹配对称的括号

为了匹配括号部分,我们可以尝试下面的这些正则表达式:

  1. \(.*\) 括号及括号内部的任何字符
  2. \([^)]*\) 从一个开括号到最近的闭括号
  3. \([^()]*\) 从一个开括号到最近的闭括号,但是不容许其中包含开括号

针对

1
var = foo(bar(this), 3.7) + 2 * (that -1);

上面三个正则表达式匹配的位置分别为:

  1. (bar(this), 3.7) + 2 * (that -1)
  2. (bar(this)
  3. 无法匹配,孤立的看,能匹配到‘(this)’,但是因为表达式要求它必须紧接在foo之后,所以无法匹配。

需要匹配的部分 (bar(this), 3.7)

大多数系统中,正则表达式无法匹配任意深度的嵌套结构。Perl,.NET和PCRE/PHP都提供了解决的办法。但是,即使不用这些功能,我们也可以用正则表达式匹配特定深度的嵌套括号,但不是任意深度的嵌套括号。处理单层嵌套的正则表达式是:

1
			\[^()]*(\([^()]*\)[^()]*)*\)
去除文本首尾的空白字符

最好的办法是使用下面两个替换:

1
2
			s/^\s+//;
			s/\s+$//;

为了增加效率,我们用 + 而不是 *,因为如果事实上没有需要删除的空白字符,就不用做替换。

人们似乎更希望用一个正则表达式来解决整个问题,以下这些方法不推荐,但是对理解正则表达式的工作原理及其问题所在,非常有意义。

1
		s/\s*(.*?)\s*$/$1/s

这个正则表达式曾被用作降解忽略优先量词的绝佳例子,但现在不是了,因为人们认识到它比普通的办法慢的多。之所以效率这么低,是因为忽略优先约束的点号每次应用是都要检查 \s*$ 。这需要大量的回溯。

1
		s/^\s*((?:.*\S)?)\s*$/$1/s

这个表达式看起来比上一个要复杂,不过它的匹配倒是很容易理解,而且所花的时间也只是普通方法的2倍。在 ^\s* 匹配了文本开头的空格之后, .* 马上匹配到文本的末尾。后面的 \S 强迫它回溯直到找到一个非空字符,把剩下的空白字符留给最后的 \s*$ ,捕获括号之外的。

问号在这里是必须的,因为如果一行数据只包含空白字符的行,必须要出现问号,表达式才能正常工作。如果没有问号,可能会无法匹配,错过这种只有空白字符的行。

1
		s/^\s+|\s+$//g

这是最容易想到的正则表达式,但它不正确(其实这三个正则表达式都不正确),这种是顶级的多选分支排列严重影响本来可能使用的优化措施。

/g 这个修饰符是必须的,它容许每个多选分支匹配,去掉开始和结束的空格,看起来用 /g 是多此一举,因为我们知道我们只希望去掉最多两部分空白字符,每部分对应单独的子表达式。这个表达式所用时间是普通表达式的4倍。

匹配HTML标签

最常见办法就是用 <[^>]+> ,但是如果标签中含有‘>’,就不能正常匹配了,例如

1
<input name=dir value=">">

‘<…>’中能够出现引用文本和非引用形式的“其他文本”,其中包括除了 ‘>’ 和引号之外的任意字符。HTML的引文可以用单引号,也可以用双引号。但不容许转义嵌套的引号,所以我们可以直接用 “[^”]*” 和 ‘[^’]*’ 来匹配。

把这些和“其他文本”表达式 [^’”>] 合起来,我们得到:

1
2
3
4
5
6
7
8
9
10
<("[^"]*"|'[^']*'|[^'">])*>
<					# 开始尖括号 “<”
	(				# 	任意数量的...
		"[^"]*"		# 		双引号字符串
		|			# 		或者是...
		'[^']*'		#		单引号字符串
		|			#		或者是...
		[^'">]		#		“其他文本”
	)*				#
>					# 结束尖括号 “>”

打造高效正则表达式

聪明的正则表达式实现有许多办法来优化,提高取得结果的速度。优化通常有两种办法:

  1. 加速某些操作。某些类型的匹配,例如 \d+ 极为常见,引擎可能对此有特殊的处理方案,执行速度比通用的处理机制要快。

  2. 避免冗余操作。如果引擎认为,对于产生正确结果来说,某些特殊的操作是不必要的,或者某些操作能够应用到比之前更少的文本,忽略这些操作能够节省时间。例如,一个以 \A (行开头)开头的正则表达式只有在字符串的开头位置才能匹配,如果在此处无法匹配,传动装置不会徒劳地尝试其他位置(进行无谓的尝试)。

通常来说优化能节省时间,但并非永远如此。只有在检测优化措施是否可行所需的时间少于节省下来的匹配时间的情况下,优化才是有益的。事实上,如果引擎检查之后认为不可能进行优化,结果总是会更慢,因为最开始的检查需要花费时间。所以,在优化所需的时间,节省的时间,以及更重要的因素——优化的可能性之间,存在互相制约的关系。

正则表达式的应用原理

  1. 正则表达式编译 检查正则表达式的语法正确性,如果正确,就将其编译为内部形式
  2. 传动开始 传动装置将正则引擎“定位”到目标字符串的起始位置。
  3. 元素检测 引擎开始测试正则表达式和文本,依次测试正则表达式的各个元素
    • 相连元素,例如 Subject 中的 S,u,b,j,e 等等,会依次尝试,只有当某个元素匹配失败时才会停止。
    • 量词修饰元素,控制权在量词(检查量词是否应该继续匹配)和被限定的元素(测试能否匹配)之间轮换。
    • 控制权在捕获型括号内外进行切换会带来一些开销。括号内的表达式匹配的文本必须保留,这样才能通过 $1 来引用。因为一对括号可能属于某个回溯分支,括号的状态就是用于回溯的状态的一部分,所以进入和退出捕获型括号时需要修改状态。
  4. 寻找匹配结果 如果找到一个匹配结果,传统型 NFA 会“锁定”在当前状态,报告匹配成功。而对 POSIX NFA 来说,如果这个匹配是迄今为止最长的,它会记住这个可能的匹配,然后从可用的保存状态继续下去。保存的状态都测试完毕之后返回最长的匹配。
  5. 传动装置的驱动过程 如果没有找到匹配,传动装置就会驱动引擎,从文本中的下一个字符开始新一轮的尝试(回到步骤3)。
  6. 匹配彻底失败 如果从目标字符串的每一个字符(包括最后一个字符之后的位置)开始的尝试都失败了,就会报告匹配彻底失败。

应用之前的优化措施

编译缓存

集成式处理中的编译缓存

Perl 和 awk 使用的就是集成式处理方法,非常容易进行编译缓存。从内部来说,每个正则表达式都关联到代码的某一部分,第一次执行时在编译结果与代码之间建立关联,下次执行时只需要引用即可。这样最节省时间,代价就是需要一部分内存来保存缓存的表达式。

程序式处理中的编译缓存

调用“应用此表达式”函数之后,作为参数的正则表达式模式会与保存的正则表达式相比较,如果存在于缓存中,就使用缓存的版本。如果没有,就直接编译这个正则表达式,将其存入缓存(如果缓存有容量限制,可能会替换一个旧的表达式)。如果缓存用完了,就必须放弃一个编译形式,通常是最久未使用的那个。(最近最久未使用算法)

面向对象式处理中的编译缓存

正则表达式的编译是用户通过 New Regex、re.compile 和 Pattern.compile(分别对应 .NET、Python 和 java.util.regex)之类的构造函数来进行的。

  • 预查必须字符/子字符串优化
  • 长度判断优化
通过传动装置进行优化

字符串起始/行锚点优化

这种优化措施能够判断,任何以 ^ 开头的正则表达式只能在 ^ 能够匹配的情况下才可能匹配,所以只需要在这些位置应用即可。

隐式锚点优化

能使用此种优化的引擎知道,如果正则表达式以 .* 或 .+ 开头,而且没有全局性多选结构,则可以认为此正则表达式的开头有一个看不见的 ^ 。这样就能使用“字符串起始/行锚点优化”,节省大量时间。

字符串结束/行锚点优化

这种优化遇到末尾为 $ 或者其他结束锚点的正则表达式时,能够从字符串末尾倒数若干字符的位置开始尝试匹配。例如正则表达式 regex(es)?$ 匹配只可能从字符串末尾倒数的第8个字符(8个字符而不是7个,是因为许多流派中, $ 能够匹配字符串末尾的换行符之前的位置)开始,所以传动装置能够跳到那个位置,略过目标字符串中许多可能的字符。

开头字符/字符组/子串识别优化

这是“预查必须字符/子字符串优化”的更一般的版本,这种优化使用同样的信息(正则表达式的任何匹配必须以特定字符或文字子字符串开头),容许传动装置进行快速子字符串检查,所以他能够在子字符串中合适的位置应用正则表达式。例如, this|that|other 只能从 [ot] 的位置开始匹配,所以传动装置预先检查字符串中的每个字符,只在可能匹配的位置进行应用,这样能节省大量的时间。能够预先检查的子串越长,“错误的开始位置”就越少。

内嵌文字字符串检查优化

有点类似初始字符串识别优化,不过更加高级,针对的是在匹配中固定位置出现的文字字符串。如果正则表达式是 \b(perl|java).regex.info\b ,那么任何匹配中都要有‘.regex.info’,所以智能的传动装置能够使用高速的Boyer-Moore字符串检索算法寻找‘.regex.info’,然后往前数4个字符,开始实际应用正则表达式。

一般来说,这种优化只有在内嵌文字字符串与表达式起始位置的距离固定是才能进行。因为它不能用于 \b(vb|java).regex.info\b, 这个表达式虽然包含文字字符串,但此字符串与匹配文本起始位置的距离是不确定的(2个或4个字符)。这种优化同样也不能用于 \b(\w+).regex.info\b ,因为 (\w+) 可能匹配任意数目的字符。

长度识别传动优化

如果当前位置距离字符串末尾的长度的长度小于成功匹配所需最小长度,传动装置会停止匹配尝试。

优化正则表达式本身

文字字符串连接优化

也许最基本的优化就是,引擎可以把 abc 当作“一个元素”,而不是三个元素“a,然后是 b,然后是 c”。如果能够这样,整个部分就可以作为匹配迭代的一个单元,而不需要进行三次迭代。

化简量词优化

约束普通元素——例如文字字符或者字符组——的加号、星号之类的量词,通常要经过优化,避免普通NFA引擎大部分逐步处理开销。正则引擎内的主循环必须通用,能够处理引擎支持的所有结构。

消除无必要括号

如果某种实现方式认为 (?:.) 与 .* 是完全等价的,那么它就会用后者替换前者。

消除不需要的字符组

只包含单个字符的字符组有点儿多余,因为它要按照字符组来处理,而这么做完全没有必要。所以聪明的实现方式会在内部把 . 转换成 \. 。

忽略优先量词之后的字符优化

忽略优先量词,例如 “(.*?)” 中的 *? ,在处理过程时,引擎通常必须在量词作用的对象(点号)和 “ 之后的字符之间切换。因为种种原因,忽略优先量词通常比匹配优先量词要慢,尤其是对上文中“化简量词优化”的匹配优先限定结构来说,更是如此。另一原因是,如果忽略优先量词在捕获型括号之内,控制权必须在括号内外切换,这样会带来额外的开销。

所以这种优化的原理是,如果文字字符跟在忽略优先量词之后,只要引擎没有触及那个文字字符,忽略优先量词可以作为普通的匹配优先量词来处理。所以,包含此优化的实现方式在这种情况下会切换到特殊的忽略优先量词,迅速检测目标文本中的文字字符,在遇到此字符之前,跳过常规的“忽略”状态。

此优化还有各种其他形式,例如预查一组字符,而不是特殊的一个字符(例如,检查 [’”][.*?)[”’’] 中的 [’”] ,这有点类似前面介绍的开头字符识别优化)。

常识性优化

避免重新编译

举例来说,如果希望在循环中使用正则表达式,就应该在循环外创建这个正则表达式对象,在循环中重复使用。

使用非捕获型括号
不要滥用括号
不要滥用字符组
使用起始锚点

将文字文本独立出来

从量词中“提取”必须的元素

用 xx* 代替 x+ 能够暴露匹配必须的‘x’。同样的道理, -{5,7} 可以写作 ——{0,2} 。

“提取”多选结构开头的必须元素

用 th(?:is|at) 替代 (?:this|that) ,就能暴露出必须的 th 。如果不同多选分支的结尾部分相同,我们也可以从右面提取。例如

1
(?:optim|standard)ization 
许文忠 wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!