常用的几个 AWK 命令

Note

这篇文章非原创,原文为: Getting Started With AWK Command ,本文是我对它的翻译和整理, 并非完全直译原文,很多地方的措辞改成了我自己理解后认为更加易懂的方式。

AWK 可以追溯到 Unix 时代,它是 POSIX 标准的一部分,所以在任意一个类 Unix 系统上都可以使用。

Perl 这样的多用途语言相比,AWK 有时会因为年代久远或者缺少功能而被人诟病,但我依然喜欢它,并且会在日常工作中使用。

它在处理数据文件的时候很有优势,有时候一行代码就能实现相对复杂的功能。

这篇文章就罗列了一些这样简洁但有用的 AWK 程序,它们都不超过 80 个字符。

即便你没有 AWK 的使用经验,通过练习这几个小程序也可以理解 AWK 的核心功能。

这个是我们后面练习 AWK 所使用的数据文件,练习使用时记得把第 8 行的 这里有8个空格 替换为8个空格。

这个数据文件的源文件地址在这里: 数据文件地址

 1CREDITS,EXPDATE,USER,GROUPS
 299,01 jun 2018,sylvain,team:::admin
 352,01    dec   2018,sonia,team
 452,01    dec   2018,sonia,team
 525,01    jan   2019,sonia,team
 610,01 jan 2019,sylvain,team:::admin
 78,12    jun   2018,öle,team:support
 8这里有8个空格
 9
10
1117,05 apr 2019,abhishek,guest

了解 AWK 中的 预定义变量自动变量

在写 AWK 程序的时候,常见的预定义变量和自动变量有:

  • RS : The input record separator, by default a newline,数据分隔符。AWK 一次处理一条 Record ,这个 RS 就是把输入数据流拆分为 Record 的分隔符。默认的 RS 是 换行符 ,所以如果不去指定这个 RS,一行就是一条 Record。

  • NR : The total number of input records seen so far,也就是 Record 的编号。如果 RS 是 换行符 ,那么 NR 就恰好等于数据的行号。

  • FS/OFS : The input/output field separator, a space by default,分隔单个词的字符,默认是 空格 。FS 是输入流的词分隔符,OFS 是输出流的词分隔符。比如你的数据是用逗号分开的 CSV 文件,那么用 AWK 读的时候,就要指定 FS 为逗号。

  • NF : The number of fields in the current input record,当前 Record 中的 Field 的数量。

man awk 查看 AWK 的手册,可以看到官方手册对这几个变量的全称。

其他还有很多变量,也可以查 在线手册 查看完整的介绍,只是日常使用知道这几个就差不多够了。

AWK 基本使用

打印所有 Record

这个例子可能没啥用,但是可以很好的介绍 AWK 的基本用法。

1awk '1 { print }' file.txt
 1# 输出
 2CREDITS,EXPDATE,USER,GROUPS
 399,01 jun 2018,sylvain,team:::admin
 452,01    dec   2018,sonia,team
 552,01    dec   2018,sonia,team
 625,01    jan   2019,sonia,team
 710,01 jan 2019,sylvain,team:::admin
 88,12    jun   2018,öle,team:support
 9这里有8个空格
10
11
1217,05 apr 2019,abhishek,guest

AWK 程序由一个或多个 pattern { action } 语句构成。 如果在处理当前的 Record 时, pattern 的计算结果是非零值, 也就是 AWK 中的 true , 语句块 { } 中的 action 就会被执行。 上面这个例子中, 1 是一个非零常量,也就是说在处理每条 Record 时的 pattern 都是 true , 所以 AWK 会对所有的 Record 执行 print 动作,这样就把所有数据内容都打印出来了。

另外, { print } 是 AWK 默认的 action ,所以其实这个例子里可以将其省略:

1awk 1 file.txt
2# 或
3awk '1' file.txt
 1# 输出
 2CREDITS,EXPDATE,USER,GROUPS
 399,01 jun 2018,sylvain,team:::admin
 452,01    dec   2018,sonia,team
 552,01    dec   2018,sonia,team
 625,01    jan   2019,sonia,team
 710,01 jan 2019,sylvain,team:::admin
 88,12    jun   2018,öle,team:support
 9这里有8个空格
10
11
1217,05 apr 2019,abhishek,guest

移除前几个 Record

可以让 AWK 从第几行开始打印,比如下面这个例子,我们指定行号为大于 1,即 NR>1 (首行的行号是 1)。

1awk 'NR>1' file.txt
2# 与这样等效
3awk 'NR>1 { print }' file.txt
 1# 输出
 299,01 jun 2018,sylvain,team:::admin
 352,01    dec   2018,sonia,team
 452,01    dec   2018,sonia,team
 525,01    jan   2019,sonia,team
 610,01 jan 2019,sylvain,team:::admin
 78,12    jun   2018,öle,team:support
 8这里有8个空格
 9
10
1117,05 apr 2019,abhishek,guest

