awk使用笔记
编者按
本篇为编者阅读博客[笔记] The AWK Programming Language(ADDISON-WESLEY, 1988)做的笔记,跟着博客把awk编程相关的内容进行梳理以及少数地方的勘误,如果有需要请阅读原文。
1.AWK 入门教程(AN AWK TUTORIAL)
1.1 Getting Started
Awk 程序一般都很简短,只有一两行。通用格式为:
1 | awk 'patterns { actions }' files |
pattern
:用于过滤出匹配的行(lines matched by any of the patterns)action
:对匹配的行执行的动作
举例文件为:
1 | cat emp.data |
三列分别表示:员工姓名,时薪,工作时长。下面的程序计算工时非零的员工的工资:
1 | awk '$3 > 0 {print $1, $2 * $3}' emp.data |
$3 > 0
为action,匹配第3列大于0的行{print $1, $2 * $3}
为action,打印姓名,时薪x工时'$3 > 0 {print $1, $2 * $3}'
,单引号里整个为awk程序,通常awk程序较简短,如果awk程序较长,可以把内容放到文件中,通过-f
选项进行读取处理。
如下就是使用-f
选项进行读取处理:
1 | cat start.awk |
AWK 程序的结构
1 | pattern { action } |
pattern 和 action 都是可选的(optional),因此用 {}
将 action 扩起来,以 便与 pattern 区分开。特别地,
-
如果 pattern 为空:例如
{ print $1 }
,则对所有行执行 action1
2
3
4
5
6
7awk '{print $0}' emp.data
Beth 4.00 0
Dan 3.75 0
Kathy 4.00 10
Mark 5.00 20
Mary 5.50 22
Susie 4.25 18 -
如果 action 为空:例如
$3 == 0
,则打印匹配的行(默认 action)1
2
3awk '$3 == 0' emp.data
Beth 4.00 0
Dan 3.75 0
1.2 简单输出(Simple Output)
Awk 中只有两种数据类型:
- 数字(number)
- 字符串(strings of characters.)
Awk 读取每行,然后以空格或 tab 将行分割成多个字段(列),其中:
$1
、$2
、$3
:分别表示第 1、2、3 个字段(列)$0
:该行(the entire line)NF
:字段数量(Number of Fields),即列数$NF
、$(NF-1)
:最后一个字段(最后一列)、倒数第二个字段(列)NR
: 行数数量(Number of Rows),即当前行数FNR
:和NR
功能类似,awk每打开一个文件,FNR即重置为0,NR则继续累加。对单个文件操作,功能一样。
例子
- 打印整行:
{ print }
或{ print $0 }
- 打印第 1 和 3 列:
{ print $1, $3 }
- 打印列数、第 1 列、最后一列、倒数第 2 列:
{ print NF, $1, $NF, $(NF-1) }
- 打印第 1 列、第 2、3 列之积:
{ print $1, $2 * $3 }
- 打印行并加上行号:
{ print NR, $0 }
1.3 高级输出(Fancier Output)
printf
格式化输出
printf
能够输出几乎全部格式,格式为:printf (format, value1 , value2 , ... , value3,...)
举例:
1 | awk '{ printf("total pay for %s is $%.3f\n", $1, $2 * $3) }' emp.data |
还有一个print
输出,区别为**print
会自动加换行符,而 printf
不会**。
如上面的例子使用print
输出:
1 | awk '{ print("total pay for "$1" is "$2*$3) }' emp.data |
排序输出
使用sort对结果进行排序即可
1.4 选择/过滤行(Selection)
通用选择行
- 列值过滤:
awk '$2 >= 5' emp.data
- 表达式计算结果过滤:
awk '$2 * $3 > 50 { printf("$%.2f for %s\n", $2 * $3, $1) }' emp.data
- 字符串匹配:
awk '$1 == "Susie" { printf("$%.2f for %s\n", $2 * $3, $1) }' emp.data
- 正则:
awk '/Susie/ { printf("$%.2f for %s\n", $2 * $3, $1) }' emp.data
- 其他组合:
$2 >= 4 || $3 >= 20
,!($2 < 4 && $3 < 20)
,NF != 3
特殊选择BEGIN/END
BEGIN
:在第一行之前匹配(即,在程序开始时执行且只执行一次)END
:在最后一行之后匹配(即,在程序结束之前执行且只执行一次)
1 | awk 'BEGIN { print "NAME RATE HOURS" } $3>=0 ' emp.data |
1.5 计算(Computing with AWK)
统计数量:工时为0的员工人数
1 | awk ' $3==0 {emp+=1} END {printf("%d employees worked 0 hours\n",emp)}' emp.data |
数值类型的变量默认初始化为 0,因此不需要自己初始化 emp
变量。
求和、求平均
1 | awk '{total+=$2*$3} END {printf("total pay is %.2f,average pay is %.2f\n",total,total/NR)}' emp.data |
注意,上面出现的action均是一个,如果多个action可以通过分号或者换行隔开,如上例:
1 | awk '{total+=$2*$3;print "test more 1 action"} END {printf("total pay is %.2f,average pay is %.2f\n",total,total/NR);print "test more 1 action";print "test more 1 action"}' emp.data |
处理文本:打印时薪最高的员工信息($2最大)
1 | awk '$2>max {max=$2;emp=$1} END {printf "%s gets the max %.3f\n",emp,max}' emp.data |
字符串拼接(concatenation):在一行内打印所有员工名
1 | awk '{all=all$1" "} END {print all}' emp.data |
打印最后一行
1 | awk '{last=$0} END {print last}' emp.data |
内置函数(Built-in Functions)
1 | awk '{print $1,length($1)}' emp.data |
统计行数、单词数、字符数
1 | cat count2.awk |
1.6 控制流 Statements
和其他编程语言一样,awk中也有相关控制语句。
if-else
1 | cat if.awk |
注意到逗号可以将较长的行分为多行。
while
举例:计算复利
-
输入格式:本金 利率 年限,如
1000 0.1 2
-
输出格式:每年的本金和利息之和,输出
1
21100.00
1210.00
awk程序为:
1 | { i = 1 |
for
如上例,计算复利,输入格式:本金 利率 年限,如 1000 0.1 2
1 | { for (i=1; i<=$3; i++) |
1.7 数组
awk行倒序打印文本
使用数组保存每一行内容:
while
循环倒序打印数组,
1 | awk '{tmp[NR]=$0}END{i=NR;while(i>0){print tmp[i--]}}' emp.data |
for
循环倒序打印数组,
1 | awk '{tmp[NR]=$0}END{for (i=NR;i>0;i--) print tmp[i]}' emp.data |
1.8 实用单行命令
编号 | 功能 | AWK 程序 | 类似效果的命令 |
---|---|---|---|
1 | 打印总行数 | END { print NR } |
cat <file> | wc -l |
2 | 打印第 10 行 | NR == 10 |
head -n10 <file> | tail -n1 |
3 | 打印最后一列 | { print $NF } |
|
4 | 打印最后一行的最后一列 | { f = $NF } END { print f } 或 END {print $NF} |
|
5 | 打印有 4 列以上的行 | NF > 4 |
|
6 | 打印最后一列的值大于 4 的行 | $NF > 4 |
|
7 | 打印所有输入的总字段数 | { nf += NF } END { print nf } |
|
8 | 打印包含 Beth 的总行数 |
/Beth/ { n++ } END { print n } |
|
9 | 打印第 1 列的最大值及对应的行(假设第 1 列为正) | $1 > max { max = $1; line = $0 } END { print max, line } |
|
10 | 打印列数大于 1 的行 | NF > 1 |
|
11 | 打印长度大于 80 的行 | length($0) > 80 |
|
12 | 打印每行的列数,及该行 | { print NF, $0 } |
|
13 | 打印第 2, 1 列 | { print $2, $1 } |
|
14 | 交换第 1, 2 列,打印全部行 | { t = $1; $1 = $2; $2 = t; print } #勘误:原博客语句使用’'~",错误 |
|
15 | 第 1 列换成行号,打印全部行 | { $1 = NR; print } |
|
16 | 去掉第 2 列,打印全部行 | { $2 = ""; print } |
|
17 | 列倒序,打印全部行 | '{ for(i=NF; i>0; i--) printf("%s ", $i); printf("\n");} ' |
|
18 | 打印每行的和 | '{ sum=0; for(i=1; i<=NF; i++) sum += $i; print sum }' |
|
19 | 打印所有行的和 | '{ for(i=1; i<=NF; i++) sum += $i; } END { print sum }' |
|
20 | 所有字段取绝对值,打印全部行 | '{ for(i=1; i<=NF; i++) if ($i<0) $i = -$i; print }' |
2. AWK 编程语言(THE AWK LANGUAGE)
AWK 工作流程:
- 语法检查
- 按行读取输入
- 针对每行分别匹配 pattern,然后执行对应的 action
- 如果 pattern 为空,匹配所有行
- 如果 action 为空,打印匹配的行
本章将用下面的输入作为例子:
1 | cat countries.txt |
这个输入的特别之处:
- 列之间用 tab 分隔(
\t
) - 最后一列有空格(
North America
,North America
)
AWK 程序格式
- 同一个 statement 太长需要换行时,用单斜杠(\)连接
- 多个 statement 可以写到同一行,用分号(
;
)分开 - 注释以
#
开头 - 空格和 tab 会被忽略,因此可以适当添加空格/空行提高程序可读性
例子:
1 | { print \ |
2.1 Patterns
6 种 pattern:
-
BEGIN { statements }`:程序开始时(读取任何输入之前)执行一次
-
END { statements }`:程序结束时(读完所有输入之后)执行一次
-
expression { statements }
:
expression` 为真时执行 -
/regular expression/ { statements }`:匹配到正则表达式时执行
-
compound pattern { statements }
:复合表达式(包含
||,
&&` 等逻辑)为真时执行 -
pattern1 , pattern2 { statements }
:**匹配到
pattern1时开始对接下来的每一行执行 actions,匹配到
pattern2` 时停止对后面的行执行**
BEGIN 和 END
BEGIN
和END
模式的 action 不能为空。BEGIN
和END
不能与其他 pattern 混用。- 可以有多个
BEGIN
,按顺序执行;END
同理。 BEGIN
和END
顺序没关系,总是BEGIN
先执行。
BEGIN
的一个用处是:在开始处理输入之前设置字段分隔符(FS
,Field Separator)。 默认的 FS
是空格和 tab。
BEGIN
还用来打印表头等信息。
1 | cat fs.awk |
FS也可以通过命令行选项-F'\t'
指定。
Expressions as Patterns
AWK 中,任何类型的操作符都可以处理任何类型的数据:
- 数字会被自动转换成字符串
- 字符串也会被自动转换成数字
比较操作符一共有 8 个:
<
<=
==
!=
>=
>
~
:匹配(matched by)!~
:不匹配(not matched by)
字符串比较:
- “Canada”
<
“China” - “Asia”
<
“Asian” $0 >= "M"
:选择首字母的 ASCII 码大于M
的行
1 | awk 'NR==2' countries.txt |
String-Matching Patterns
类型:
/regexpr/
:对整行进行匹配,等价于$0 ~ /regxpr/
expression ~ /regexpr/
:对expression
进行匹配expression !~ /regexpr/
:反匹配
Regular Expressions
1 | awk '/.*ndia/' countries.txt |
Compound Patterns
1 | awk 'NR==1||NR==2' countries.txt |
Range Patterns
格式:pattern1, pattern2
,表示:
- 匹配到
pattern1
时开始对该行及后面的行执行 action - 匹配到
pattern2
时,终止对后面的行执行 action(对当前行还是会执行)
1 | awk 'NR==2,NR==4 {print NR,$0}' countries.txt |
2.2 Actions
内置变量
record 和 line 的关系说明:
- 默认情况下,record 分隔符(
RS
)是换行符,因此一个行就是一个 record。 - 如果显式修改
RS
,也可以让多个行对应一个 record(multiline record),后面会介绍到。
如无特殊说明,本文中 line 和 record 是等价的。
变量 | 解释 | 默认值 |
---|---|---|
ARGC |
命令行参数个数 | 无 |
ARGV |
命令行参数列表 | 无 |
FILENAME |
当前文件的文件名 | 无 |
FNR |
record number in current file | 无 |
FS |
字段分隔符(field separator) | 空格或 tab |
NF |
当前行的字段数 | 无 |
NR |
已经读取的记录数(number of records) | 无 |
OFMT |
output format for numbers | "%.6g" |
OFS |
输出字段分隔符(output field separator) | " " |
ORS |
输出记录分隔符(output record separator) | \n |
RLENGTH |
length of string matched by match function | 无 |
RS |
输入记录分隔符(input record separator) | \n |
RSTART |
start of string matched by match function | 无 |
SUBSEP |
subscript separator | \034 |
-
每次读取一个新记录后,会设置 FNR, NF 和 NR
-
$0
发生改变,或者创建了新的字段后,NF 会重置1
2
3
4awk 'FNR==1 {print $0,"NF="NF;$2=10;print $0" new NF="NF}' emp.d
ata
Beth 4.00 0 NF=3
Beth 10 0 new NF=3 -
执行
match
函数后,RLENGTH and RSTART 会被重新赋值
字段变量(Field Variables)
例子,设置分隔符,并替换第 4 列:
1 | cat replace.awk |
- 如果
$0
发生变化,$1
,$2
和 NF 等会被重新计算 - 如果
$1
或$2
等等发生变化,$0
也会被重新构建,构建时使用OFS
作为字段分隔符
不存在的字段:
- 如果访问不存在的字段,例如
$(NF+1)
,得到的是空字符串。 - 给一个不存在的字段赋值,就会创建该字段,例如
$5 = 1000 * $3 / $2
每行的字段数量可能不相同,但有一个最大字段限制,一般是 100。
内置字符串函数
-
gsub(r, s)
:在当前行($0
)中进行字符串替换,等价于gsub(r, s, $0)
1
2
3
4awk 'gsub("a","x")' <<< "aa bb cc"
xx bb cc
awk 'gsub(/a/,"x")' <<< "aa ab cc"
xx xb cc -
gsub(r, s, t)
:在字符串t
中进行替换1
2awk 'gsub("a","x",$2)' <<< "aa ab cc"
aa xb cc -
index(s, t)
:寻找子字符串出现的位置 -
length(s)
:字符串长度 -
match(s, r)
:匹配字符串,会设置RSTART
和RLENGTH
-
split(s, a)
:将字符串 s 分隔为数组 a,使用默认分隔符(FS) -
split(s, a, fs)
:将字符串 s 分隔为数组 a,使用指定分隔符 -
sprintf()
:格式化字生成符串 -
sub(r, s)
:字符串替换(leftmost),等价于sub(r, s, t)
-
sub(r, s, t)
:字符串替换(leftmost) -
substr(s, p)
:返回从位置 p 开始到最后的子字符串(即 suffix) -
substr(s, p, n)
:返回从位置 p 开始,长度为 n 的子字符串1
2awk '{$1 =substr($1, 1, 3); print $0 }' <<< '12345 12345'
123 12345
在替换函数中,&
字符是一个变量,表示匹配到的字符串,来看下面的例子:
1 | gsub(/a/, "&b&", "banana") |
等价于
1 | gsub(/a/, "aba", "banana") |
字符串和数字类型互相转换:
number ""
:将数字转换成字符串string + 0
:将字符串转换成数字
因此,对不同类型的变量可以这样做转换和比较:
$1 "" == $2
$1 + 0 == $2 + 0
控制流
关键字:
-
next
:开始下一次主输入循环(main input loop),即,开始处理下一行1
2
3
4
5
6awk '{if (FNR==2) next;else print NR,$0}' emp.data
1 Beth 4.00 0
3 Kathy 4.00 10
4 Mark 5.00 20
5 Mary 5.50 22
6 Susie 4.25 18 -
exit [<expr>]
:立即跳转到 END 部分;如果已经在 END 部分,立即退出程序;将expr
的执行结果作为返回值。
1 | awk '{if (FNR==2) exit; else print NR,$0;} END {print "END"}' emp.data |
数组
AWK 提供了一维数组。数组不需要提前声明,也没有容量大小。
例子,行倒序打印:{ x[NR] = $0 } END { for (i=NR; i>0; i--) print x[i] }
。
AWK 中的数组是用字符串索引的,因此也叫关联数组(associative arrays)。
例子,分别计算 Asia 和 Europe 的总人口:
1 | /Asia/ { pop["Asia"] += $3 } |
例子,分别为所有地区(第 4 列是地区)计算总人口:
1 | BEGIN { FS = "\t" } |
判断 key 是否存在:if ("Africa" in pop) ...
。
从数组中删除一个元素的操作
1 | delete array[subscript] |
例子:for (i in pop) delete pop[i]
Split 到数组
split("7/4/76", arr, "/")
得到的数组 arr
:[7,4,76]
举例,以数组形式打印第一行:
1 | awk 'FNR==1 {split($0, arr);for (i=1;i<=NF;i++) print i,arr[i]}' countries.txt |
数组是用字符串来索引的,这可能有点反直觉。但由于 1
的字符串形式是 "1"
,因此 自动类型转换之后,arr[1] == arr["1"]
。
多维数组
1 | awk 'BEGIN {for (i=1;i<5;i++) for (j=1;j<5;j++) arr[i,j]=0;} END {for (i=1;i<5;i++) for (j=1;j<5;j++) printf("[%s,%s]=%s\n",i,j,arr[i,j]);}' <<< '' |
2.3 User-Defined Functions
函数格式:
1 | function name(parameter-list) { |
函数定义可以出现在任何位置。
例子:递归函数调用:
1 | { print max(S1, max(S2, S3)) } # print maximum of $1, $2, $3 |
函数的参数:
- 非数组按值传递,传递的是值的复制
- 数组按引用传递,能改变数组内的值
2.4 OutPut
print
:等价于print $0
print expr, expr, ...
:打印多个表达式的值,之间用 OFS 分隔,最后以 ORS 结束print expr, expr, ... > <file>
print expr, expr, ... >> <file>
print expr, expr, ... | other_command
:重定向到其他命令的标准输入close(filename)
,close(command)
:system(command)
:
Output Separators
内置变量:
- OFS(Output Field Separator):默认是单个空格
- ORS(Output Record Separator):默认是单个换行符(
\n
)
举例:每行最后增加个"-":
1 | awk 'BEGIN{ORS="-\n"} {print}' countries.txt |
Output to file
1 | { print($1, $3) > ($3 > 100 ? "bigpop" : "smallpop") } |
Output to Pipes
1 | BEGIN { FS = "\t" } |
Closing Flies and Pipes
1 | close("sort -t'\t' +1rn") |
close is necessary if you intend to write a file, then read it later in the same program.
3. 几个awk程序赏析
打印TCP状态
1 | ss -an |awk '/^tcp/ {state[$2]++} END {for (i in state) print i,state[i]}' |
监控CPU使用异常并触发sysrq打印进程记录
1 | !/bin/sh |
赞赏支持一下