其实只要你花上几分钟了解哪怕只是awk 10%的语法,它都能梦幻般的提高你操作文本文件的能力。下面就让我来教你一些最有用的操作---注意,不是“最基础的”,只要你愿意花上5分钟时间练习下这些技巧,我相信你就将会领悟到我认为这门语言最为有趣的东西。
awk实际上是一门非常有趣的小型编程语言,它设计的主要目的就是为了处理字符串。
让我们先来看个例子,假设我们有一个小文件(logs.txt)看起来像是下面这样,很简单,它只有2行带有IP地址的log信息:
[root@butbueatiful ~]> cat logs.txt
07.46.199.184 [28/Sep/2010:04:08:20] "GET /robots.txt HTTP/1.1" 200 0 "msnbot"
123.125.71.19 [28/Sep/2010:04:20:11] "GET / HTTP/1.1" 304 - "Baiduspider"
这是两条由Apache生成的日志信息,很简单,它们显示了Bing和Baidu的爬虫昨天到访过我的网站。
awk同其它命令行程序(比如grep)一样,都是从stdin读取输入并写到stdout,所以你可以很容易的通过管道使用它,Awk的使用也很简单,你唯一需要关心的它令后面的那个字符串参数: awk '{print $0}'
大部分awk程度都是以{开头,并以}结束的,{}中包含的命令会在输入中的每一行上执行,大部分awk程序都会打印些什么东西,上面的命令会原封不动的将它读到的输入再打印出来,并添加一个换行,$0代表一整行。所以这个程序很典型---它除了将输入拷贝到输出,其它什么都没做。
awk还会根据输入行中的空白字符(空格,tab)自动将行切分为字段,并自动处理连着的分隔字符,你可以通过$1,$2,$3等来引用分割后的字段。
[root@butbueatiful ~]> echo 'this is a test' |awk '{print $3}'
a
[root@butbueatiful ~]> awk '{print $1}' logs.txt
07.46.199.184
123.125.71.19
很简单,并且很有用,不是吗?但是有时我需要从字符串的结尾开始打印,这时我可以使用一个名为NF的特殊变量,它包含了当前行的字段数,因此我可以使用$NF来打印最后一个字段,我也可以通过这个值来反向查找某个字段,同时我还可以在一行打印多个值:
[root@butbueatiful ~]> echo 'this is a test' |awk '{print $NF}'
test
[root@butbueatiful ~]> awk '{print $1, $(NF-2)}' logs.txt
07.46.199.184 200
123.125.71.19 304
更进一步---现在你可以看到,我们可以将日志文件分割,并只打印我们关心的部分。另一个很有用的变量就是NR,这个变量表示当前正在处理的输入的行号。在我演示NR的同时,我还想展示一下如何使用print来格式化输出,你可以使用逗号来分隔print的多个参数,它们会被转化为空格,但是下面的例子我没有使用逗号,所以结果中也没有插入空格:
[root@butbueatiful ~]> awk '{print NR ") " $1 " -> " $(NF-2)}' logs.txt
1) 07.46.199.184 -> 200
2) 123.125.71.19 -> 304
很强大,并且一点也不难,对吧,如果你喜欢,你还可以使用printf函数,或许你对它更熟一些。现在,问题来了,不是所有字段都是以空格作为分隔符的,比如下面这个:
[root@butbueatiful ~]> awk '{print $2}' logs.txt
[28/Sep/2010:04:08:20]
[28/Sep/2010:04:20:11]
日期字段使用了/和:作为分隔符,当然我可以通过一条awk命令来完成这个操作,但是我想向你演示一种更简单也是你更为熟悉的方式:Unix管道,下面我要做的就是通过管道使用另外一条awk命令来根据冒号对日期进行分割,要做到这一点,我的第二个awk命令需要使用2个{},不过我暂时不想对这个多做解释,你只要看看我是如何做的就行了:
[root@butbueatiful ~]> awk '{print $2}' logs.txt | awk 'BEGIN{FS=":"}{print $1}'
[28/Sep/2010
[28/Sep/2010
我为FS(也就是字段分隔符)指定了一个不同的值,也就是":",然后打印出了第一个字段,现在没有时间了,只有日期!如果你不想看到输出中的那个[,最简单的方法就是使用sed,或许你早就知道了:
[root@butbueatiful ~]> awk '{print $2}' logs.txt |awk 'BEGIN{FS=":"}{print $1}' |sed 's/\[//'
28/Sep/2010
28/Sep/2010
使用相同的技巧,我还可以进一步根据'/‘字符来切分日期,但是我认为你已经掌握这个技巧了,所以我就不罗嗦了,下面,让我们来试着加入一点点逻辑 判断,如果我只想得到状态为200的行,我可以使用grep,但是如果我想得到IP地址中包含200的行,或者是日期在2000年以后的行,那我就需要先 使用awk,然后在通过grep来查找了,不过的问题是输出结果没了上下文,还好,Awk支持if条件判断,所以我们可以像下面这样:
[root@butbueatiful ~]> awk '{if ($(NF-2)=="200") {print $0}}' logs.txt
07.46.199.184 [28/Sep/2010:04:08:20] "GET /robots.txt HTTP/1.1" 200 0 "msnbot"
现在,只有符合条件的行才会被输出,这个if语句实在是太熟悉不过了,应该不需要我做过多的解释,下面让我通过一个蠢例子向你演示下awk如何实现跨行保存状态,假设我想得到这个文件中所有HTTP相应的状态字段的和,虽然我实在想不出有有什么理由这样做,但是为了演示字段求和,就让我们先忘掉这一 点吧,要做到这一点,我只需要创建一个变量就okay了:
[root@butbueatiful ~]> awk '{a+=$(NF-2); print "Total so far:", a}' logs.txt
Total so far: 200
Total so far: 504
很显然,这是有问题的,大部分情况下,我并不需要中间值,只需要最后的结果就可以了,当然我可以使用tail -n1,但是还有一种更好的方式,我可以使用END关键字告诉awk只在最后一行调用print:
[root@butbueatiful ~]> awk '{a+=$(NF-2)}END{print "Total:", a}' logs.txt
Total: 504
如果你想要了解更多关于awk的知识,有几本好书以及大量的线上资源可供你参考,只需要花上一些闲暇时间,你就可以很容易的学到关于awk的一切, 但是要熟练使用它,还是有些挑战的,因为它的编码方式实在有点特别---你实际上是在写一个for循环的内部实现,仔细想想,这其实有点 MapReduce 的感觉,只是一开始会让人有些迷惑。
附:awk中的特殊变量
变量 描述
$n 当前记录的第n个字段,字段间由FS分隔。
$0 完整的输入记录。
ARGC 命令行参数的数目。
ARGIND 命令行中当前文件的位置(从0开始算)。
ARGV 包含命令行参数的数组。
CONVFMT 数字转换格式(默认值为%.6g)
ENVIRON 环境变量关联数组。
ERRNO 最后一个系统错误的描述。
FIELDWIDTHS 字段宽度列表(用空格键分隔)。
FILENAME 当前文件名。
FNR 同NR,但相对于当前文件。
FS 字段分隔符(默认是任何空格)。与-F功能一样
IGNORECASE 如果为真,则进行忽略大小写的匹配。
NF 当前记录中的字段数。
NR 当前记录数。
OFMT 数字的输出格式(默认值是%.6g)。
OFS 输出字段分隔符(默认值是一个空格)。
ORS 输出记录分隔符(默认值是一个换行符)。
RLENGTH 由match函数所匹配的字符串的长度。
RS 记录分隔符(默认是一个换行符)。
RSTART 由match函数所匹配的字符串的第一个位置。
SUBSEP 数组下标分隔符(默认值是\034)。
阅读(1900) | 评论(0) | 转发(0) |