awk命令详解

awk 其名称的由来很简单,就是由其三个创始人Alfred Aho 、Peter Weinberger 和 Brian Kernighan 姓氏的首个字母组成。很多人都说它是一个效率神器,以前也多多少少用过它一些简单的用法。后来python一上手,pandas一用上,反而忘了它。正所谓技多不压身,这么高大上的程序语言(速度快,常用命令简单, 效率高),一定要学会它。所以这篇文章特将awk的基本语法,常用的命令以及相对应的命令记录下来,以备需要时直接复制使用。

awk语言的最基本功能是在文件或者字符串中基于指定规则浏览和抽取信息,awk抽取信息后,才能进行其他文本操作。awk脚本通常用来格式化文本文件中的信息。awk是以文件的每一行为处理单位的,即其对所接收文件或者其他文本内容的一行执行相应的命令,来处理文本。

1
awk '{命令}' file1, file2, ...

或者

1
其他命令的输出 | awk '{命令}'

举个例子:

1
awk '{print $1}' demo.txt

awk的命令一般写在花括号里,但是花括号并不是必须的,某些情况下它可以省略,或者压根不需要。下面看具体讲解和实例。

基本用法

awk内置变量

前面说了,awk一般以行为单位进行处理,下表中的记录行即表示文件的一行,这是默认情况下以换行符作为判断为一行的标志,当然也可以手动修改以其他字符作为换行标志。下表中的字段可以理解为使用分隔符分隔记录行后的每一个元素。
以下是awk内置变量及其含义:

变量名 含义
$0 当前记录行
$1 ~ $n 当对当前记录行进行分隔时,表示分隔后第几个字段
FS 字段分隔符,默认是空格
RS 记录行分隔符,默认为换行符
NF 对记录行进行分隔后,字段的个数
NR 已经读出对记录数,从1开始,多个文件时继续累加计数
FNR 已经读出对记录数,从1开始,多个文件时每个文件单独计数
ORS 输出时的记录行分隔符, 默认是换行符
OFS 输出时的字段分隔符, 默认是空格

$n、-F、FS、RS

首先看 $0 ~ $n 和 -F 的例子:

1
2
3
4
5
6
7
8
9
10
>> cat awk_test.txt
a,b,c,d
e,f,g,h

>> awk '{print $0}' awk_test.txt
a,b,c,d
e,f,g,h
>> awk -F ',' '{print $1,$2,$3,$4}' awk_test.txt # 实例2
a b c d
e f g h

这里的 ‘-F’ 命令用于定义按什么进行分隔,功能同python里的split。‘$0’ 表示整个记录行(实例1),‘$1,$2,$3,$4’表示分隔后的每个元素。

再继续来看FS和RS

1
2
3
4
5
6
7
8
9
>> awk -F ',' '{print $1 FS $2,$3,$4}' awk_test.txt # 实例3
a,b c d
e,f g h

>> awk -F ',' '{print $1 RS $2,$3,$4}' awk_test.txt # 实例4
a
b c d
e
f g h

实例3中,FS的值已经由‘-F’命令赋值成‘,’了;实例4中,RS默认为换行符,所以输出结果进行了换行。

如果想用多个分隔符分隔记录行,只需要将多个分隔符放在 [分隔符1 隔符2 ..] 中,如果想用一个或者多个相同分隔符进行分隔,可以写成 [分隔符1 隔符2 ..]+ 。继续看例子:

1
2
3
4
5
>> echo 'I am Poe,my qq is 0000001' | awk -F '[" ",]' '{print $3 " " $7}'
Poe 0000001

>> echo 'I am Poe,,my qq is 0000001' | awk -F '[" ",]+' '{print $3 " " $7}'
Poe 0000001

不解释了,已经很清楚了。

NF、NR、FNR

先看 NF和NR

1
2
3
4
5
6
7
>> cat awk_test.txt
a,b,c,d
e,f,g,h

>> awk -F ',' '{print NR") "$0"t size is "NF}' awk_test.txt
1) a,b,c,d size is 4
2) e,f,g,h size is 4

可以看到,结果中有行号,即为NR, 输出结果中的 4 即为字段数。
再看以下FNR及其和NR的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>> cat file.txt
aaaaaa
bbbbbb

>> awk -F ',' '{print NR") "$0}' awk_test.txt file.txt
1) a,b,c,d
2) e,f,g,h
3) aaaaaa
4) bbbbbb

>> awk -F ',' '{print FNR") "$0}' awk_test.txt file.txt
1) a,b,c,d
2) e,f,g,h
1) aaaaaa
2) bbbbbb

>> awk -F ',' '{if (NR==FNR) print $0}' awk_test.txt file.txt
a,b,c,d
e,f,g,h

很清楚了吧

OFS、ORS

同样的看个例子:

1
2
3
4
5
6
7
8
9
>> echo '1 2 3' | awk 'BEGIN {OFS="|"} {print $1,$2,$3}' # 实例5
1|2|3

