awk 是一个强大的文本分析工具,它是 Linux 中功能强大的数据处理引擎之一,awk 可以非常轻松地处理比如每行都是相同格式的文本,比如日志,csv 格式等等。相对于 grep 的查找,sed 的编辑,awk 在其对数据分析并生成报告时,显得尤为强大。

当使用 awk 命令处理一个或者多个文件时,它会依次读取文件的每一行内容,然后对其进行处理,awk 命令默认从 stdio 标准输入获取文件内容,awk 使用一对单引号来表示一些可执行的脚本代码,在可执行脚本代码里面,使用一对花括号来表示一段可执行代码块,可以同时存在多个代码块。awk 的每个花括号内同时可以有多个指令,每一个指令用分号分隔,awk 其实就是一个脚本编程语言。

awk 命令和 sed 命令结构相同,通常情况下,awk 将每个输入行解释为一条记录而每一行中的内容(由空格或者制表符分隔)解释为每一个字段,一个或者多个连续空格或者制表符看做定界符。awk 中 $0 代表整个记录。

awk ' /MA/ { print $1 }' list

解释:打印包含 MA 的行中的第一个单词。再举一个具体的例子,比如

echo 'this is one world\nthat is another world' | awk '{print $1}'

那么输出就是 awk 处理之后的每一行第一个字符也就是:

this
that

基本格式

awk 命令的基本格式

awk [options] 'script' file

options 这个表示一些可选的参数选项,script 表示 awk 的可执行脚本代码(一般被{} 花括号包围),这个是必须的。file 这个表示 awk 需要处理的文件,注意需要是纯文本文件(意味着 awk 能够处理)。

awk 自定义分隔符

之前提到的,awk 默认的分割符为空格和制表符,awk 会根据这个默认的分隔符将每一行分为若干字段,依次用 $1, $2,$3 来表示,可以使用 -F 参数来指定分隔符

awk -F ':' '{print $1}' /etc/passwd

解释:使用 -F 来改变分隔符为 : ,比如上面的命令将 /etc/passwd 文件中的每一行用冒号 : 分割成多个字段,然后用 print 将第 1 列字段的内容打印输出

在 awk 中同时指定多个分隔符,比如现在有这样一个文件 some.log 文件内容如下

Grape(100g)1980
raisins(500g)1990
plum(240g)1997
apricot(180g)2005
nectarine(200g)2008

现在我们想将上面的 some.log 文件中按照 “水果名称(重量)年份” 来进行分割

$ awk -F '[()]' '{print $1, $2, $3}' some.log
Grape 100g 1980
raisins 500g 1990
plum 240g 1997
apricot 180g 2005
nectarine 200g 2008

-F 参数中使用一对方括号来指定多个分隔符,awk 处理 some.log 文件时就会使用 “(“ 或者 “)” 来对文件的每一行进行分割。

awk 内置变量的使用

awk 除了 $ 和数字表示字段还有一些其他的内置变量:

  • $0 这个表示文本处理时的当前行,$1 表示文本行被分隔后的第 1 个字段列,$2 表示文本行被分割后的第 2 个字段列,$3 表示文本行被分割后的第 3 个字段列,$n 表示文本行被分割后的第 n 个字段列
  • NR 表示文件中的行号,表示当前是第几行
  • NF 表示文件中的当前行被分割的列数,可以理解为 MySQL 数据表里面每一条记录有多少个字段,所以 $NF 就表示最后一个字段,$(NF-1) 就表示倒数第二个字段
  • FS 表示 awk 的输入分隔符,默认分隔符为空格和制表符,可以对其进行自定义设置
  • OFS 表示 awk 的输出分隔符,默认为空格,也可以对其进行自定义设置
  • FILENAME 表示当前文件的文件名称,如果同时处理多个文件,它也表示当前文件名称
  • RS 行分隔符,用于分割行,默认为换行符
  • ORS 输出记录的分隔符,默认为换行符

比如我们有这么一个文本文件 fruit.txt 内容如下,用它来演示如何使用 awk 命令工具

peach    100   Mar  1997   China
Lemon    150   Jan  1986   America
Pear     240   Mar  1990   Janpan
avocado  120   Feb  2008   china

awk '{print $0}' fruit.txt   # 表示打印输出文件的每一整行的内容
awk '{print $1}' fruit.txt   # 表示打印输出文件的每一行的第 1 列内容
awk '{print $1, $2}' fruit.txt

文件的每一行的每一列的内容除了可以用 print 命令打印输出以外,还可以对其进行赋值

awk '{$2 = "***"; print $0}' fruit.txt

上面的例子就是表示通过对 $2 变量进行重新赋值,来隐藏每一行的第 2 列内容,并且用星号 * 来代替其输出

在参数列表中加入一些字符串或者转义字符之类的东东

awk '{print $1 "\t" $2 "\t" $3}' fruit.txt

像上面这样,你可以在 print 的参数列表中加入一些字符串或者转义字符之类的东东,让输出的内容格式更漂亮,但一定要记住要使用双引号。

