谈谈日志解析中的正则表达式
正则表达式算是编程的基础知识,日常功能开发中也经常会用到。
以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配置,日志解析的正则表达式自动生成就很简单了。