用 perl 统计分析日志应用实战

七阶子
14 min, 2719 words

归类: 脚本运用

标签: perl

文本处理还是 perl 强,这是它发家的基本盘。我自学会 perl 后,每当有文本处理需求, 第一想到的还是 perl 。比如现在我在日常工作中就会用它来做如下的事情:

  • 代码生成
  • 配置格式转换
  • 日志分析

本文讲一下用 perl 进行简单日志分析的实战。

背景问题:大凡服务都会写日志,当我们发现在生产日志上发现某行错误日志频繁打印时, 就要引起警惕。最初的观察可能只要用 grep 类工具抽取相应日志行,有个直观的印象。 但如果要提取每行日志的关键信息,比如订单号、客户号或设备号之类,并根据该关键字 统计频度呢?那简单的 grep 工具可能就不好使了。

perl 脚本简单实现

但凡掌握一种脚本语言,应该都可以满足此需求。而我选择用 perl ,它是专为处理文本 诞生的脚本。先给出示例脚本 log-analyse.pl

#! /usr/bin/env perl
# Usage: ./log-analyse.pl *.log
use strict;
use warnings;

my $pattern = 'context sentence with key: (\S+)';
my $log = {};

while (<>) {
	chomp;
	if (/$pattern/) {
		my $key = $1;
		++$log->{$key};
	}
}

foreach my $key (keys %$log) {
	print "$key\t$log->{$key}\n";
}

前面两行是注释,可忽略,第一行 #! 是 Linux 惯用的启动器注释。后面两行 use 是开启警告与严格语法的意思,在写简单 perl 脚本可能用不上,但强制加上,能助你写 出更易读、更具可维护性的 perl 程序。

然后定义了一个正则表达式变量,$pattern 。其实也可以直接写在后面 while if // 里面,但提出来先定义一个变量更好些,后面要改成搜查其他日志的正则表达式更容易。 然后预定义了一个 hash 引用变量 $log ,作为保存解析日志的容器。

第三段的 while (<>) 循环是整个脚本的核心。其中 <> 叫行读取器,是 perl 在分 析文本文件的一个惯用法。在 <> 空括号里面其实可以接受一个文件句柄,比如 <STDIN> 就是读取标准输入的意思。如果留空,perl 的默认解释是:

  • 如果 perl 脚本启动时没有文件名参数,由读取标准输入,即等效 <STDIN>
  • 如果脚本启动有文件参数,则打开该文件,读取该文件的内容行
  • 脚本也可能接受多个文件参数,则依次处理每一个文件

这个逻辑与许多 linux 命令行工具的工作习惯是很契合的,既可以处理管道流,也可以 读文件,或多个文件。而其他语言要实现这个功能,还是有点费劲的。

因为 <> 行读取器是放在 while 循环中,所以它会读取标准输入或参数文件的每一 行。每读入一行,保存在默认变量 $_ 中。chomp 默认操作 $_ 变量,作用是去除 行尾的回车符 \n ,在这个示例中,它可用可不用。随后的 if 是匹配正则表达式, 默认绑定匹配的也是 $_ 变量。如果匹配成功,就将正则表达式的第一个分组保存在 $1 自动变量中。

因为我们在之前定义的正则表达中,将关键字存在第一个分组 () 中,所以也将 $1 再转存至局部变量 $key 中。再看之前定义的 hash 容器 $log ,就是为了保存一系 列 key-value 值的,值部分存个整数表示关键字在日志中出现的次数。perl 的标量是弱 类型,用 ++ 操作符就把操作数当成整数自增了,未初始化时就是 0 。

最后一段用 foreach 将保存在 $log hash 容器中的数据打印到标准输出,每一行是 关键字、制表符、频度次数。运行时可重定向保存,或拷至 excel 再分析。

使用方法

写完 perl 脚本,首先建议用 -c 命令行选项检查一下语法是否正确:

$ perl -c log-analyse.pl
log-analyse.pl syntax OK

如果脚本语法正确,会打印 syntax OK ,否则会打印编译错误信息,指导你去修改。 因为是解释型脚本语言,单独的编译检查不是必须的,每次运行时也还会先编译,能检查 出语法错误,未通过编译这步,自然也不会有后续影响。

在语法通过后,严谨使用前还要检查业务逻辑是否正确,我们可以使用小样本输入来检查 脚本行为是否符合预期,所以给脚本提供一个日志文件作为命令行参数,如:

$ ./log-analyse.pl 1.log
# output here

我们在脚本中是用 print 直接打印到默认的标准输出的,故直接观察输出结果是否合 理即可。如果感觉没业务逻辑没问题,那就可以在命令行参数写上实际要处理的文件了, 还可以用通配符 * 喂给许多日志文件,如:

$ ./log-analyse.pl *.log > output.txt

如果担心输出太多,可以重定向文件保存输出。

脚本可优化点讨论

perl 有许多隐式规则,以简化脚本的第一次编写,其中默认变量 $_ 是最常用的。如 果担心 $_ 变量不安全,尤其随着脚本逻辑复杂化后,不知道后续什么操作就把默认变 量 $_ 的值给覆盖了,那可以在 while 循环中读入每一行,立即将 $_ 赋值给自 定义的局部变量。如:

