常用的几个 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
未完待续