正则表达式算是编程的基础知识,日常功能开发中也经常会用到。

以Java为例,简单的一个String.format就会用到Pattern.match。

Nginx的location配置,sed、grep等常用的linux命令也会用到正则。

还有很多类似正则表达式的应用,比如Spring的PathURI解析。

日志解析中的正则表达式

日志解析领域,是重度依赖Regex的。一提到正则就会有人说性能问题,耗Cpu。

的确,由于正则的书写方式不合理,导致的性能问题非常之多,甚至还有死循环的bug。

能写正则和写好正则之间需要大量的实践积累,也需要了解非常多的背景知识。

去年做过一个日志采集工具,其中就用到了正则来解析NginxLog,性能调的还可以。

正则表达式的几个基础概念

1、DFA和NFA引擎、回溯

2、贪婪和非贪婪

3、固化分组

4、量词匹配、优先匹配

具体概念就不展开讲了,自行查阅,提供2个blog可以看看。

引擎贪婪与非贪婪

繁琐啰嗦的表达式

为了解析一行NginxLog,你可能会把正则写成这样:

"([%d]+/[%a]+/[%d]+:[%d]+:[%d]+:[%d]+ %+0800)" ([%d|%.]+) (.-) ([%d|%.]+) ([%a|%-]+) ([%d]+) "([http|https]+)://([^"]*)" ([%d]+) ([%d]+) "([^"]*)" "(.*)"

精确的提供了每个字段的类型、长度等等,这样的表达式性能很好,但是看起来非常的啰嗦。

有人为了省事,会大量用(.*)来替代表达式中的明确的类型和长度信息。

简单测试一下就会发现(.*)的性能惨不忍睹。主要是因为大量的贪婪回溯,导致性能下降。

注意:最后一个分组字段使用(.*)是非常合理的。

简化一下

用(.*)来简化性能不好,那有没有性能好,又简单的表达式呢?可以试试非贪婪模式。

"(.-)" (.-) (.-) (.-) (.-) (.-) "(.-)://(.-)" (.-) (.-) "(.-)" "(.*)"

这个看起来是不是非常清晰,一个坑一个字段,最后一个分组仍然使用贪婪模式。

看到这里是不是觉得以后正则表达式有可能自动生成了,不需要烧脑完成。

这个正则的语法是以Lua为例的,其他语言也有类似的,只是符号不一样而已。Java是(.*?)

这个表达式的性能比第一个啰嗦的表达式性能略差,差距很小,基本是可以接受的。

继续深入

将简化的表达式进行一下整理替换,得到下面的:

"([^"]*)" ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) "([^:]*)://([^"]*)" ([^ ]*) ([^ ]*) "([^"]*)" "(.*)"

这里就看到比较重的分隔符含义了,一般来说我们的日志都有分隔符在的。

但列与列之间的分隔符并不一定一致,比如NginxLog,不知道为什么Nginx要把Log默认写成那个样子。

实际应用中,这种风格写法的Regex非常的实用。看过一些国内日志业务的解析功能也大概是这个思路吧。

性能上比非贪婪的要好,和第一个表达式的性能是一样的。

自动化

上面两个正则表达式看起来规律性都非常强,也就是说有极大的可能可以自动生成这个表达式。

"$time_local" $remote_addr $upstream_addr $request_time $request_method $status "$scheme://$host$request_uri" $request_length $body_bytes_sent "$http_referer" "$http_user_agent"

这个是Nginx的logformat配置,日志解析的正则表达式自动生成就很简单了。