Linux 日志处理三剑客之 awk:取列、取行、过滤与统计计算

Linux 文本处理三剑客之 awk:取列、取行、过滤与统计计算

文章目录

  • [一、认识 awk](#一、认识 awk)
    • [1.1 awk 是什么](#1.1 awk 是什么)
    • [1.2 awk 和 grep、sed 的区别](#1.2 awk 和 grep、sed 的区别)
    • [1.3 awk 的基本语法](#1.3 awk 的基本语法)
    • [1.4 BEGIN、主体和 END 的执行顺序](#1.4 BEGIN、主体和 END 的执行顺序)
  • 二、准备测试文件
    • [2.1 空格分隔版本](#2.1 空格分隔版本)
    • [2.2 逗号分隔版本](#2.2 逗号分隔版本)
  • [三、awk 语法测试](#三、awk 语法测试)
    • [3.1 打印整行内容](#3.1 打印整行内容)
    • [3.2 使用 BEGIN 和 END](#3.2 使用 BEGIN 和 END)
    • [3.3 print 输出字符串的注意事项](#3.3 print 输出字符串的注意事项)
  • [四、awk 获取列](#四、awk 获取列)
    • [4.1 字段变量的含义](#4.1 字段变量的含义)
    • [4.2 打印指定列](#4.2 打印指定列)
    • [4.3 拼接固定格式输出](#4.3 拼接固定格式输出)
    • [4.4 使用 NF 获取最后一列](#4.4 使用 NF 获取最后一列)
    • [4.5 使用 -F 指定分隔符](#4.5 使用 -F 指定分隔符)
  • [五、awk 获取行](#五、awk 获取行)
    • [5.1 使用 NR 按行号过滤](#5.1 使用 NR 按行号过滤)
    • [5.2 使用比较运算符](#5.2 使用比较运算符)
    • [5.3 使用逻辑运算符](#5.3 使用逻辑运算符)
    • [5.4 使用正则匹配行](#5.4 使用正则匹配行)
    • [5.5 使用字段条件过滤行](#5.5 使用字段条件过滤行)
    • [5.6 使用范围条件取连续内容](#5.6 使用范围条件取连续内容)
  • [六、awk 取行取列实操](#六、awk 取行取列实操)
    • [6.1 提取网卡 IP 地址](#6.1 提取网卡 IP 地址)
    • [6.2 提取内存信息](#6.2 提取内存信息)
    • [6.3 过滤 /etc/passwd 指定字段](#6.3 过滤 /etc/passwd 指定字段)
  • [七、awk 统计计算](#七、awk 统计计算)
    • [7.1 统计行数](#7.1 统计行数)
    • [7.2 统计空行数量](#7.2 统计空行数量)
    • [7.3 对字段求和](#7.3 对字段求和)
    • [7.4 分组计数与分组求和](#7.4 分组计数与分组求和)
    • [7.5 计算平均值](#7.5 计算平均值)
    • [7.6 计算内存可用率](#7.6 计算内存可用率)
    • [7.7 查找最大值和最小值](#7.7 查找最大值和最小值)
  • 八、常见运维场景
  • 九、复习检查点
  • 总结
  • 十、注意事项

一、认识 awk

1.1 awk 是什么

awk 是一种文本处理工具,也可以理解成一门小型文本处理语言。它会一行一行读取输入内容,然后根据条件执行动作。

和普通命令相比,awk 的能力更像"边读文本边写一点逻辑":

  • 可以按列取值;
  • 可以按行号过滤;
  • 可以用正则筛选;
  • 可以做变量赋值;
  • 可以做数学运算;
  • 可以计数、求和、求平均值;
  • 可以在读取文件前后分别执行不同动作。

awk 这个名字来自三位作者的姓氏首字母:Alfred Aho、Peter Weinberger、Brian Kernighan。

日常运维里不用把 awk 当成完整编程语言去背,先抓住一句话就够了:

awk 最常用的能力,就是把一行文本切成多列,然后根据行和列做过滤、输出、统计。

1.2 awk 和 grep、sed 的区别

Linux 文本处理三剑客各有侧重点:

工具 更擅长的事情 常见理解
grep 按关键字或正则找行 找到符合条件的行
sed 按行编辑文本 替换、删除、插入、打印
awk 按字段分析文本 取列、过滤、计算、格式化输出

举个简单例子,如果有一份人员信息:

bash 复制代码
张伟 28 男 13912345678
李芳 34 女 13887654321
王强 42 男 13712345678

需求不同,适合的工具也不同:

需求 推荐工具 示例
找出包含"男"的行 grep grep '男' test.txt
把"男"替换为"M" sed sed 's/男/M/g' test.txt
只输出姓名和年龄 awk awk '{print $1,$2}' test.txt
统计年龄总和 awk awk '{sum+=$2} END{print sum}' test.txt

所以学习 awk 时,不要只把它当成"另一个 grep"。它真正强的地方是字段处理和统计计算。

1.3 awk 的基本语法

awk 的常用格式如下:

bash 复制代码
awk [选项] 'BEGIN{...} 模式{动作} END{...}' 文件

也可以简化成:

bash 复制代码
awk '条件{动作}' 文件

几个关键部分的作用如下:

部分 作用
BEGIN{...} 读取文件前执行一次,常用于初始化变量、打印表头
模式 行过滤条件,可以是行号、正则、字段比较等
{动作} 对满足条件的行执行操作,比如 print、累加、赋值
END{...} 文件读取完成后执行一次,常用于输出统计结果
文件 要处理的文本文件,也可以来自管道输入

最简单的写法是:

bash 复制代码
awk '{print}' test.txt

这条命令没有写条件,表示每一行都执行 {print}print 没有指定字段时,默认打印整行。

1.4 BEGIN、主体和 END 的执行顺序

awk 的执行顺序很固定:

  1. 先执行 BEGIN{},只执行一次;
  2. 开始读取文件,每次读取一行;
  3. 对当前行判断是否满足模式条件;
  4. 满足条件就执行 {动作}
  5. 文件全部读取完成后,执行 END{},只执行一次。

例如:

bash 复制代码
awk 'BEGIN{print "start"} {print} END{print "end"}' test.txt

可以理解成:

  • 还没读文件,先输出 start
  • 读取每一行时,把当前行打印出来;
  • 所有行读完后,输出 end

这个执行顺序非常重要。后面做统计时,变量通常在主体部分累加,最后在 END 中统一输出结果。

二、准备测试文件

2.1 空格分隔版本

后面的基础示例先使用一个空格分隔的 test.txt

bash 复制代码
cat > test.txt <<'EOF'
张伟 28 男 13912345678
李芳 34 女 13887654321
王强 42 男 13712345678
赵敏 29 女 13687654321
刘洋 36 男 13512345678
陈静 31 女 13487654321
李明 25 男 13312345678
吴丽 27 女 13287654321
钱伟 39 男 13112345678
孙芳 33 女 13087654321
EOF

查看文件:

bash 复制代码
[root@atguigu ~]# cat test.txt
张伟 28 男 13912345678
李芳 34 女 13887654321
王强 42 男 13712345678
赵敏 29 女 13687654321
刘洋 36 男 13512345678
陈静 31 女 13487654321
李明 25 男 13312345678
吴丽 27 女 13287654321
钱伟 39 男 13112345678
孙芳 33 女 13087654321

这份文件一共有 4 列:

列号 含义
第 1 列 姓名
第 2 列 年龄
第 3 列 性别
第 4 列 手机号码

awk 默认把连续空格或制表符当成字段分隔符,所以这种文件可以直接使用 $1$2$3$4 取列。

2.2 逗号分隔版本

实际工作中,很多文件不是空格分隔,而是逗号、冒号、竖线等字符分隔。为了后面演示 -F,可以准备一个逗号分隔版本:

bash 复制代码
cat > test.csv <<'EOF'
张伟,28,男,13912345678
李芳,34,女,13887654321
王强,42,男,13712345678
赵敏,29,女,13687654321
刘洋,36,男,13512345678
陈静,31,女,13487654321
李明,25,男,13312345678
吴丽,27,女,13287654321
钱伟,39,男,13112345678
孙芳,33,女,13087654321
EOF

对于这种文件,必须告诉 awk 分隔符是逗号:

bash 复制代码
awk -F',' '{print $1,$2}' test.csv

否则 awk 会按照默认空格分列,结果就不是想要的姓名和年龄。

三、awk 语法测试

3.1 打印整行内容

打印文件全部内容:

bash 复制代码
[root@atguigu ~]# awk '{print}' test.txt
张伟 28 男 13912345678
李芳 34 女 13887654321
王强 42 男 13712345678
赵敏 29 女 13687654321
刘洋 36 男 13512345678
陈静 31 女 13487654321
李明 25 男 13312345678
吴丽 27 女 13287654321
钱伟 39 男 13112345678
孙芳 33 女 13087654321

这条命令等价于:

bash 复制代码
awk '{print $0}' test.txt

$0 表示当前整行内容。没有写字段时,print 默认打印当前整行。

3.2 使用 BEGIN 和 END

在读取文件前输出 123,读取文件后输出 456

bash 复制代码
[root@atguigu ~]# awk 'BEGIN{print 123} {print} END{print 456}' test.txt
123
张伟 28 男 13912345678
李芳 34 女 13887654321
王强 42 男 13712345678
赵敏 29 女 13687654321
刘洋 36 男 13512345678
陈静 31 女 13487654321
李明 25 男 13312345678
吴丽 27 女 13287654321
钱伟 39 男 13112345678
孙芳 33 女 13087654321
456

如果要输出表头,BEGIN 很适合:

bash 复制代码
[root@atguigu ~]# awk 'BEGIN{print "姓名 年龄 性别 手机号码"} {print}' test.txt
姓名 年龄 性别 手机号码
张伟 28 男 13912345678
李芳 34 女 13887654321
王强 42 男 13712345678
赵敏 29 女 13687654321
刘洋 36 男 13512345678
陈静 31 女 13487654321
李明 25 男 13312345678
吴丽 27 女 13287654321
钱伟 39 男 13112345678
孙芳 33 女 13087654321

3.3 print 输出字符串的注意事项

print 输出数字时可以不加引号:

bash 复制代码
awk 'BEGIN{print 123}'

输出普通字符串、中文、英文时,需要加引号:

bash 复制代码
awk 'BEGIN{print "姓名 年龄 性别 手机号码"}'

错误写法:

bash 复制代码
awk 'BEGIN{print 姓名 年龄 性别 手机号码} {print}' test.txt

这类写法容易报错,因为 awk 会把没有引号的内容当成变量或表达式来解析。

也可以使用逗号分隔多个输出项:

bash 复制代码
[root@atguigu ~]# awk 'BEGIN{print "姓名","年龄","性别","手机号码"} {print}' test.txt
姓名 年龄 性别 手机号码
张伟 28 男 13912345678
李芳 34 女 13887654321
王强 42 男 13712345678
赵敏 29 女 13687654321
刘洋 36 男 13512345678
陈静 31 女 13487654321
李明 25 男 13312345678
吴丽 27 女 13287654321
钱伟 39 男 13112345678
孙芳 33 女 13087654321

这里的逗号不是原样输出逗号,而是告诉 awk:输出多个字段,中间用输出分隔符隔开。默认输出分隔符是空格。

四、awk 获取列

4.1 字段变量的含义

awk 处理每一行时,会自动把这一行切成多个字段:

变量 含义
$0 当前整行
$1 第 1 列
$2 第 2 列
$3 第 3 列
$NF 最后一列
$(NF-1) 倒数第 2 列
NF 当前行字段数量
NR 当前处理到第几行

这里容易混的是 $NFNF

  • NF 是字段数量;
  • $NF 是最后一列的值。

例如一行内容是:

bash 复制代码
张伟 28 男 13912345678

那么:

表达式 结果
$1 张伟
$2 28
$3
$4 13912345678
NF 4
$NF 13912345678

4.2 打印指定列

打印第 1 列和第 2 列:

bash 复制代码
[root@atguigu ~]# awk '{print $1,$2}' test.txt
张伟 28
李芳 34
王强 42
赵敏 29
刘洋 36
陈静 31
李明 25
吴丽 27
钱伟 39
孙芳 33

打印第 1、2、3、4 列,并添加表头:

bash 复制代码
[root@atguigu ~]# awk 'BEGIN{print "name age gender tel"} {print $1,$2,$3,$4}' test.txt
name age gender tel
张伟 28 男 13912345678
李芳 34 女 13887654321
王强 42 男 13712345678
赵敏 29 女 13687654321
刘洋 36 男 13512345678
陈静 31 女 13487654321
李明 25 男 13312345678
吴丽 27 女 13287654321
钱伟 39 男 13112345678
孙芳 33 女 13087654321

如果只想看手机号:

bash 复制代码
awk '{print $4}' test.txt

或者使用最后一列:

bash 复制代码
awk '{print $NF}' test.txt

当每一行字段数量固定时,$4$NF 结果一样;但如果字段数量不固定,$NF 更适合取"最后一个字段"。

4.3 拼接固定格式输出

awk 不只能原样打印字段,还能把字段拼接成指定格式。

例如输出 name:xxx,age:xxx

bash 复制代码
[root@atguigu ~]# awk '{print "name:"$1",age:"$2}' test.txt
name:张伟,age:28
name:李芳,age:34
name:王强,age:42
name:赵敏,age:29
name:刘洋,age:36
name:陈静,age:31
name:李明,age:25
name:吴丽,age:27
name:钱伟,age:39
name:孙芳,age:33

注意这里的拼接方式:

bash 复制代码
print "name:"$1",age:"$2

含义是:

  • "name:" 是固定字符串;
  • $1 是第 1 列;
  • ",age:" 是固定字符串;
  • $2 是第 2 列。

awk 中,字符串和变量挨在一起就会自动拼接。

4.4 使用 NF 获取最后一列

查询最后一列和倒数第二列:

bash 复制代码
[root@atguigu ~]# awk '{print $NF,$(NF-1)}' test.txt
13912345678 男
13887654321 女
13712345678 男
13687654321 女
13512345678 男
13487654321 女
13312345678 男
13287654321 女
13112345678 男
13087654321 女

$(NF-1) 外层括号不能省略。因为 NF-1 是一个表达式,表示"字段数量减 1",再通过 $() 取这一列的值。

可以这样理解:

bash 复制代码
$NF       # 最后一列
$(NF-1)   # 倒数第二列
$(NF-2)   # 倒数第三列

4.5 使用 -F 指定分隔符

awk 默认使用空格或制表符分隔字段。如果文件变成逗号分隔:

bash 复制代码
张伟,28,男,13912345678
李芳,34,女,13887654321

直接执行:

bash 复制代码
awk '{print $1,$2}' test.csv

结果可能只输出整行,因为在 awk 看来,这一行没有空格,整行就是第 1 列。

正确写法是使用 -F 指定分隔符:

bash 复制代码
[root@atguigu ~]# awk -F',' '{print $1,$2}' test.csv
张伟 28
李芳 34
王强 42
赵敏 29
刘洋 36
陈静 31
李明 25
吴丽 27
钱伟 39
孙芳 33

常见分隔符示例:

文件格式 分隔符 命令写法
空格分隔 默认 awk '{print $1}' file
逗号分隔 , awk -F',' '{print $1}' file
冒号分隔 : awk -F':' '{print $1}' file
竖线分隔 ` `
一个或多个空格、斜杠 正则 awk -F'[ /]+' '{print $3}' file

-F 后面也可以写正则表达式,这在处理命令输出时很常用。

五、awk 获取行

5.1 使用 NR 按行号过滤

NR 表示当前读取到第几行。

获取第 1 行:

bash 复制代码
[root@atguigu ~]# awk 'NR==1' test.txt
张伟 28 男 13912345678

获取第 3 到第 5 行:

bash 复制代码
[root@atguigu ~]# awk 'NR>=3&&NR<=5' test.txt
王强 42 男 13712345678
赵敏 29 女 13687654321
刘洋 36 男 13512345678

这里没有写 {print},因为 awk 默认动作就是打印整行。也就是说:

bash 复制代码
awk 'NR==1' test.txt

等价于:

bash 复制代码
awk 'NR==1 {print}' test.txt

5.2 使用比较运算符

awk 中常见比较运算符如下:

运算符 含义 示例
== 等于 NR==1
> 大于 $2>30
>= 大于等于 NR>=3
< 小于 $2<30
<= 小于等于 NR<=5
!= 不等于 $3!="男"

例如查询年龄大于 30 的行:

bash 复制代码
[root@atguigu ~]# awk '$2>30' test.txt
李芳 34 女 13887654321
王强 42 男 13712345678
刘洋 36 男 13512345678
陈静 31 女 13487654321
钱伟 39 男 13112345678
孙芳 33 女 13087654321

查询年龄小于等于 30 的行:

bash 复制代码
[root@atguigu ~]# awk '$2<=30' test.txt
张伟 28 男 13912345678
赵敏 29 女 13687654321
李明 25 男 13312345678
吴丽 27 女 13287654321

5.3 使用逻辑运算符

多个条件可以使用逻辑运算符组合:

运算符 含义 示例
&& 并且 $2>30 && $3=="男"
` `
! 取反 ! /男/

查询年龄大于 30 且性别为男:

bash 复制代码
[root@atguigu ~]# awk '$2>30 && $3=="男"' test.txt
王强 42 男 13712345678
刘洋 36 男 13512345678
钱伟 39 男 13112345678

查询性别不是男的行:

bash 复制代码
[root@atguigu ~]# awk '$3!="男"' test.txt
李芳 34 女 13887654321
赵敏 29 女 13687654321
陈静 31 女 13487654321
吴丽 27 女 13287654321
孙芳 33 女 13087654321

查询第 1 行到第 5 行里年龄大于 30 的行:

bash 复制代码
awk 'NR>=1 && NR<=5 && $2>30' test.txt

这种写法在实际过滤日志、命令输出时非常常见。

5.4 使用正则匹配行

awk 可以直接使用 /正则/ 过滤整行。

查询包含"男"的行:

bash 复制代码
[root@atguigu ~]# awk '/男/' test.txt
张伟 28 男 13912345678
王强 42 男 13712345678
刘洋 36 男 13512345678
李明 25 男 13312345678
钱伟 39 男 13112345678

查询包含"张"的行:

bash 复制代码
[root@atguigu ~]# awk '/张/' test.txt
张伟 28 男 13912345678

查询空行:

bash 复制代码
awk '/^$/' file

如果只想查询非空行,可以取反:

bash 复制代码
awk '!/^$/' file

5.5 使用字段条件过滤行

整行正则匹配是看整行内容,有时会误匹配。更准确的方式是指定某一列。

查询第 1 列包含"张"的行:

bash 复制代码
[root@atguigu ~]# awk '$1~/张/' test.txt
张伟 28 男 13912345678

查询第 3 列等于"男"的行:

bash 复制代码
[root@atguigu ~]# awk '$3=="男"' test.txt
张伟 28 男 13912345678
王强 42 男 13712345678
刘洋 36 男 13512345678
李明 25 男 13312345678
钱伟 39 男 13112345678

查询第 3 列不是"男"的行:

bash 复制代码
awk '$3!="男"' test.txt

查询第 2 列大于 30 的行:

bash 复制代码
awk '$2>30' test.txt

字段正则匹配常用两个符号:

写法 含义
$1~/张/ 第 1 列匹配正则
$1!~/张/ 第 1 列不匹配正则

5.6 使用范围条件取连续内容

awk '条件1,条件2' 可以获取从条件 1 匹配行到条件 2 匹配行之间的内容,包括开始行和结束行。

例如:

bash 复制代码
[root@atguigu ~]# seq 10 | awk '/3/,/5/'
3
4
5

也可以按行号取范围:

bash 复制代码
[root@atguigu ~]# awk 'NR==3,NR==5' test.txt
王强 42 男 13712345678
赵敏 29 女 13687654321
刘洋 36 男 13512345678

这种写法适合截取日志中的一段内容,比如从某个开始标记到某个结束标记。

需要注意,范围条件不是"只判断一次"。如果后面再次遇到条件 1,还会再次开启范围匹配。

六、awk 取行取列实操

6.1 提取网卡 IP 地址

实际运维里,awk 经常和其他命令通过管道组合使用。

查看网卡信息:

bash 复制代码
[root@atguigu ~]# ip a s ens33
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 00:0c:29:9d:02:d5 brd ff:ff:ff:ff:ff:ff
    inet 192.168.88.111/24 brd 192.168.88.255 scope global noprefixroute ens33
       valid_lft forever preferred_lft forever
    inet6 fe80::5f08:a955:5a42:b85a/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

目标是提取 IPv4 地址 192.168.88.111

可以先取第 3 行,再按空格和 / 分隔:

bash 复制代码
[root@atguigu ~]# ip a s ens33 | awk 'NR==3' | awk -F '[ /]+' '{print $3}'
192.168.88.111

也可以合并成一条 awk

bash 复制代码
[root@atguigu ~]# ip a s ens33 | awk -F '[ /]+' 'NR==3 {print $3}'
192.168.88.111

这里的分隔符是:

bash 复制代码
-F '[ /]+'

含义是:用一个或多个空格或 / 作为分隔符。

第 3 行内容大概是:

bash 复制代码
    inet 192.168.88.111/24 brd 192.168.88.255 scope global noprefixroute ens33

[ /]+ 分隔后:

字段 内容
$1 空字段或 inet,取决于前导空格处理细节
$2 inet192.168.88.111
$3 192.168.88.111
$4 24

不同系统输出格式可能略有差异。单纯获取本机 IP 时,更直接的命令是:

bash 复制代码
hostname -I

但这个例子很适合用来理解 awk 的行列组合能力。

6.2 提取内存信息

查看内存:

bash 复制代码
[root@atguigu ~]# free
              total        used        free      shared  buff/cache   available
Mem:        2027896      220472     1685724        9748      121700     1666284
Swap:       2097148           0     2097148

如果要取 Swap 的 used 值,也就是第 3 行第 3 列:

bash 复制代码
[root@atguigu ~]# free | awk 'NR==3 {print $3}'
0

如果要计算内存可用容量百分比:

bash 复制代码
[root@atguigu ~]# free | awk 'NR==2 {print $7/$2*"100%"}'
82.1671%

这里:

  • NR==2 表示只处理 Mem: 这一行;
  • $2 是总内存;
  • $7 是 available;
  • $7/$2 得到可用比例;
  • 乘以 "100%" 时,awk 会把前面的计算结果乘以 100,再拼上 % 的显示效果。

更推荐写得清楚一点:

bash 复制代码
free | awk 'NR==2 {print ($7/$2)*100"%"}'

如果希望保留两位小数,可以使用 printf

bash 复制代码
free | awk 'NR==2 {printf "available: %.2f%%\n", ($7/$2)*100}'

6.3 过滤 /etc/passwd 指定字段

/etc/passwd 是冒号分隔文件,字段含义大致如下:

bash 复制代码
用户名:密码占位符:UID:GID:注释:家目录:登录 Shell

查看第 1、3、4 列:

bash 复制代码
awk -F':' '{print $1,$3,$4}' /etc/passwd

查询第 3 列以 0 或 1 开头的行,并输出第 1、3、4 列:

bash 复制代码
[root@atguigu ~]# awk -F':' '$3~/^[01]/ {print $1,$3,$4}' /etc/passwd

查询第 3 列大于 100 的行,并输出第 1、3、最后一列:

bash 复制代码
[root@atguigu ~]# awk -F':' '$3>100 {print $1,$3,$NF}' /etc/passwd

这里把行过滤和列输出结合起来了:

bash 复制代码
条件 {动作}

对应到这条命令就是:

bash 复制代码
$3>100 {print $1,$3,$NF}

意思是:如果第 3 列大于 100,就输出第 1 列、第 3 列和最后一列。

七、awk 统计计算

7.1 统计行数

统计 test.txt 行数:

bash 复制代码
[root@atguigu ~]# awk '{i=i+1} END{print i}' test.txt
10

这条命令可以拆开理解:

bash 复制代码
{i=i+1}

每读取一行,就让变量 i 加 1。

bash 复制代码
END{print i}

文件读完后,打印变量 i

awk 中变量没有提前赋值时,参与数字计算会按 0 处理,所以第一行读取时相当于:

bash 复制代码
i=0+1

更常见的简写是:

bash 复制代码
awk '{i++} END{print i}' test.txt

其实统计行数也可以直接打印 NR

bash 复制代码
awk 'END{print NR}' test.txt

因为读取到最后一行时,NR 正好就是总行数。

7.2 统计空行数量

统计 /etc/services 中空行数量:

bash 复制代码
[root@atguigu ~]# awk '/^$/' /etc/services | wc -l
17

也可以直接在 awk 内部计数:

bash 复制代码
[root@atguigu ~]# awk '/^$/ {i++} END{print i}' /etc/services
17

两种方式都能实现:

写法 思路
`awk '/^$/' file wc -l`
awk '/^$/ {i++} END{print i}' file awk 自己筛选并计数

第二种写法更能体现 awk 的统计能力。

7.3 对字段求和

test.csv 中年龄求和:

bash 复制代码
[root@atguigu ~]# awk -F',' '{sum=sum+$2} END{print sum}' test.csv
324

可以简写成:

bash 复制代码
awk -F',' '{sum+=$2} END{print sum}' test.csv

执行过程如下:

读取行 $2 sum
张伟 28 28
李芳 34 62
王强 42 104
... ... ...
孙芳 33 324

最后在 END 中输出 sum

如果文件是空格分隔的 test.txt,可以不写 -F','

bash 复制代码
awk '{sum+=$2} END{print sum}' test.txt

7.4 分组计数与分组求和

统计男生年龄总和:

bash 复制代码
[root@atguigu ~]# awk '$3=="男" {sum+=$2} END{print sum}' test.txt
170

统计男生数量:

bash 复制代码
[root@atguigu ~]# awk '$3=="男" {count++} END{print count}' test.txt
5

统计女生年龄总和:

bash 复制代码
[root@atguigu ~]# awk '$3=="女" {sum+=$2} END{print sum}' test.txt
154

统计女生数量:

bash 复制代码
[root@atguigu ~]# awk '$3=="女" {count++} END{print count}' test.txt
5

如果想一次性统计男女数量,可以用数组:

bash 复制代码
[root@atguigu ~]# awk '{count[$3]++} END{for(i in count) print i,count[i]}' test.txt
男 5
女 5

数组先不用想得太复杂。这里可以理解为:

  • count["男"]++:男出现一次就加 1;
  • count["女"]++:女出现一次就加 1;
  • END 中遍历数组,把每个类别和数量打印出来。

7.5 计算平均值

计算所有人的平均年龄:

bash 复制代码
[root@atguigu ~]# awk '{sum+=$2;count++} END{print sum/count}' test.txt
32.4

保留两位小数:

bash 复制代码
[root@atguigu ~]# awk '{sum+=$2;count++} END{printf "%.2f\n",sum/count}' test.txt
32.40

计算男生平均年龄:

bash 复制代码
[root@atguigu ~]# awk '$3=="男" {sum+=$2;count++} END{print sum/count}' test.txt
34

计算女生平均年龄:

bash 复制代码
[root@atguigu ~]# awk '$3=="女" {sum+=$2;count++} END{print sum/count}' test.txt
30.8

为了避免没有匹配数据时除以 0,可以写得更稳一点:

bash 复制代码
awk '$3=="男" {sum+=$2;count++} END{if(count>0) print sum/count; else print 0}' test.txt

7.6 计算内存可用率

使用 free 计算内存 available 百分比:

bash 复制代码
[root@atguigu ~]# free | awk 'NR==2 {printf "available: %.2f%%\n", ($7/$2)*100}'
available: 82.17%

如果要输出已用率,可以使用 used / total

bash 复制代码
free | awk 'NR==2 {printf "used: %.2f%%\n", ($3/$2)*100}'

也可以同时输出两项:

bash 复制代码
free | awk 'NR==2 {printf "used: %.2f%%, available: %.2f%%\n", ($3/$2)*100, ($7/$2)*100}'

这种写法经常用于脚本巡检。相比手动看 free 输出,直接算出百分比更适合告警判断。

7.7 查找最大值和最小值

输出年龄最大的人:

bash 复制代码
[root@atguigu ~]# awk '$2>max {max=$2;name=$1} END{print "name:"name",age:"max}' test.txt
name:王强,age:42

这条命令的逻辑是:

  • 每读取一行,就拿 $2 和当前 max 比较;
  • 如果 $2 更大,就更新 maxname
  • 读完后输出最终保存的姓名和最大年龄。

输出年龄最小的人,可以这样写:

bash 复制代码
[root@atguigu ~]# awk 'NR==1 || $2<min {min=$2;name=$1} END{print "name:"name",age:"min}' test.txt
name:李明,age:25

这里用 NR==1 初始化最小值。否则 min 默认按 0 处理,所有年龄都大于 0,就无法正确更新最小值。

输出年龄最大的男生:

bash 复制代码
[root@atguigu ~]# awk '$3=="男" && $2>max {max=$2;name=$1} END{print "name:"name",age:"max}' test.txt
name:王强,age:42

输出年龄最小的男生:

bash 复制代码
[root@atguigu ~]# awk '$3=="男" && (count==0 || $2<min) {min=$2;name=$1;count++} END{print "name:"name",age:"min}' test.txt
name:李明,age:25

输出年龄最大的女生:

bash 复制代码
[root@atguigu ~]# awk '$3=="女" && $2>max {max=$2;name=$1} END{print "name:"name",age:"max}' test.txt
name:李芳,age:34

输出年龄最小的女生:

bash 复制代码
[root@atguigu ~]# awk '$3=="女" && (count==0 || $2<min) {min=$2;name=$1;count++} END{print "name:"name",age:"min}' test.txt
name:吴丽,age:27

最大值一般可以直接从默认 0 开始比较;最小值建议用第一条有效记录初始化,这样不容易出错。

八、常见运维场景

场景一:查看磁盘使用率超过阈值的分区

bash 复制代码
df -Th | awk -F '[ %]+' 'NR>1 && $6>=10'

含义:

  • df -Th 查看文件系统类型和容量;
  • -F '[ %]+' 使用空格和 % 作为分隔符;
  • NR>1 跳过表头;
  • $6>=10 过滤使用率大于等于 10 的行。

生产环境中阈值通常不是 10,而是 80、85、90 这类值:

bash 复制代码
df -Th | awk -F '[ %]+' 'NR>1 && $6>=85 {print $1,$6"%",$NF}'

场景二:提取进程列表中的指定字段

查看进程时只输出用户、PID、CPU、内存和命令:

bash 复制代码
ps aux | awk 'NR==1 {print $1,$2,$3,$4,$11} NR>1 {print $1,$2,$3,$4,$11}'

如果只看 CPU 使用率大于 10 的进程:

bash 复制代码
ps aux | awk 'NR>1 && $3>10 {print $1,$2,$3,$11}'

场景三:统计某类日志数量

统计日志中包含 ERROR 的行数:

bash 复制代码
awk '/ERROR/ {count++} END{print count}' app.log

统计每种状态码出现次数,假设状态码在第 9 列:

bash 复制代码
awk '{code[$9]++} END{for(i in code) print i,code[i]}' access.log

如果要排序,可以交给 sort

bash 复制代码
awk '{code[$9]++} END{for(i in code) print i,code[i]}' access.log | sort -k2 -nr

场景四:从配置文件中提取有效配置

过滤空行和注释行:

bash 复制代码
awk '!/^#/ && !/^$/' nginx.conf

如果配置文件前面有空格,可以写得更稳一点:

bash 复制代码
awk '!/^[[:space:]]*#/ && !/^[[:space:]]*$/' nginx.conf

这个命令经常用于快速查看"真正生效的配置内容"。

九、复习检查点

可以用下面这些问题检查自己是否掌握了本篇重点:

  1. awk '{print $1,$2}' test.txt$1$2 分别表示什么?
  2. $0NF$NFNR 的区别是什么?
  3. 为什么逗号分隔文件要使用 awk -F','
  4. BEGIN{}END{} 分别在什么时候执行?
  5. awk '$2>30' test.txt 为什么可以筛出年龄大于 30 的行?
  6. $1~/张//张/ 有什么区别?
  7. awk 'NR>=3&&NR<=5' test.txtawk 'NR==3,NR==5' test.txt 有什么相似点?
  8. 统计某列总和时,为什么要在主体中累加,在 END 中输出?
  9. 求最小值时,为什么不能简单依赖变量默认值 0?
  10. df -Th | awk -F '[ %]+' 'NR>1 && $6>=85' 这条命令每一部分分别是什么意思?

总结

awk 的核心不是命令有多长,而是处理思路很清晰:

  1. 一行一行读取文本;
  2. 按分隔符切成多个字段;
  3. NR、正则或字段条件筛选目标行;
  4. $1$2$NF 等字段变量取出目标列;
  5. 需要统计时,用变量在主体中累加,在 END 中输出结果。

只要抓住"行"和"列"这两个维度,awk 就不难理解。

常用口诀可以记成:

NR 管行号,NF 管列数;$1 取第一列,$NF 取最后列;-F 定分隔符,END 出统计结果。

实际运维中,awk 很少单独存在,更多时候是和管道一起使用:

bash 复制代码
df -Th | awk ...
free | awk ...
ip a | awk ...
ps aux | awk ...
cat file | awk ...

它的价值就在这里:前面的命令负责产生文本,awk 负责把文本整理成我们真正需要的数据。

十、注意事项

  • awk 默认分隔符是空格和制表符,处理逗号、冒号、竖线分隔文件时要使用 -F
  • $0 表示整行,$1 表示第 1 列,NF 表示字段数量,$NF 表示最后一列,不要混用。
  • 输出中文、英文等字符串时要加引号,数字可以不加引号。
  • 字段比较字符串时也要加引号,例如 $3=="男"
  • 正则匹配整行用 /正则/,匹配指定字段用 $字段~/正则/
  • 管道输出格式可能因系统版本不同略有差异,写脚本前要先确认字段位置。
  • 求最小值时要先初始化,常见写法是 NR==1 || $2<min,或者使用计数变量判断第一条有效记录。
  • 统计结果建议在 END{} 中输出,否则每读取一行都会输出一次中间结果。
  • 如果数据中字段可能包含空格,例如 CSV 中的带引号字段,普通 awk -F',' 不能完整处理复杂 CSV,需要使用专门工具或脚本解析。
  • 运维脚本中使用 awk 做阈值判断时,要明确单位和字段位置,避免把表头、百分号或不同命令格式当成普通数据处理。
相关推荐
空堂与归2 小时前
Linux 命令实战:AI 开发者必备的终端操作手册
linux
QYR-分析3 小时前
注塑机机械手自动化装备行业发展研判:存量升级催生增量红利,2032 年市场规模逼近 9 亿美元
运维·自动化
我命由我123453 小时前
匈牙利命名法
java·服务器·后端·学习·java-ee·kotlin·学习方法
小田的博客3 小时前
SAP MM 供应商银行主数据更新报错!message R1228!
android·java·服务器
牢姐与蒯3 小时前
Linux基本指令及知识点(二)
linux·运维·服务器
小玮看世界4 小时前
[Python]从“脏”数据到优雅实现:一个IoT滑动窗口最大值问题的测试驱动优化实录
linux·前端·python
ᥬ 小月亮4 小时前
SonarQube使用教程(Docker安装)
运维·docker·容器
去伪存真20254 小时前
数字样机、态势推演与3D汇报场景下的平台选型对比
运维·服务器
自动化测试行业观察4 小时前
从“自动化”到“智能化”:TestMan AI测试平台引领软件测试范式转移
运维·自动化测试·人工智能·测试工具·自动化·app测试·移动应用测试
xiaoxiangsiyan4 小时前
运维之前端反调试学习
运维·前端·学习·状态模式