while (<>) {
	chomp;
    my $line = $_;
	if ($line =~ /$pattern/) {
        print "$line\n";
	}
}

这里为说明示意,把 if 里面的业务逻辑删减为原样打印,这就只相当于做了 grep 的工作,打印匹配行。因为前面使用 chomp 去除了换行符,所以在打印每一行时要加上 \n 。好像这是多此一举了,其实不然,因为你不知道读入文件本是 linux 换行符,还 是 windows 换行符,甚至可能没有换行符,比如文件的最后一行。chomp 的作用就是 归一化,去除行尾可能的换行符,然后在自己的业务中显式打印换行符,这是 perl 的惯 用法。但是要注意不能偷懒试图将前两行合并成一行,如:

my $line = chomp($_);

因为这样的话,$line 接收的就是 chomp 的返回值,表示该操作移除了多少个字符, 就不是你以为读入的(去除换行符之后的)文本行了。

最后,在输出 hash 时是无序的,可能按内部存储的任意顺序打印,但可以通过 sort 先排序以控制打印顺序。如:

foreach my $key (sort keys %$log) {
	print "$key\t$log->{$key}\n";
}

也可以逆序打印,只要加上 reverse 即可,如:

foreach my $key (reverse sort keys %$log) {
	print "$key\t$log->{$key}\n";
}

其实,以上的 keyssortreverse 都是 perl 的内置函数而已,但是在只有 一个参数时可以省略小括号,就像操作符作用于操作数,读起来也像英文句子。设计 perl 的作者是自然语言学家。现在很多国人期望有“中文编程”,其实像 perl 这种具有 “英文编程”风格的语言是值得参考的。

上面都是根据关键字排序(字符串字典序),如果要根据日志频度,即 hash 的值排序呢? sort 当然也是能接收自定义排序方法的,就相当于其他语言常用 lambda 传给 sort 作为可选参数。在 perl 中最简单的写法是这样:

foreach my $key (reverse sort {$log->{$a} <=> $log->{$b}} keys %$log) {
	print "$key\t$log->{$key}\n";
}

sort 与操作数即 keys 之间插入一个代码块 {} ,里面用 <=> 三路比较符 返回一个值,表示比较结果(类似 strcmp 的结果可能是 0 1 -1),用于排序依据。在 该代码块中,$a$b 就是自动变量,代表要比较的两个值,这里就是要比较的两 个 key 。如果比较判据比较复杂,不适合内联写在 {} 中,可以先定义函数,然后用 函数名替换这个代码块 {} 。可以为该函数(子过程)取个好听点的名字,使其代入后 读起来仍比较顺畅,如:

foreach my $key (reverse sort by_value keys %$log) {
    print "$key\t$log->{$key}\n";
}

sub by_value
{
    $log->{$a} <=> $log->{$b};
}

perl 的子过程不严格要求先定义再调用,只要同一个文件中有定义即可。

提取多字段报表

另一个常见需求,是多一行日志中提取多个字段信息,再打印出来。这个需求改起来也简 单,只要修改正则表达式,把要提取的信息用小括号分组出来,如:

while (<>) {
    if (/log conentxt f1:(\S+), f2:(\S+)/) {
        my $field1 = $1;
        my $field2 = $2;
        # todo: 对 $field1 $field2 可以作进一步处理
        print "$field1\t$field2\n";
    }
}

所以,问题的关键主要是正则表达式。perl 正则表达式是工业事实标准,其他许多语言 都会提供正则表达式库,有的还特意命名 perl 兼容的正则表达式库。在 perl 中,正则 表达式是内置的,正则表达式是第一类操作数,而不是通过库函数调用实现的,所以 perl 天生最适合处理基于正则表达式的文本处理。

这类需求可能就是 perl 诞生之初的本源需求,因为其全名就叫 Practical Extraction And Report Language,实用文本提取与报表语言。 如上简单的文本提取需求,还可以简洁地写成单行 perl 程序,形如:

$ perl -lpe 's/regexp/replace/' < input-files-or-stdin

其实 -l 选项相当于隐含 while(<>){} 循环,-e 参数就是写在该循环大括号里的 语句,-p 选项再隐含每个循环末尾执行 print 语句打印当前行也即 $_ 。如果用 -n 选项代替 -p 则不会自动 print $_ ,而是由 -e 参数自己按需打印。

网上可以搜到很多精妙的单行 perl 完成复杂任务,可完美替代 shell+sed+awk 的工作。 但个人而言,还是更推荐写 10 行到 100 行规模的 perl 脚本程序,可加注释、扩充, 更具可维护性。

结语

本文介绍了最擅长文本处理的脚本语言 perl 在分析日志的一个实战运用,可供有 perl 基础的读者参考。对完全不熟悉 perl 者,这里的脚本应该可直接运行,但也要了解正则 表达式,把 $patter 的定义改成自己想要的正则表达式。

源代码:log-analyse.pl