Golang实现“不包含”正则表达式功能

大哥大姐哪位知道,Golang实现“不包含”正则表达式功能
最新回答
深蓝菇凉

2024-09-27 00:05:19

需求背景

业务开发中,需要对错误日志进行监控,目前我们的错误日志格式是error......,那么,我们如何匹配到错误日志呢?

我们可以通过^error正则表达式来实现。

现在假设我们想实现一个如下功能:对偶然出现的超时问题进行过滤,不匹配到这个日志,那么我们应该如何实现呢?

知识点普及基本语法

既然我们要通过正则表达式的方式来过滤日志,我们应该对正则表达式中的一些语法有一定了解。

字符描述^匹配输入字符串的开始位置,除非在方括号表达式中使用,当该符号在方括号表达式中使用时,表示不接受该方括号表达式中的字符集合。$匹配输入字符串的结尾位置。如果设置了RegExp对象的Multline属性,则$也匹配\n或\r。要匹配$字符本身,请使用\$。()标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。要匹配这些字符,请使用\(和\)。*匹配前面的子表达式零次或多次。要匹配*字符,请使用\*。+匹配前面的子表达式一次或多次。要匹配+字符,请使用\+。.匹配除换行符\n之外的任何单字符。要匹配.,请使用\.。?匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。要匹配?字符,请使用\?。\|指明两项之间的一个选择。要匹配\|,请使用\\|。零宽断言

该部分内容参考正则表达式零宽断言详解。

适用场景

在介绍零宽断言的概念时,我们先来看一下会用到零宽断言的场景。

有时候我们需要捕获的内容前后必须是特定内容时,但又不捕获这些特定内容的时候,零宽断言就起作用了。

例如:有如下两个字符串:abcdefg、bcdefg。我们想要找到前面是abc的de字符串。这时就会用到了零宽断言。

概念

零宽断言正如它的名字一样,是一种零宽度的匹配,它匹配到的内容不会保存到匹配结果中去,最终匹配结果只是一个位置而已。

作用是给指定位置添加一个限定条件,用来规定此位置之前或者之后的字符串必须满足限定条件才能使正则中的子表达式匹配成功。

注意:这里所说的子表达式并非只有用小括号括起来的表达式,而是正则表达式中的任意匹配单元。

str:="abZW863"pattern:="/ab(?=[A-Z])/"regexp.MatchString(pattern,str)

在以上代码中,正则表达式的语义是:匹配后面跟随任意一个大写字母的字符串ab,最终匹配结果是ab,因为零宽断言(?=[A-Z])并不匹配任何字符,知识用来规定当前位置的后面必须是一个大写字母。

str:="abZW863"pattern:="/ab(?![A-Z])/"regexp.MatchString(pattern,str)

以上代码中,正则表达式的语义是:匹配后面不跟随任意一个大写字母的字符串ab。正则表达式没能匹配任何字符,因为在字符串中,ab的后面跟随有大写字母。

零宽断言是用来查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像\b、^、$那样用于指定一个位置,这个位置应该用于满足一个条件(即断言)。因此它们也被称为零宽断言。断言用来声明一个应该为真的事实,正则表达式只有当断言为真时才会继续进行匹配。

(?=exp)也叫零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式exp。

(?<=exp)也叫零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式exp。

负向零宽断言

如果我们想要的功能是:确保某个字符没有出现,但是不想去匹配他,这应该怎么办呢?就是开头提到的问题。这就是负向零宽断言。

零宽度负预测先行断言(?!exp)断言此位置的后面不能匹配表达式exp。

同理,有了后面的不匹配,就会有前面的不匹配,即(?<!exp):零宽度负回顾后发断言。

总结

(?=exp):零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式exp。

(?<=exp):零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式exp。

(?!exp):零宽度负预测先行断言,断言此位置的后面不能匹配表达式exp。

(?<!exp):零宽度负回顾后发断言来断言此位置的前面不能匹配表达式exp。

“不包含”功能实现

通过上面的零宽断言学习,我们知道了如果要实现查询出以error开头但又不包含timeout的日志,可以使用如下正则表达式:"^error((?!timeout).)*$"。

下面我们来逐步分析一下这个正则表达式:

?!timeout:是零宽度负预测先行断言,断言此位置的后面不能匹配表达式timeout。

(?!timeout).:会向前查找,看看前面是不是没有timeout字符串,如果没有(是其它字符串),那么.(点号)就会匹配这些其它字符。该表达式不会捕获任何的字符,只是判断。

((?!timeout).)*:表达式(?!timeout).只会执行一次,所以,我们将这个表达式用括号包囊成组(group),然后用*(星号)修饰——匹配0次或多次。

正则表达式测试

我们现在来测试一下,这个正则表达式是否可以正常工作。

首先我们来测试一下没有timeout的错误日志能否正常匹配:error1testnormalerror。如下,我们发现是可以正常匹配到的。

