书接上回《正则表达式 - 从 0 到 1(前篇)》,这一篇主要是对正则表达式进阶语法的介绍。javascript
在上一篇中,介绍了正则表达式经常使用的语法,好比字符类、重复、分组等,可是正则表达式还有一些高级语法,这些语法日常可能比较少使用到,可是当你碰到特定场景的时候,就会忍不住叫一声——真香!html
正则表达式拥有一些高级功能,可是并非全部正则引擎都支持,就好比上一篇中提到的自定义字符类的减法、交集等。还有一些正则表达式受环境不一样而拥有特定的特性、语法,好比上一篇中提到的自定义字符类中的 ]
定界符、在使用 /
标记正则时,/
自己也须要转义(自定义字符类中又不须要了)之类的,还有甚至预约义字符类 \d
、\w
、\s
所指代的字符类范围都根据不一样的正则引擎而不同。java
因此,在使用正则表达式以前,仍是要具体熟悉一下所使用的正则引擎是否有特定的要求。对于 \d
、\w
、\s
这些预约义字符类,可使用自定义字符类来强制指定范围,好比 \d
可使用 [0-9]
来代替,可是,这并非必须的!由于即使是不一样正则引擎中 \d
指代的范围不一样,但不变的事实是,\d
永远匹配数字。python
举个例子,\d
在 JavaScript、Java、PCRE 等支持 Unicode 的正则引擎中仅与 ASCII 数字匹配,可是在其余大部分支持 Unicode 的正则引擎中,\d
还与 Unicode 中的其余数字系统匹配。正则表达式
好比真·阿拉伯数字(东方阿拉伯数字 Eastern Arabic numerals)“٠١٢٣٤٥٦٧٨٩”:express
在 JavaScript 中是没法匹配的:编程
// 你能够把下面的代码复制到浏览器的 Console 中执行(Chrome 快捷键:Ctrl+Shift+J / Command+Option+J)
const regex = /\d+/;
const numbers1 = '0123456789';
const result1 = numbers1.match(regex);
console.log(numbers1.length); // 10
console.log(result1); // ["0123456789", ......]
const numbers2 = '٠١٢٣٤٥٦٧٨٩';
const result2 = numbers2.match(regex);
console.log(numbers2.length); // 10
console.log(result2); // null
复制代码
而在 C#.NET 中,则能够成功匹配:浏览器
// 你能够在 https://ideone.com/dzh4iI 在线测试下面这个代码
using System;
using System.Text.RegularExpressions;
namespace RegexTester {
public class Program {
public static void Main(string[] args) {
Regex regex = new Regex(@"\d+");
var numbers1 = "0123456789";
var result1 = regex.Matches(numbers1);
Console.WriteLine("Length: {0}", numbers1.Length); // Length: 10
Console.WriteLine("Matched: {0}", result1[0]); // Matched: 0123456789
var numbers2 = "٠١٢٣٤٥٦٧٨٩";
var result2 = regex.Matches(numbers2);
Console.WriteLine("Length: {0}", numbers2.Length); // Length: 10
Console.WriteLine("Matched: {0}", result2[0]); // Matched: ٠١٢٣٤٥٦٧٨٩
}
}
}
复制代码
在 Python3 中,也能够成功匹配:app
# 你能够在 https://ideone.com/BIYK0O 在线测试下面这个代码
import re
regex = r'\d+'
numbers1 = '0123456789'
result1 = re.search(regex, numbers1)
print(len(numbers1)) # 10
print(result1[0]) # 0123456789
numbers2 = '٠١٢٣٤٥٦٧٨٩'
result2 = re.search(regex, numbers2)
print(len(numbers2)) # 10
print(result2[0]) # ٠١٢٣٤٥٦٧٨٩
复制代码
\d
自己的含义就是匹配“数字字符”,所以匹配到 Unicode 中其余非 ASCII 数字也无可厚非,由于那些字符确实都是数字字符。因此,在实际应用过程当中,要根据实际状况,决定是继续使用 \d
仍是转成更明确的 [0-9]
。毕竟,在多语言环境下,支持 Unicode 的 \d
带来的用户体验可能会更好。可是若是代码编写的时候存在考虑不完善的地方(也就是代码存在 Bug),\d
带来的 Unicode 支持可能会产生未知的后果。编程语言
嗯,扯了这么多,就是想说明一个事实,就是正则具体支持什么语法都是根据引擎实现而决定的,甚至还和不一样引擎的不一样版本相关,特别是本文将要提到的这些“高级语法”,以及我我的也不多接触到的其余“高级 S Plus Max 语法”。
因此,本文也仅做参考,做为知识扩充阅读便可,具体在读者所用的正则引擎是否支持,也还请读者自行测试。
下面咱们就开始吧~
在知道了分组与分组的反向引用以后,在有些时候,可能会出现一些问题。
考虑这样的正则表达式:/^(a?)b\1$/
,若是要匹配的字符串是 aba
,那毫无疑问,能够匹配成功,可是若是要匹配的字符串是 ba
或是 b
呢,因为第一个分组中的 a?
是能够没有的,此时第一个分组就没有东西了。因此后面的 \1
也应该是空的,因此,结果是 ba
没法成功匹配而 b
能够成功匹配。
再考虑这样的正则表达式:/^(a)?b\1$/
,与上面相似,若是要匹配的字符串是 aba
,毫无疑问,能够匹配成功,可是对于 ba
或是 b
呢?因为第一个分组总体都是可选的,因此此时,第一个分组将不存在。此时后面须要引用 \1
,对于大多数正则引擎来讲,ba
和 b
都将会致使失败。可是 JavaScript 是一个例外,对于 JavaScript 来讲,即使第一个分组是可选的,在它不存在的时候,它也表示一个空分组。因此对 JavaScript 来讲,ba
匹配失败,可是 b
能够成功匹配。
对于相似于 /(test)\7/
、/(test)\12/
这样,分组数只有 1 个,但却引用了 7 号分组、12 号分组,这在绝大多数正则引擎中都是一个错误。可是,JavaScript 因为支持八进制转义符,因此,若是分组不存在,JavaScript 会尝试将其解释为一个八进制字符,因此 \7
、\12
都是合法的,可是 \8
、\9
是不合法的八进制字符,就属于错误。而 Java 中,对不存在的组的引用将不会匹配任何内容。在 .NET 中,虽然支持八进制转义符,可是必须是两位数的八进制,因此,\7
属于错误,而 \12
确是合法的。
注:对 JavaScript 和 .NET 来讲,只有分组号不存在时,才会尝试解释为八进制转义符。
在“分组”中,咱们知道正则中可使用一对小括号 ()
来建立一个分组,而后在“分组引用”中,咱们学会了使用序号来引用一个分组的内容。
可是,在不少状况下,分组也被用来协助重复、枚举,而这些分组的存在会干扰咱们对分组的统计计数,致使后面进行分组引用的时候编号很难肯定。
为了方便计数,咱们能够将不须要被引用的分组设置为“非捕获组”,只要在分组的开头,括号内,添加 ?:
便可。
示例:
/^(130|131)(\d{4})\2$/
匹配 130、131 号段,后面 8 位数字前四位和后四位相同的手机号码,好比13012341234
、13156785678
/^(?:130|131)(\d{4})\1$/
同上。
这里第一个例子里使用前面说的普通分组,也叫“捕获组”,要引用第二个分组 (\d{4})
时,分组号是 2。而第二个例子中,将第一个分组设置为了非捕获组,那么分组序号将从第二个开始设置为 1,因此要引用 (\d{4})
这个分组时,分组号就是 1。
有了非捕获组,在复杂状况下仍是很难编号,而且使用编号引用的话也会使正则变得难以阅读。为了方便分组,正则引入了“命名组”的概念,也就是给分组起名字,这样就不须要去数分组的序号了,只要使用分组的名字去匹配便可。
命名组在不一样正则引擎中语法不一样,第一个支持命名组的正则引擎是 Python 的 re,使用的语法是 (?P<name>group)
,而要引用这个命名组,则使用 (?P=name)
。后来 .NET 也开始支持命名组,可是微软使用的语法是 (?<name>group)
或 (?'name'group)
,而要引用命名组则使用 \k<name>
或 \k'name'
,.NET 的命名组中的组名可使用尖括号,也可使用单引号,二者在正则引擎中没有区别。
在 Python 和 .NET 都有了命名组,而且有了三种命名组的写法后,Perl 5.10 冒了出来,同时支持 Python 和 .NET 的三种写法,而且在这个基础上,还给分组引用又带来了两种新的语法:\k{name}
和 \g{name}
。emmmmmm,新增的这两种语法与 Python 和 .NET 的那三种在引擎中彻底等同,没有任何区别。(╯‵□′)╯︵┻━┻
Java 也使用了 .NET 的语法,可是只支持使用尖括号做为组名的语法,而不支持使用单引号的形式。
JavaScript 从 ES2018 开始支持命名捕获组,与 Java 同样,使用 .NET 的语法,也只支持使用尖括号做为组名的语法而不支持使用单引号的形式。
总之,命名组如今几乎全部正则引擎都支持,可是具体使用的语法,还请读者自行尝试!
示例:
/^<(?P<tag>[a-zA-Z][a-zA-Z0-9]*)(?:\s+[^>]*)?>.*<\/(?P=tag)>$/
使用 Python 语法,能够简单匹配 HTML 标签(复杂状况暂不考虑)
/^<(?<tag>[a-zA-Z][a-zA-Z0-9]*)(?:\s+[^>]*)?>.*<\/\k<tag>>$/
使用 .NET 尖括号语法,同上
/^<(?'tag'[a-zA-Z][a-zA-Z0-9]*)(?:\s+[^>]*)?>.*<\/\k'tag'>$/
使用 .NET 引号语法,同上
上面的例子中,先是以 <
符号开头,表示 HTML 标签开始。而后跟着一个名称为 tag
的命名组,组内容为 [a-zA-Z][-a-zA-Z0-9]*
,即为单个字母,或字母后跟任意个数字、字母或是 -
,也就是 HTML 标签名称的规则。而后跟着一个非捕获组,用于匹配 HTML 标签的属性,以一个或多个空格开头,跟着任意个不是 >
的字符(只作简单匹配,复杂状况暂不考虑),这个非捕获组后面有一个 ?
,表示 HTML 属性无关紧要。再后面跟着一个 >
符号,表示 HTML 标签结尾。而后是 .*
用于匹配 HTML 标签的内容(只作简单匹配,复杂状况咱不考虑),而后是 <\/
,表示 HTML 结束标签的开始,由于使用 /
来标记正则,因此 /
须要 \
进行转义。而后是引用前面名称为 tag
的分组。最后跟着一个 >
表示 HTML 结束标签的结尾。
在正则表达式中,有一些仅仅表明“位置”的符号,好比 ^
表示字符串的开头位置,$
表示字符串结尾的位置。实际上,正则还有一个表示位置的符号 \b
,它表示“单词边界”。
它与上一篇中提到的 \d
、\w
和 \s
不同,不属于字符类,它仅仅表示一个特殊的位置,这个位置一般有这样的特征:
^
相同的位置$
相同的位置具体哪些字符属于单词字符,也是取决于所使用的正则表达式引擎的,一般来讲,能被 \w
匹配的字符都是单词字符。可是 Java 是个例外,在 Java 中,部分 Unicode 字符能够做为 \b
的单词字符,但却没法被 \w
匹配。
示例:
/\bbed\b/
匹配独立为单词的bed
,好比Lying in bed, but can't sleep
、There is a bed in the corner
,可是Sleeping in the bedroom
就没法匹配到了。
还记得上一篇提到的“重复”吗?除了 {m}
这样的固定次数匹配,其余的“重复”的具体重复次数都是不肯定的,好比 +
能够匹配至少一次,但上不封顶。
这就麻烦了,考虑这个示例:/<.+>/
,咱们想要使用这个正则表达式来匹配相似于 <div>
这样的字符串,可是,若是你的字符串是 <div>Test</div>
,你会发现,匹配结果是整个字符串,也的确,整个字符串也的确知足正则的条件,毕竟 .
表示任意字符,因此 d
、i
、v
、>
、T
、e
、s
、t
、<
和 /
都是知足条件的。
在正则里,不定次数的重复都是“贪婪”的,贪婪的意思就是它会尽量多的匹配字符,因此上面这个例子中,<
匹配完第一个字符后,.+
开始重复任意次,因此它会一直日后找,直到找到一个不知足 .
的字符,可是后面都是匹配的,因此,就匹配到最后一个字符,而后再去尝试找知足 >
的字符。可是因为 .+
已经匹配了全部字符,所以 >
没法匹配到,因此 .+
须要作一个让步,放出一个字符 >
,使得 >
匹配成功。
为了使上面的例子符合咱们的要求,只匹配到第一个 >
就结束,咱们能够将重复转为“懒惰”模式,只须要在重复符号后加一个 ?
便可,好比 +
变为 +?
,*
变为 *?
,?
变为 ??
,{m,n}
变为 {m,n}?
。上面的例子能够改为 /<.+?>/
,就能够匹配到 <div>Test</div>
中的 <div>
了。
在使用贪婪模式的时候,必定要当心!由于正则表达式在遇到贪婪重复的时候,会一直日后递归匹配,直到发现第一个不知足条件的结果时,才会一点一点向前回溯,直到找到知足条件的结果,或者回溯到原点,才会匹配失败。这个过程是比较危险的,由于这会致使正则表达式的匹配时间呈指数性爆炸式增加,而且会使 CPU 占用大量上涨。
2019 年 7 月初,全球知名的 CDN 提供商 Cloudflare 出现了全球范围的 502 故障,缘由就是因为使用了一个贪婪匹配的正则表达式。感兴趣的读者能够阅读 Cloudflare 的博客(具体讲解在博客附录部分):blog.cloudflare.com/details-of-…
若是咱们在进行正则匹配的时候,要忽略大小写,咱们一般可使用 [a-zA-Z]
来同时匹配小写与大写,可是对于复杂状况,这样作可能会比较麻烦。
正则表达式一般都支持使用选项进行行为控制,可是不一样的正则引擎支持的选项都不尽相同,具体使用的正则引擎支持哪些选项,须要读者自行查询相关文档。
常见的选项有:
i
:忽略大小写s
:单行模式,使得 .
与包括换行符在内的全部字符匹配(默认 .
是不包含换行符的)m
:多行模式,使得 ^
和 $
再也不只表示整个字符串的开头和结尾,而是表示每一行的开头与结尾。不一样的正则引擎设置选项的方式也都不一样,大概总结一下,有如下几种设置选项的方式:
/Hello/i
,这将对整个正则表达式生效,使其忽略大小写。new RegExp('Hello', 'i')
,这也将对整个正则表达式生效,使其忽略大小写。(?flag)
语法设置选项,好比 /(?i)Hello/
。
/Hello (?i)World/
中,Hello
是要求 H
大写,ello
小写的,可是 World
则不区分大小写,因此能够匹配 Hello WORLD
、Hello World
、Hello world
,可是 HELLO World
则没法匹配。Hello World(?i)
将没有任何效果,或者在某些正则引擎中被视为错误。Hello World(?i)
会使得整个正则表达式忽略大小写。示例:
/^Apple$/i
能够匹配Apple
、apple
、APPLE
、appLE
、AppLe
等。
有时,咱们须要为匹配结果增长一些条件限制,但又不想在匹配结果中包含这些限制条件。什么意思呢?好比咱们要匹配区号为 0123 的中国座机号码,而且匹配结果不包含区号。中国的座机号码一般有两种,一种是 012-12345678,前面三位是区号,后面 8 位是座机号码,另外一种是 0123-1234567,前面四位是区号,后面 7 位是座机号码,而且区号一般都是以 0 开头。
emmmmmm,是否是想打人,这都什么复杂的条件……
只要先用 StartsWith 函数检查字符串是以 0123 开头,而后使用 SubString 函数截取后面几位便可……
emmmmmm,要用正则匹配……
正则里有一个功能叫作“零宽断言”,用于声明当前位置要匹配的内容,但仅仅是声明,不作任何匹配。零宽断言分为前瞻和后顾两种模式,前瞻和后顾又分为正向和负向两种方式,排列组合一下一共四种:正向前瞻 (?=regex)
、负向前瞻 (?!regex)
、正向后顾 (?<=regex)
和负向后顾 (?<!regex)
。
前瞻,就是向前看的意思,也就是从当前位置向字符串后面😓看;然后顾,就是向后看的意思,也就是从当前位置向字符串前面😓看。正好相反,是由于对于正则来讲,永远都是从字符串“前面”开始向“后面”进行匹配(对于 LTR 语言来讲,就是从左到右),因此,对于正则来讲,前瞻就是“向字符串后面看”,后顾就是“向字符串前面看”。
正向和负向,分别表示匹配成功和匹配失败。
示例:
/(?<=0123-)\d{7}/
能够匹配0123-
后面的 7 位任意数字,好比0123-1234567
,匹配结果为1234567
,不包含0123-
。而0234-1234567
、012-1234567
等不是由0123-
开头的就会匹配失败。
/(?<!\d{4}-)\d{8}/
能够匹配前面跟的不是 4 位数字加一个-
的 8 位任意数字,好比012-12345678
匹配结果为12345678
,01212345678
匹配结果为01212345
。而0123-12345678
前面跟了 4 位数字加一个-
,因此匹配失败。
/[a-z]+(?=ed)/
能够匹配ed
结尾的字母串,好比ended
匹配结果为end
,opened
匹配结果为open
,abcededed
匹配结果为abceded
(贪婪原则)。而相似于end
、be
等不是由ed
结尾的则会匹配失败。要注意的是,bedroom
能够匹配成功,匹配结果为一个字母b
。
/\d{4}(?!\.12)/
能够匹配不是由.12
结尾的 4 位数字,好比1234.56
匹配结果为1234
,1234.1
匹配结果为1234
,12345.12
匹配结果为1234
。而1234.12
则会匹配失败。
/[a-z]+(?!ed)/
能够匹配所有由字母组成的字符串。
/(?<!0123)\d+/
能够匹配所有由数字组成的字符串。
(・∀・(・∀・(・∀・*)??? 匹配所有由字母组成的字符串?匹配所有由数字组成的字符串?但是上面不是说负向零宽断言,应该匹配“不是由 ed
结尾的字母串”、“不是由 0123
开头的数字串”吗?
再仔细想一想,opened
这种字母串,虽然是由 ed
结尾,可是若是把 opened
看做一个总体,这一个总体后面可就没东西了,因此这个总体并非由 ed
结尾的,因此匹配成功,匹配结果就是 opened
;而数字串同理,01231234567
这样的数字串,虽然是由 0123
开头的,可是把他看做一个总体,总体前面没有数字了,因此这个总体不是由 0123
开头的,因此匹配成功,匹配结果就是 01231234567
。注意!这与贪婪原则或懒惰原则没有关系!
因此,在使用负向的零宽断言时必定要注意匹配结果是否有意义!
部分正则引擎不支持后顾式的零宽断言 (?<=regex)
和 (?<!regex)
,好比 JavaScript(Chrome 62 开始支持,Firefox、Safari 至今彻底不支持,Node.JS 与 Chrome 使用相同的 V8 引擎,因此自 Chrome 支持之后 Node.JS 也开始支持)。
零宽断言的概念比较复杂,它和 ^
、$
、\b
相似,就表示一个位置。
再举个例子,好比我要匹配一个 11 位的数字串,它中间要包含 1234
。这是两个条件,要同时知足。好比 13012345678
、12341301234
、13056781234
等。
使用现有的正则能力,是否能够完成呢?
若是仅仅检查长度:/^\d{11}$/
,就没有办法检查是否包含 1234
了。
若是简单的 /^\d*1234\d*$/
,这样虽然能匹配到包含 1234
的数字串,可是没有办法保证总体的长度。
若是写成 /^\d{3}1234\d{4}$/
,能够成功匹配相似于 13012345678
这样的数字串,可是 12341301234
这种就无能为力了。
可怕的想法是:/^(1234\d{7}|\d1234\d{6}|\d{2}1234\d{5}|\d{3}1234\d{4}|\d{4}1234\d{3}|\d{5}1234\d{2}|\d{6}1234\d{1}|\d{7}1234)$/
,能够完美匹配,可是……emmmmmm,是否是有笨……
实际上,若是能灵活使用零宽断言,这个问题就能够很好的解决了:
示例:
/^(?=\d{11}$)\d*?1234\d*/
能够匹配 11 位数字,而且包含1234
的数字串。
仅此而已,很是简单。这里要对“仅仅表示一个位置”有一个深入的理解。
没有人说过 $
必定要放在整个正则表达式的结尾,也没有人说过正向前瞻零宽断言必定要放在正则其余内容的后面。
这里正则在匹配的时候,首先碰到一个正向前瞻零宽断言,因此会直接“向字符串后面看”,检查从当前位置(也就是起始位置)开始,后面是否跟着 11 位数字,而且 11 位数字以后是字符串的结尾。此时正则匹配的位置仍是在字符串的开头,这只是一个“预检测”。检查经过,正则开始从当前位置正式开始匹配 \d*?1234\d*
,第一个 \d*?
采用懒惰模式,后面一个实际上采用贪婪模式或是懒惰模式都无所谓,由于前面 \d*?1234
必定是成功匹配了 0 到 7 位数字 + 1234
,而以前检查了字符串必定是由 11 位数字组成,因此,后面的 \d*
必定是匹配剩余的全部数字,因此用贪婪模式便可。
但注意,上面的示例中,最后面的 \d*
是不能省略的,虽然即使是省略了,也能够匹配成功,可是匹配结果将不会包含 1234
后面的内容。由于零宽断言只作检查,并不会将检查的内容放入匹配结果中,因此,结果只包含 \d*?1234\d*
所匹配到的内容。
if A then B else C
是常见编程语言中的基本逻辑之一(虽然不一样语言语法不尽相同),它表示判断条件 A 是否成立,若成立则进入 B,若不成立则进入 C。
在 Python、.NET、Perl、PCRE 这些正则引擎中也有相似的结构,语法是 (?ifthen|else)
。注意 if 和 then 之间没有空格。else 能够省略,变为 (?ifthen)
。
其中 if 可使用零宽断言或是分组引用来指定。当条件成立时,匹配 then,条件不成立时,匹配 else。
示例:
/^\d{4}(0[1-9]|1[012])(?(?<=0[469]|11)(0[1-9]|[12]\d|30)|(?(?<=02)(0[1-9]|[12]\d)|(0[1-9]|[12]\d|3[01])))$/
能够匹配YYYYMMDD
格式的日期,其中年份没有要求,可是日期必须合法(容许 2 月 29 日),好比20120229
、20130430
、20150531
等,可是20190230
、20130631
、20150740
、20181305
就没法匹配。
这里首先 \d{4}
判断字符串以 4 位数字开头,表示年份。而后 (0[1-9]|1[012])
匹配合法的月份(01 ~ 12)。而后后面的总体是一个条件语句,条件是正向后顾零宽断言 (?<=0[469]|11)
,由于月份已经被匹配了,因此此时的位置处于月份以后,因此须要使用“后顾”来判断前面的月份,也就是判断是有 30 天的“小月”。若条件成立,则匹配 (0[1-9]|[12]\d|30)
;若条件不成立,则匹配后面的内容,然后面又是一个条件语句,条件仍是正向后顾零宽断言 (?<=02)
,因为至此位置仍是处于月份以后,因此也是使用“后顾”来判断,若是月份为 02
,则条件成立,匹配 (0[1-9]|[12]\d)
;不然条件不成立,匹配 (0[1-9]|[12]\d|3[01])
。
虽然正则匹配日期的写法不止一种,大多数状况下都是使用枚举的方式直接匹配,这个例子只是举一个使用条件语句的例子而已。
条件语句使用分组引用来指定条件相似于这样:
示例:
/^(a)?b(?(1)c|d)$/
能够匹配abc
和bd
,其余组合都不能匹配
/^(?<x>a)?b(?('x')c|d)$/
同上,使用命名捕获组
因为第一个分组 (a)?
是可选的,所以后面判断第一个分组是否存在,若存在则条件成立,匹配 c
不然匹配 d
。
有时咱们须要匹配一个递归嵌套的字符串,好比 XML 标签、前缀表达式等。他们的特色是层层嵌套,每一层都有区别,但又有特定的规则。
在软件开发的时候,咱们一般会使用递归函数来作一些嵌套、重复的事情。
正则中也有相似的东西:Perl、PCRE、Ruby 等正则引擎支持递归重复,而 .NET 支持平衡组。
递归重复在不一样的正则引擎中语法也不太同样,在 Perl 中,使用 (?R)
或 (?0)
;在 Ruby 中,使用 \d<0>
;PCRE(以及 PHP、Delphi、R 这些基于 PCRE 的正则引擎)同时支持 Perl 和 Ruby 的这三种语法。
正则在匹配的时候,若是碰到递归重复标记,将会从当前位置开始,将整个正则表达式从新匹配一遍,一层一层递归,直到最内层匹配结束以后,再一层一层往上回溯。若其中一层匹配失败,将会致使整个递归匹配失败。
示例:
/([a-z])(?R)??\1/
能够匹配字符数为偶数个的回文字,好比aa
、abba
、abccba
、abcddcba
等。而字符数为奇数个,或者不是回文,则匹配失败,好比aba
、abcba
、abab
都会匹配失败。
/\([+\-*\/](?:\s(?:\d+|(?R))){2}\)/
能够匹配前缀表达式,要求操做符只有+
、-
、*
和/
,操做数有且仅有两个。好比(+ 1 2)
、(* (/ 6 2) (+ 5 (- 7 6)))
、(/ 123 (/ (* 99 99) (+ 0 1)))
等。若是括号不匹配、操做符不匹配、操做数不匹配,都会致使匹配失败!
注意,递归重复必须拥有跳出条件,好比上例中使用的枚举,或是 ?
标记为可选。若递归重复没有跳出条件将致使递归死循环错误。好比,/(?R)/
将直接报错,由于这将触发无限递归。因为递归重复是针对整个正则表达式进行重复,所以若是正则表达式以 ^
开头将会永远匹配失败,由于当发生递归时,“当前”位置永远不多是字符串开头。
还有,注意递归重复的性能问题,对于上面的第一个示例,其中的递归标记使用了两个 ?
表示懒惰,这在某些状况下能够加快速度。测试匹配 abcdefggfedcba
这个字符串,使用懒惰模式 /([a-z])(?R)??\1/
完成匹配须要 50 步,而使用贪婪模式 /([a-z])(?R)?\1/
则须要 81 步;而测试匹配 abcdefggfedcbb
(最后一个 a
改为了 b
),懒惰模式须要 97 步,而贪婪模式则须要 165 步。
平衡组是微软在 .NET 中支持的一种“递归重复”的解决方案。
与递归重复不一样的是,平衡组并非采用重复整个正则表达式的方式来实现的,而是采用命名捕获组或是非捕获组来实现的。这比递归重复更加灵活。
使用命名捕获组的语法是:(?<name-balance>group)
或 (?'name-balance'group)
,其中 name
是捕获组的名字,而 balance
是平衡组的名字;使用非捕获组的语法是省略捕获组的名字:(?<-balance>group)
或 (?'-balance'group)
。
在绝大多数正则引擎中,命名捕获组若是出现屡次,那么匹配结果中命名捕获组的值将会是最后一次出现的值,好比 /^(?<name>\w)+$/
匹配 ab
,则捕获组 name
的结果只能获得 b
,而 ^(?<name>\w)(?<name>\w)$
这样重复的组名则被视为是错误。
但在 .NET 中,/^(?<name>\w)+$/
和 ^(?<name>\w)(?<name>\w)$
均可以对 ab
匹配成功,而且虽然捕获组 name
的值会被 b
覆盖,可是全部的历史匹配结果都会存储在捕获组的 Captures
属性中。
// 你能够在 https://ideone.com/SLDjlH 在线测试下面这个代码
using System;
using System.Text.RegularExpressions;
namespace RegexTester {
public class Program {
public static void Main(string[] args) {
Regex regex = new Regex(@"^(?<name>\w)+$");
var str = "abcde";
var result = regex.Matches(str);
for (var i = 0; i < result.Count; ++i) {
var item = result[i];
Console.WriteLine("Group Count: {0}", item.Groups.Count);
foreach (Group group in item.Groups) {
Console.WriteLine(@"Group ""{0}"" = ""{1}""", group.Name, group.Value);
foreach (Capture groupItem in group.Captures) {
Console.WriteLine(@"Group ""{0}"" Captured ""{1}""", group.Name, groupItem.Value);
}
}
}
}
}
}
// 输出结果:
/* Group Count: 2 Group "0" = "abcde" Group "0" Captured "abcde" Group "name" = "e" Group "name" Captured "a" Group "name" Captured "b" Group "name" Captured "c" Group "name" Captured "d" Group "name" Captured "e" */
复制代码
正由于如此,.NET 拥有了追踪捕获组历史的能力,也就所以创造了平衡组。
正则引擎在匹配的时候,若是遇到捕获组,将会把捕获组放入 Captures
栈中,而遇到平衡捕获组时,将会在指定的捕获组的 Captures
栈中 pop 出最后一个结果。若是对应 Captures
栈中没有结果了,则匹配将会失败。
示例:
/^(?<char>[a-z])+[a-z]?(?<-char>\k<char>)+(?(char)(?!))$/
能够匹配任意回文字,好比aa
、aba
、abba
、abcba
、abccba
等。
这个例子中,先是一个 (?<char>[a-z])+
命名捕获组 char
匹配 [a-z]
,并重复至少一次,而且每次重复都将匹配到的字母压入 Captures
栈中。而后是 [a-z]?
能够匹配可选的一个任意字母,由于回文字最中间的字母不必定须要重复。以后是 (?<-char>\k<char>)+
,其中 \k<char>
反向引用捕获组 char
的值,若匹配成功,(?<-char>\k<char>)
平衡组将会 pop 出 Captures
栈中最后一个结果,此时捕获组 char
的值变为倒数第二个匹配的值;若是不存在 char
这个分组,或是 Captures
已经为空,则直接匹配失败,这个过程重复至少一次。最后是一个条件语句 (?(char)(?!))
,省略了 else,它判断分组 char
是否存在,若存在则匹配 (?!)
,这是一个永远失败的匹配语法(前瞻匹配空字符串 /(?=)/
永远成立,负向前瞻匹配空字符串 /(?!)/
永远失败)。由于回文字是对称的,因此字母数量必定是相等的,所以若是知足条件,此时 char
分组应当所有被平衡组抵消而不存在了。若分组 char
仍然存在,则表示回文字母的数量先后不一致,使用 (?!)
强行匹配失败。
示例:
/^(?:[^<>]*?(?:(?'bracket'<)[^<>]*?)+?(?:(?'-bracket'>)[^<>]*?)+?)+(?(bracket)(?!))$/
匹配彻底配对的<>
包围的字符串。好比bbb<aaa<ab<abc>abc<aaa<aaa>>a>aaa>aaa
。
注意,在匹配失败的时候,性能损失是肉眼可见的。
这个例子中,最外层是一个 (?:...)+
非捕获组的重复。
后面是一个条件语句 (?(bracket)(?!))
,由于咱们要求 <>
括号配对,那么二者数量必定是相等的,所以若是知足条件,此时 bracket
分组应当所有被平衡组抵消而不存在了。若分组 bracket
仍然存在,则表示 >
的数量小于 <
的数量,所以括号不匹配,使用 (?!)
强行匹配失败。
在第一个非捕获组中,先是 [^<>]*?
匹配任意个非括号字符;而后是一个 (?:(?'bracket'<)[^<>]*?)+?
要求至少出现一次的非捕获组,其中 (?'bracket'<)
表示匹配一个 <
并放入 Captures
栈,而后 [^<>]*?
匹配任意个非括号字符;而后又是一个 (?:(?'-bracket'>)[^<>]*?)+?
要求至少出现一次的非捕获组,其中 (?'-bracket'>)
平衡组尝试匹配一个 >
,若匹配成功,再检查 Captures
栈是否非空,若是 Captures
栈为空则直接匹配失败,由于此时没有对应的 <
来与之配对了,若 Captures
栈非空,则 pop 出一个后继续匹配 [^<>]*?
任意个非括号字符。
实际上就是这样,正则还有不少东西没有提到,感兴趣的读者能够去 www.regular-expressions.info/tutorial.ht… 看到更多更详细的介绍。
是否是看的愈来愈迷糊?这仍是上一篇中的那个简单高效的文本查找匹配工具吗?如今怎么有点怀疑人生了?
emmmmmm……
的确是这样的,正则本应该简单高效,再软件开发过程当中,复杂的逻辑理应交给编程语言去实现。而像这种条件语句、递归循环之类的就没有必要用正则去写了。
可是,存在即合理,既然提供了这样的语法功能,那么就必定有应用场景。这些复杂语法功能,看过了,了解一下,语法不必定记住,至少知道有这么个东西,再碰到特定场景的时候,可以想起来,老是会喊一声“真香”的。
记得要点赞、分享、评论三连,更多精彩内容请关注ihap 技术黑洞!