>> cat awk_test.txt | awk -F ',' '{print $1$2}'
ab
ef

>> cat awk_test.txt | awk -F ',' 'BEGIN {ORS="---"} {print $1$2}' # 实例6
ab---ef---

这里出现了一个新的语法,BEGIN,其作用是定义在命令执行之前需要执行的操作,在实例5中,在命令执行之前将输出时的字段分隔符赋值成了‘|’。实例6同理。

BEGIN、END 命令、运算符

BEGIN 和 END 命令

在许多编程情况中,需要在 awk 开始处理输入文件中的文本之前执行初始化操作。这个时候 可以定义一个 BEGIN 块。

因为 awk 在开始处理输入文件之前会执行 BEGIN 块,因此它是初始化 各种变量(包括内置变量,全局变量)的极佳位置。
相对应的,awk 还提供了另一个特殊块,叫作 END 块。 awk 在处理了输入文件中的所有行之后执行这个块。通常, END 块用于执行最终计算或打印应该出现在输出流结尾的摘要信息。

下面这个例子的作用是统计某个文件夹下的文件占用的字节数:

1
2
>> ll |awk 'BEGIN {size=0} {size=size+$5} END{print "[end]size is ",size/2014/1024, "M"}'
[end]size is 3.3 M

这样一个简单的例子信息量还是挺丰富的。首先可以看到所有的命令都在 ‘ ’ 里,BEGIN and END 以及记录行正常的操作部分,都有单独的代码块,用 {} 区分。另外我们可以看到,awk支持自定义变量,还可以进行各种运算。实际上awk也是一个轻量级的编程语言。这里代码“BEGIN {size=0}”可以省略,awk默认size=0.

awk运算符

既然awk也算是一个编程语言,那自然支持各种运算。如下表所示:

运算符 解释
=、+、-、+=、-=、=、/=、%=、*=、++、–等 赋值及数学运算
||、&& 逻辑或、逻辑与
<、<=、>、>=、!=、== 关系运算符
?: 三目运算符
in 数组中是否存在某键值

随便举个例子,其余的看看就好:

1
2
3
4
>> awk 'BEGIN{a="b";print a=="b"?"ok":"err"}'
ok
>> awk 'BEGIN{a="b";print a=="c"?"ok":"err"}'
err

awk 数组和循环

awk的循环包含while、do…while、for循环。这里不介绍了。具体可看链接:https://www.cnblogs.com/ginvip/p/6352157.html

说说数组。awk的数组像极了python里的dict。有数组的地方,常常也有循环,当然这不是绝对的。
这次的实例使用一个真实的log文件,先看看log前2行长什么样(涉及到实际业务,做了一些处理):

1
2
3
>> cat awk_log.log | head -n 2
2019-10-22 18:11:42,726 feature_name_unify.py[line:99] ERROR: d_type, origin_name, sample_type, exam_name: exam,awk,linux,awk命令简介
2019-10-22 18:11:42,727 feature_name_unify.py[line:99] ERROR: d_type, origin_name, sample_type, exam_name: exam,grep,linux,awk命令简介1

整个log文件有0.18亿行。现在想统计按逗号分隔后倒数第三个位置会出现哪几种字符,及其频数。为了简单起见,用前20行做统计:

1
2
3
4
5
6
>> cat awk_log.log | head -n 20 | awk -F ',' '{a[$6]++} END {for (x in a) print x "t" a[x]}'
awk 7
grep 6
seed 3
ls 3
pwd 1

这个例子先将这个log文件的前20行输出,以便awk处理;在使用awk处理时,先使用逗号进行分隔;后面对记录行的每一个操作都写在单引号 ‘’ 里,每一个{}是一个代码块;对于代码块 {a[$6]++} ,a表示一个数组,这个数组可以理解为python里的dict,$6是其索引;初始时,a[$6] = 0,后面随着处理的记录行增加,每来一个相同的索引,就加1;END {for (x in a) print x “t” a[x]}在处理完所有记录行后执行,此时用一个循环,打印出数组a的索引及其值。就如同打印出python字典里的key和value。

再来一个更复杂的例子,统计一个公司每个部门的人数及具体姓名:

1
2
3
4
5
6
7
8
9
10
11
>> cat company.csv | head -n 3
部门,姓名,性别
技术研发部,张三,男
财务部,韩梅梅,女

>> cat company.csv | head -n 16 | awk -F ',' '{if (NR==1) next}{a[$1]++;b[$1]=b[$1]","$2} END {for (x in a) print x "t" a[x] "t" b[x}'
技术研发部 5 ,张三,李四,王五,李雷,tom
财务部 4 ,张一,李二,王三,Bob
商务部 3 ,张二,李三,王四
战略部门 2 ,张五,王六
总裁办 1 ,三一