然后我们再来测试一下包含timeout的错误日志能够被过滤:error2testwithtimeouterr,如下,我们可以发现没有匹配到这条日志,这条日志被过滤掉了。这正好符合我们的需求。

go代码实现

那么我们能否在go代码中实现这个功能呢。我们来试验一下。

packagemainimport("fmt""regexp")funcmain(){pattern:="^error((?!timeout).)*$"error1:="error1testnormalerr"error2:="error2testwithtimeouterr"match1,err:=regexp.MatchString(pattern,error1)match2,err:=regexp.MatchString(pattern,error2)fmt.Printf("match:%v,err:%v\n",match1,err)fmt.Printf("match:%v,err:%v\n",match2,err)}

运行结果如下:

match:false,err:errorparsingregexp:invalidorunsupportedPerlsyntax:`(?!`match:false,err:errorparsingregexp:invalidorunsupportedPerlsyntax:`(?!`

我们可以看到,结果抛错了,错误信息为:invalidorunsupportedPerlsyntax:(?!。这说明go是不支持零宽断言的。

通过文档我们也可以发现,零宽断言是不支持的。

语法介绍(?=re)beforetextmatchingre(NOTSUPPORTED)(?!re)beforetextnotmatchingre(NOTSUPPORTED)(?<=re)aftertextmatchingre(NOTSUPPORTED)(?<!re)aftertextnotmatchingre(NOTSUPPORTED)

那么该怎么办呢?别慌,前辈们已经造好轮子了,那就是regexp。

关于regexp的功能,其中一段介绍如下:

Regexp2isafeature-richRegExpengineforGo.Itdoesn'thaveconstanttimeguaranteeslikethebuilt-inregexppackage,butitallowsbacktrackingandiscompatiblewithPerl5and.NET.You'lllikelybebetteroffwiththeRE2enginefromtheregexppackageandshouldonlyusethisifyouneedtowriteverycomplexpatternsorrequirecompatibilitywith.NET.

Regexp2是Go的一个功能丰富的RegExp引擎。它不像内置的regexp包那样有固定的时间保证,但是它允许回溯,并且与Perl5和.net兼容。使用regexp包中的RE2引擎可能会更好,只有在需要编写非常复杂的模式或需要与.net兼容时才应该使用它。

关于regexp和regexp2的部分比较如下:

Categoryregexpregexp2Catastrophicbacktrackingpossibleno,constantexecutiontimeguaranteesyes,ifyourpatternisatriskyoucanusethere.MatchTimeoutfieldPython-stylecapturegroups(?P<name>re)yesno(yesinRE2compatmode).NET-stylecapturegroups(?<name>re)or(?'name're)noyescomments(?#comment)noyesbranchnumberingreset(?|a|b)nonopossessivematch(?>re)noyespositivelookahead(?=re)noyesnegativelookahead(?!re)noyespositivelookbehind(?<=re)noyesnegativelookbehind(?<!re)noyesbackreference\1noyesnamedbackreference\k'name'noyesnamedasciicharacterclass[[:foo:]]yesno(yesinRE2compatmode)conditionals(?(expr)yes\|no)noyes

从上面关于regexp2的介绍和同regexp的比较我们可以发现,regexp2是支持零宽断言功能的。那么我们按照文档来试一试。

packagemainimport("fmt""github.com/dlclark/regexp2""regexp")funcmain(){pattern:="^error((?!timeout).)*$"error1:="error1testnormalerr"error2:="error2testwithtimeouterr"match1,err:=regexp.MatchString(pattern,error1)match2,err:=regexp.MatchString(pattern,error2)fmt.Printf("match1:%v,err:%v\n",match1,err)fmt.Printf("match2:%v,err:%v\n",match2,err)reg,err:=regexp2.Compile(pattern,0)iferr!=nil{fmt.Printf("reg:%v,err:%v\n",reg,err)return}match3,err:=reg.FindStringMatch(error1)match4,err:=reg.FindStringMatch(error2)fmt.Printf("match3:%v,err:%v\n",match3,err)fmt.Printf("match4:%v,err:%v\n",match4,err)}

运行结果如下:

match1:false,err:errorparsingregexp:invalidorunsupportedPerlsyntax:`(?!`match2:false,err:errorparsingregexp:invalidorunsupportedPerlsyntax:`(?!`match3:error1testnormalerr,err:<nil>match4:<nil>,err:<nil>

可以看到match3是正常匹配到的。测试成功。

结语

从上面我们可以看到,regexp2的功能还是很强大的,如果我们需要实现复杂的正则表达式,推荐使用。

但是有一点我们需要注意:那就是regexp2的时间复杂度无法保证。从Itdoesn'thaveconstanttimeguaranteeslikethebuilt-inregexppackage中我们也可以看到,他不像官方包会保证确定的时间复杂度。因此,在生产环境中使用时,一定要慎重!!!!

参考文档

Golang正则表达式不支持复杂正则和预查问题解决

正则表达式零宽断言详解