Record 切片

有了上个例子,不难推导出对 Record 进行切片的写法:

1awk 'NR>1 && NR<4' file.txt
1# 输出
299,01 jun 2018,sylvain,team:::admin
352,01    dec   2018,sonia,team

可以看到只打印出了第 2、3 行。

移除仅含空白符的 Record

这里需要转换一下思路,我们的目的是:把仅含空白符(空格或制表符)的 Record 移除, 换成 AWK 的处理逻辑应该是:仅打印出非仅含空白符的 Record,有点拗口。

所以我们的 pattern 要能判断出每一个 Record 是不是仅含空白符。 这里我想到了用正则来判断。

匹配至少一个非空白符的正则是: \S+。 那么这个程序可以这样写:

1awk '/\S+/ { print }' file.txt
1# 输出
2CREDITS,EXPDATE,USER,GROUPS
399,01 jun 2018,sylvain,team:::admin
452,01    dec   2018,sonia,team
552,01    dec   2018,sonia,team
625,01    jan   2019,sonia,team
710,01 jan 2019,sylvain,team:::admin
88,12    jun   2018,öle,team:support
917,05 apr 2019,abhishek,guest

可以看到,这样不仅把第 8 行的仅含空格的 Record 移除,也把第 9、10 行的仅含 换行符 的 Record 也移除掉了。

再看一下这个写法:

1awk 'NF' file.txt

NF 是 Field 的编号,这个程序的匹配规则是:每个 Record 中的 Field 数量不为 0。 而我们知道 Field 是由 Record 拆分出来的,如果不特别指定,分隔符是 FS ,也就是 空格 。 对于仅含空白字符的 Record 或者仅含 换行符 的 Record ,当然就拆分不出 Field ,所以它们的 NF 就是 0,会被过滤掉。 那么这个程序的输出为:

1# 输出
2CREDITS,EXPDATE,USER,GROUPS
399,01 jun 2018,sylvain,team:::admin
452,01    dec   2018,sonia,team
552,01    dec   2018,sonia,team
625,01    jan   2019,sonia,team
710,01 jan 2019,sylvain,team:::admin
88,12    jun   2018,öle,team:support
917,05 apr 2019,abhishek,guest

移除仅含 换行符 的 Record

和上一个例子比起来,这个例子是要把第 8 行保留下来。

我们同样先用正则来试一下:

匹配 换行符 是: \n 匹配 非换行符 是: [^\n] 匹配一个或多个 非换行符 是: [^\n]+

那么这个程序可以这样写:

1awk '/[^\n]+/ { print }' file.txt
 1# 输出
 2
 3CREDITS,EXPDATE,USER,GROUPS
 499,01 jun 2018,sylvain,team:::admin
 552,01    dec   2018,sonia,team
 652,01    dec   2018,sonia,team
 725,01    jan   2019,sonia,team
 810,01 jan 2019,sylvain,team:::admin
 98,12    jun   2018,öle,team:support
10这里有8个空格
1117,05 apr 2019,abhishek,guest

我们再看另一种写法:

1awk '1' RS='' file.txt

这个程序指定了 Record 的分隔符为 空字符串 。 也就是说对于每个 Record 的匹配规则是:每个 Record 用 空字符串 分隔后输出,每个 Record 的 pattern 都为 true,都会输出结果。 运行结果是这样:

 1# 输出
 2CREDITS,EXPDATE,USER,GROUPS
 399,01 jun 2018,sylvain,team:::admin
 452,01    dec   2018,sonia,team
 552,01    dec   2018,sonia,team
 625,01    jan   2019,sonia,team
 710,01 jan 2019,sylvain,team:::admin
 88,12    jun   2018,öle,team:support
 9这里有8个空格
1017,05 apr 2019,abhishek,guest

可以看到也可以达到目的, 这个程序基于一个晦涩难懂的 POSIX 规则,如果将 RS 设置为 空字符串 ,那么 Record 由 多个换行符 分隔。 每行的末尾有 换行符 ,而第 9、10 行的内容就是 换行符 ,所以它们连同第 8 行末尾的 换行符,这 3 个换行符当成了一个分隔符。 这就得到了上面的结果。

提取 Field

这个就比较常用了,我们经常要提取出数据中的一列或几列。 比如现在我想提取出第一列和第三列,并且输出时指定 Field 用逗号分隔:

1awk '{ print $1, $3 }' FS=,  OFS=, file.txt

Note

这个程序中的 action 中, $1$3 中间有个逗号,这个不是分隔符的意思,而是表示 我们想要输出 $1$3 两个变量,如果不写这个逗号,输出结果就不对了。 因为在 action 的引号中的部分,加不加空格都是可以的,并没有严格的格式要求, 换言之在里面指定输出的分隔符当然就是无效的。

 1# 输出
 2CREDITS,USER
 399,sylvain
 452,sonia
 552,sonia
 625,sonia
 710,sylvain
 88,öle
 9        ,
