正则表达式的秘密(四)构成元素和方法 下

366 阅读4分钟

书接上回,介绍完了捕获子组,引用和条件子组。

我们今天说一下”非捕获子组",也就是即便匹配成功也没有返回值的子组。非捕获子组包括3种:指定非捕获子组,一次性子组,断言assert。

指定非捕获子组:(?:pattern)

非常简单,使用这个方法后,子组包含的表达式pattern就不会返回任何捕获值,如果需要在非捕获子组开始位置设置特征修饰符,有两个模式:(?i:pattern) 和 (?:(?i)pattern) 注意一点:因为非捕获子组不会出现在返回数组里,所以不会对其后子组排序产生影响。这个在我们使用后向引用时,会对n有影响

举例:(?:red|green) (king|queen)匹配字符串 ”the red king”,返回值是
[0] => red king //默认返回整个匹配字符串

[1] => king //第一个子组的捕获值,因为(?:red|green)子组不捕获,所以它成了第一个子组

一次性子组(?>pattern)

一次性子组之所以叫”一次性“就是它只运行一次,并且会尽其所能吃掉尽可能多的匹配字符。

举例:(?>\w+)abc去匹配123456abc,匹配结果是失败。而\w+abc匹配是成功的,因为(?>\w+)一次性就吃掉了所有字符,导致无法再去匹配abc.

是不是和我们之前讲量词里面的加号+很像?功能相似,但一次性子组的应用范围跟广。使用它的作用,是可以大幅提升正则匹配性能,这对服务器端来说很重要,而且匹配更有指向性。

关于性能提升的内部算法,一次性子组的实现过程与”后向回溯“有关,这里引用一段官方文档的描述,有兴趣的可以了解一下:

一般模式下: \d+foo 应用到目标行 123456bar 时:在匹配了 6 个数字后匹配 ”foo” 时失败,通常的行为时匹配器尝试使 \d+ 只匹配 5 个数字, 只匹配 4 个数字,在最终失败之前依次进行尝试。

当特征的一部分得到匹配后,不再对其进行后向回溯重新评估,匹配器在第一次匹配失败后就能立刻失败。语法符号是另外一种特殊的括号, 以 (?> 开始,比如 (?>\d+)bar。

一次性子组对特征的一部分提供了”锁定”,当它包含一个匹配之后,即便之后的特征不匹配,也会阻止之后特征匹配失败后对它内部的后向回溯。后向回溯在这里失效, 其他工作照常进行。

断言子组assert

这是个英文翻译起来也很晦涩,给我们理解造成了障碍,不去管它。

它实际上是对字符串两端进行特征匹配

有2种形式:

  1. 简单的方式,通过一个标记,对目标开始或结尾的字符进行匹配。标记本身不会实际消耗任何字符。简单的断言标记有\b、\B、 \A、 \Z、\z、 ^、$ ,这个例子非常多,不具体讲了。

  2. 复杂一些的,通过断言子组。可以根据子组返回值的方向分为前瞻和后顾两类,具体如下:

类型是/非方法含义
前瞻Look ahead(?=pattern)从pattern的位置往前看,如果字符串结尾匹配pattern,则返回pattern前面的内容,不返回pattern本身;
前瞻(?!pattern)如果字符串结尾不匹配pattern,则返回pattern前面的内容,不返回pattern本身;
后顾 look back(?<=pattern)从pattern的位置往后看,如果字符串开始匹配pattern,则返回pattern后面的内容,不返回pattern本身;
后顾(?<!pattern)如果字符串开始不匹配pattern,则返回pattern后面的内容,不返回pattern本身;

举例:<span class=\"read-count\">阅读数:1234</span>我们想抓取阅读量:

前瞻 \d*(?=</span>) //返回结果:1234;

后顾 (?<=<span class=\"read-count\">阅读数:)\d* //返回结果:1234;

断言子组还有一些特别的用法

  1. 后顾和前瞻可以同时使用,比如上面的例子可以改写成(?<=<span class=\"read-count\">阅读数:)\w*(?=</span>)

  2. 多个断言子组可以同时出现并且任意顺序。比如:(?<=\d{3})(?<!999)foo 匹配前面有三个数字但不是 ”999” 的字符串 ”foo”。注意, 每个断言独立应用到对目标字符串该点的匹配。首先它会检查前面的三位都是数字, 然后检查这三位不是 ”999”。

  3. 断言可以以任意复杂度嵌套。比如 (?<=(?<!foo)bar)baz 匹配前面有 ”bar” 但是 ”bar” 前面没有 ”foo” 的 ”baz”。

  4. 一次性子组可以和后顾断言结合使用,用来指定在目标字符串末尾的有效匹配,且返回值包含断言子组。比如(?>.*)(?<=abcd),可以匹配'f471808abcd',因为后顾断言之后没有任何字符,在匹配最大字符串之后,还可以检查最后4位校验。

  5. 一次性也可以和前瞻断言配合用来指定目标字符串开始的有效匹配或截断字符串,且包含断言子组。比如(?=abcd)(?>.*),匹配’1234abcd456789‘,返回值是abcd456789.(?=abcd)获取的是一个位置,.*获取的是这个位置之后的所有字符。

  6. 断言子组可以和条件子组配合使用,比如(?(?=[a-z])\d{2}[a-z]{3}|\d{2}\d{2}),当结尾是小写字母时,匹配ddwww,或当条件不满足时,匹配dddd.

  7. 断言子组不能用量词修饰,因为对同一件事做多次断言是没有意义的.