awk 内置 NR 变量表示每一行的行号

awk '{print NR "\t" $0}' fruit.txt

1   peach    100   Mar  1997   China
2   Lemon    150   Jan  1986   America
3   Pear     240   Mar  1990   Janpan
4   avocado  120   Feb  2008   china

awk 内置 NF 变量表示每一行的列数

awk '{print NF "\t" $0}' fruit.txt

5   peach    100   Mar  1997   China
5   Lemon    150   Jan  1986   America
5   Pear     240   Mar  1990   Janpan
5   avocado  120   Feb  2008   china

awk 中 $NF 变量的使用

awk '{print $NF}' fruit.txt

上面这个 $NF 就表示每一行的最后一列,因为 NF 表示一行的总列数,在这个文件里表示有 5 列,然后在其前面加上 $ 符号,就变成了 $5 ,表示第 5 列

awk '{print $(NF - 1)}' fruit.txt

1997
1986
1990
2008

上面 $(NF-1) 表示倒数第 2 列, $(NF-2) 表示倒数第 3 列,依次类推。

awk 'NR % 6'        # 打印出了 6 倍数行之外的其他行
awk 'NR > 5'        # 打印第 5 行之后内容,类似 `tail -n +6` 或者 `sed '1,5d'`
awk 'NF >= 6'       # 打印大于等于 6 列的行
awk '/foo/ && /bar/'    # 打印匹配 `/foo/` 和 `/bar/` 的行
awk '/foo/ && !/bar/'   # 打印包含 `/foo/` 不包含 `/bar/` 的行
awk '/foo/ || /bar/'    # 或
awk '/foo/,/bar/'       # 打印从匹配 `/foo/` 开始的行到 `/bar/` 的行,包含这两行

awk 内置函数

awk 还提供了一些内置函数,比如:

  • toupper() 用于将字符转为大写
  • tolower() 将字符转为小写
  • length() 长度
  • substr() 子字符串
  • sin() 正弦
  • cos() 余弦
  • sqrt() 平方根
  • rand() 随机数

更多的方法可以参考:man awk

awk 同时处理多个文件

awk '{print FILENAME "\t" $0}' demo1.txt demo2.txt

当你使用 awk 同时处理多个文件的时候,它会将多个文件合并处理,变量 FILENAME 就表示当前文本行所在的文件名称。

BEGIN 关键字的使用

在脚本代码段前面使用 BEGIN 关键字时,它会在开始读取一个文件之前,运行一次 BEGIN 关键字后面的脚本代码段, BEGIN 后面的脚本代码段只会执行一次,执行完之后 awk 程序就会退出

awk 'BEGIN {print "Start read file"}' /etc/passwd

awk 脚本中可以用多个花括号来执行多个脚本代码,就像下面这样

awk 'BEGIN {print "Start read file"} {print $0}' /etc/passwd

END 关键字使用方法

awk 的 END 指令和 BEGIN 恰好相反,在 awk 读取并且处理完文件的所有内容行之后,才会执行 END 后面的脚本代码段

awk 'END {print "End file"}' /etc/passwd
awk 'BEGIN {print "Start read file"} {print $0} END {print "End file"}' /etc/passwd

在 awk 中使用变量

可以在 awk 脚本中声明和使用变量

awk '{msg="hello world"; print msg}' /etc/passwd

awk 声明的变量可以在任何多个花括号脚本中使用

awk 'BEGIN {msg="hello world"} {print msg}' /etc/passwd

在 awk 中使用数学运算,在 awk 中,像其他编程语言一样,它也支持一些基本的数学运算操作

awk '{a = 12; b = 24; print a + b}' company.txt

上面这段脚本表示,先声明两个变量 a = 12 和 b = 24,然后用 print 打印出 a 加上 b 的结果。

请记住 awk 是针对文件的每一行来执行一次单引号 里面的脚本代码,每读取到一行就会执行一次,文件里面有多少行就会执行多少次,但 BEGIN 和 END 关键字后脚本代码除外,如果被处理的文件中什么都没有,那 awk 就一次都不会执行。

awk 还支持其他的数学运算符

+ 加法运算符
- 减法运算符
* 乘法运算符
/ 除法运算符
% 取余运算符

在 awk 中使用条件判断

比如有一个文件 company.txt 内容如下

yahoo   100 4500
google  150 7500
apple   180 8000
twitter 120 5000

如果要判断文件的第 3 列数据,也就是平均工资小于 5500 的公司,然后将其打印输出

awk '$3 < 5500 {print $0}' company.txt

上面的命令结果就是平均工资小于 5500 的公司名单,$3 < 5500 表示当第 3 列字段的内容小于 5500 的时候才会执行后面的 {print $0} 代码块

awk '$1 == "yahoo" {print $0}' company.txt

awk 还有一些其他的条件操作符如下

< 小于
<= 小于或等于
== 等于
!= 不等于
> 大于
>= 大于或等于
~ 匹配正则表达式
!~ 不匹配正则表达式

使用 if 指令判断来实现上面同样的效果