10,
11,
1217,abhishek

现在我们可以看到,已经把第一列和第三列提取出来了,并且以逗号为分隔符。 可以看到,这条命令里没有指定 pattern ,因为 AWK 中的 pattern 默认为 1 ,所以不指定的话,就默认对所有行生效。

结合前面的例子,如果想要同时把无内容行去掉,可以这样:

1awk '/\S+/ { print $1, $3 }' FS=, OFS=, file.txt

这里我们先用正则过滤掉了所有无内容的行,然后指定 Field 分隔符将他们输出来。

再看这一种:

1awk 'NF { print $1, $3 }' FS=, OFS=, file.txt

我们指定 Field 用逗号分隔,然后输出 Field 数量不为 0 的 Record, 这样就会包含那个空格行。

 1# 输出
 2CREDITS,USER
 399,sylvain
 452,sonia
 552,sonia
 625,sonia
 710,sylvain
 88,öle
 9        ,
1017,abhishek

这个例子里我们是手动指定的 FS 和 OFS 值,还有一种更具有编程风格的写法,可以把变量赋值写在一个 BEGIN 块中:

1awk 'BEGIN { FS=OFS="," } NF { print $1,$3 }' file.txt
 1# 输出
 2CREDITS,USER
 399,sylvain
 452,sonia
 552,sonia
 625,sonia
 710,sylvain
 88,öle
 9        ,
1017,abhishek

这个例子也可以这样写:

1awk 'BEGIN { FS=OFS="," } /\S+/ { print $1, $3 }' file.txt

输出为这样:

1CREDITS,USER
299,sylvain
352,sonia
452,sonia
525,sonia
610,sylvain
78,öle
817,abhishek

按列执行计算

AWK 可以编程,从而实现数学运算。现在我们将第一列中的数字求和:

1awk '{ SUM=SUM+$1 } END { print SUM }' FS=, OFS=, file.txt
2# 运算符也可以这样写
3awk '{ SUM+=$1 } END { print SUM }' FS=, OFS=, file.txt
1# 输出
2263

AWK 变量在使用前不需要声明,一个未定义的新变量的默认值是 空字符串 ,等效于数字 0 。 我们可以注意到,数据的第一列其实是包含表头的字符以及后面行的空白符的,但是因为这些都等效于 0 ,所以它们不会干扰求和。 当然,如果我们是乘法的话就有影响了,他就直接返回 0 了。

计算非空行数

上面的例子中我们已经使用了 END 规则,我们还可以用个这个规则来计算数据中的非空行数。

1awk '/./ { COUNT+=1 } END { print COUNT }' file.txt
1# 输出
29

这个程序中,我们逐 Record 遍历输入数据流,如果 Record 能够匹配正则 /./ ,那么变量 COUNT 就加 1 。 这个正则的意思是,至少包含一个字符。 最后,END 块用于在处理完整个数据流后显示最终结果。 变量 COUNT 的名称可以随便起,但要符合这个 命名规则

但是,我们仔细检查下会发现,这个结果是错误的,因为它把那行有 8 个空格的行也算上了。

我们这样改,就可以把这行也去掉:

1awk 'NF { COUNT+=1 } END { print COUNT }' file.txt
1# 输出
28

如果想把第一行的表头也忽略,可以这样写:

1awk '+$1 { COUNT+=1 } END { print COUNT }' file.txt

在 AWK 中使用数组

我现在想创建一个姓名和信用分的映射数组。 同一个人的信用分累加起来。 如果用 Python 可以这样写:

 1credit_map = {}
 2
 3with open("file.txt", "r") as f:
 4    for idx, line in enumerate(f.readlines()):
 5        # 跳过表头
 6        if idx == 0:
 7            continue
 8        # 去掉首尾换行符
 9        line = line.strip()
10        # 去掉空行
11        if len(line) == 0:
12            continue
13        credit, _, name, _ = line.split(",")
14        if name in credit_map:
15            credit_map[name] += int(credit)
16        else:
17            credit_map[name] = int(credit)
18
19for name, credit in credit_map.items():
20    print(name, credit)
1# 输出
2sylvain 109
3sonia 129
4öle 8
5abhishek 17

用 AWK 可以这样写:

1awk '+$1 { CREDITS[$3]+=$1 }
2END { for (NAME in CREDITS) print NAME, CREDITS[NAME] }' FS=, file.txt
1# 输出
2abhishek 1
3sonia 3
4öle 1
5sylvain 2

简洁是很简洁,但是开始还是有些不好理解。 我们来解读一下: 首先 +$1 表示过滤掉表头。 然后创建了一个类似 Python 字典的哈希数组 CREDITS,把第 3 列作为键,第一列作为值,并且相同键的值累加。 然后遍历 CREDITS,遍历出的每个元素是键,然后把键和值打印出来。 Field 分隔符是逗号。 这样就实现了相同的功能了。

Note

未完待续