文本处理还是 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";
}
其实,以上的 keys
、sort
与 reverse
都是 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