awk '{if ($3 < 5500) print $0}' company.txt

上面表示如果第 3 列字段小于 5500 的时候就会执行后面的 print $0

在 awk 中使用正则表达式

比如现在我们有这么一个文件 poetry.txt 内容如下:

This above all: to thine self be true
There is nothing either good or bad, but thinking makes it so
There’s a special providence in the fall of a sparrow
No matter how dark long, may eventually in the day arrival

使用正则表达式匹配字符串 “There” ,将包含这个字符串的行打印并输出

awk '/There/{print $0}' poetry.txt

There is nothing either good or bad, but thinking makes it so
There’s a special providence in the fall of a sparrow

使用正则表达式配一个包含字母 t 和字母 e ,并且 t 和 e 中间只能有任意单个字符的行

awk '/t.e/{print $0}' poetry.txt

There is nothing either good or bad, but thinking makes it so
There’s a special providence in the fall of a sparrow
No matter how dark long, may eventually in the day arrival

如果只想匹配单纯的字符串 “t.e”, 那正则表达式就是这样的 /t.e/ ,用反斜杠来转义 . 符号 因为 . 在正则表达式里面表示任意单个字符。

使用正则表达式来匹配所有以 “The” 字符串开头的行

awk '/^The/{print $0}' poetry.txt

在正则表达式中 ^ 表示以某某字符或者字符串开头。

使用正则表达式来匹配所有以 “true” 字符串结尾的行

awk '/true$/{print $0}' poetry.txt

在正则表达式中 $ 表示以某某字符或者字符串结尾。

awk '/m[a]t/{print $0}' poetry.txt

No matter how dark long, may eventually in the day arrival

上面这个正则表达式 /m[a]t/ 表示匹配包含字符 m ,然后接着后面包含中间方括号中表示的单个字符 a ,最后包含字符 t 的行,输出结果中只有单词 “matter” 符合这个正则表达式的匹配。因为正则表达式 [a] 方括号中表示匹配里面的任意单个字符。

继续上面的一个新例子如下

awk '/^Th[ie]/{print $0}' poetry.txt

这个例子中的正则表达式 /^Th[ie]/ 表示匹配以字符串 “Thi” 或者 “The” 开头的行,正则表达式方括号中表示匹配其中的任意单个字符。

再继续上面的新的用法

awk '/s[a-z]/{print $0}' poetry.txt

正则表达式 /s[a-z]/ 表示匹配包含字符 s 然后后面跟着任意 a 到 z 之间的单个字符的字符串,比如 “se”, “so”, “sp” 等等。

正则表达式 [] 方括号中还有一些其他用法比如下面这些

[a-zA-Z]  表示匹配小写的 a 到 z 之间的单个字符,或者大写的 A 到 Z 之间的单个字符
[^a-z]    符号 `^` 在方括号里面表示取反,也就是非的意思,表示匹配任何非 a 到 z 之间的单个字符

正则表达式中的星号 * 和加号 + 的使用方法,* 表示匹配星号前字符串 0 次或者多次,+ 和星号原理差不多,只是加号表示任意 1 个或者 1 个以上,也就是必须至少要出现一次。

正则表达式问号 ? 的使用方法,正则中的问号 ? 表示它前面的字符只能出现 0 次 或者 1 次。

正则表达式中的 {} 花括号用法,花括号 {} 表示规定它前面的字符必须出现的次数,像这个 /go{2}d/ 就表示只匹配字符串 “good”,也就是中间的字母 “o” 必须要出现 2 次。

正则表达式中的花括号还有一些其他的用法如下

/go{2,10}d/   表示字母 "o" 只能可以出现 2 次,3 次,4 次,5 次,6 次 ... 一直到 10 次
/go{2,}d/     表示字母 "o" 必须至少出现 2 次或着 2 次以上

正则表达式中的圆括号表示将多个字符当成一个完整的对象来看待。比如 /th(in){1}king/ 就表示其中字符串 “in” 必须出现 1 次。而如果不加圆括号就变成了 /thin{1}king/ 这个就表示其中字符 “n” 必须出现 1 次。

使用 AWK 移除行中特定模式

接上面正则使用,比如文件中有行数据

/abc/def/123 456
/abc/def/222 456

想要移除 123,保留之前的字母和后面的数字,则可以使用

awk 'sub(/[0-9]+/,"",$1)' /path/to/file

一些组合使用

使用 awk 过滤 history 输出,找到最常用的命令

history | awk '{a[$2]++}END{for(i in a){print a[i] " " i}}' | sort -rn | head

过滤文件中重复行

awk '!x[$0]++' <file>

将一行长度超过 72 字符的行打印

awk 'length>72' file

查看最近哪些用户使用系统

last | grep -v "^$" | awk '{ print $1 }' | sort -nr | uniq -c

假设有一个文本,每一行都是一个 int 数值,想要计算这个文件每一行的和,可以使用

awk '{s+=$1} ENG {printf "%.0f", s}' /path/to/file

reference

  • http://www.ruanyifeng.com/blog/2018/11/awk.html