这里新出现了一个语法 ‘next’, 它的作用等同于 continue。这个命令出现,就不会执行后面的命令了。这里它的作用是跳过第一行,因为第一行是csv文件的header。这个例子看起来毫无意义,但是在实际处理中,特别是一些log文件,数据量几千万到亿级,用python处理代码写起来也很简单,但是论速度,肯定远远不及awk。awk处理起来很快,比常用的编程语言都要快。

3. awk的正则表达式

正则表达式,不管什么语言,语法都是基本一样的。在实际工作中也用过不少,却始终不够熟练,甚至有点蒙。awk我也是如此。这一部分 不详细讲解了(觉得自己讲不明白),但是写2个简单例子,也算对得起这一节。
下图是awk正则表达式:

awk的正则表达式写在2个 / 之间:

1
>> ls -l | awk '/^d/{print $9}'

这个例子是输出当前目录下所有的文件夹的名称(不写结果了,可自行尝试看结果)。

1
>> awk -F ':' '$5~/root/{print $0}' /etc/passwd

这个例子是以分号作为分隔符,匹配第五个字段包含‘root’的行

多文件操作及awk运行shell命令

awk 多文件操作

文章开始的时候就说过,awk的基本像是如下:

1
awk '{命令}' file1, file2, ...

实际上awk是按文件循序逐行读取的,读完file1, 接着读取file2,以此类推.
多文件处理的一个常用操作就是多文件特定内容的merge。下面用2个例子演示一下,横向合并2个文件和根据某一列或者多列合并2个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>> cat awk_test.txt
a,b,c,d
e,f,g,h

>> cat awk_test1.txt
2,3,4,a
1,2,3,e

# 横向合并2个文件
>> awk -F ',' 'NR==FNR{a[NR]=$0;next}{if (a[FNR]) print a[FNR] FS $0}' awk_test.txt awk_test1.txt # 实例7
a,b,c,d,2,3,4,a
e,f,g,h,1,2,3,e

# 根据某一列合并2个文件
>> awk -F ',' 'NR==FNR{a[$1]=$0;next}{if (a[$4]) print a[$4] FS $1 FS $2 FS $3}' awk_test.txt awk_test1.txt # 实例8
a,b,c,d,2,3,4
e,f,g,h,1,2,3

这2个例子中的简单命令直接实现了Pandas的merge、concat和join操作,关键是你可以不用安装任何包,配置任何环境。更可贵的是,速度非常快。
通常,使用 NR==FNR 条件判断是否正在读取的在第一个文件。在实例7中,记录行数为索引,将awk_test.txt文件的数据存在数组里,当读到第二个文件,对于每一个记录行,先打印数组里存的第一个文件的数据,再打印第二个文件的内容,就实现了横向拼接。实例8原理基本相同。

awk 运行shell命令

使用awk运行shell命令的基本格式是:

1
awk '{system("shell 命令")}'

需要注意一点的是system里的shell命令需要写成字符串,即写在双引号里。
来一个最简单的例子:

1
2
3
>> awk '{system("echo hello world")}' awk_test.txt
hello world
hello world

可引导echo 命令运行成功。
接下来再看一个很实用的昨天,批量修改文件名.下面这个例子给除了black_test和已有后缀以外的文件名后面加上相同的后缀.txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>> ls # 查看文件或者文件夹名称
test0 test1 test2 test3 test4.txt black_test

>> ls | grep -vE "txt|black" | awk '{system("mv " $0 " " $0 ".txt")}' # 方法1
>> ls
test0.txt test1.txt test2.txt test3.txt test4.txt black_test

>> ls | egrep -v "txt|black" | awk '{system("mv " $0 " " $0 ".txt")}' # 方法2
>> ls
test0.txt test1.txt test2.txt test3.txt test4.txt black_test

>> ls | awk '$0 !~/txt|black/ {system("mv " $0 " " $0 ".txt")}' # 方法3
>> ls
test0.txt test1.txt test2.txt test3.txt test4.txt black_test

这里涉及到grep的知识,列在下面:

  • grep -E 用来扩展选项为正则表达式,如果使用了grep 命令的选项-E,则应该使用 | 来分割多个pattern

  • grep -v 实现反向操作。例如方法1中命令 ls | grep -vE “txt|black” 实现了搜索名称中既不包含txt也不包含black的文件。

  • egrep 等同于‘grep -E’

    看了上面的grep的简介,方法1和方法2实际上是一样的。再来看后面的 “mv “ $0 “ “ $0 “.txt” 部分。这里假如$0 = “a”, 那么”mv “ $0 “ “ $0 “.txt”=”mv a a.txt”.这里需要注意的是空格,一定要记得打上。

    再来看方法3,既然awk也有正则匹配, 那就完全可以省去grep,直接用awk的正则去筛选满足条件的文件名。命令 ‘$0 !~/txt|black/‘ 即实现了相同的效果。可以看一下上一节的规则。

到这里,over!有不对的地方,欢迎提出来。