
awk文字处理小笔记(下)
使用
1.动作(Action)
动作写在{ }中,可以包含输出语句、赋值、条件判断和循环等。
1 | echo 'this is a test' | awk '{print $1,$4}' |
1 | this test |
{ }围出一个动作块print $1,$4是动作块中的输出语句- 同一动作块可以写多条语句,用分号或换行分隔
if条件语句
看日志时,常见的需求是根据HTTP状态码给请求分类。这里用一份简化的日志演示,格式为路径 状态码 耗时毫秒。
1 | cat > requests.log <<'EOF' |
1 | / 200 未命中4xx/5xx |
这里假设输入是有效的HTTP状态码,只演示4xx、5xx的分类。其他状态不直接标成“成功”,例如3xx还可能表示重定向。
日志不是越长越有用,先把值得看的几行捞出来,眼睛也能少加点班。
while循环
打印数字1~10。
1 | awk 'BEGIN {i=1; while (i<=10) {print i; i++}}' |
1 | 1 |
i++让变量每次加1,避免一直停留在同一个条件中。
do…while循环
1 | echo 'this is a test' | awk '{i=1; do {print $i; i++} while (i<=NF)}' |
1 | this |
do...while会先执行一次,再判断条件。因此遇到空行、NF=0时,上述写法仍会输出一次空内容。
如果需要逐个输出字段,并跳过空行,使用下面的for更直接。
for循环
1 | awk 'BEGIN {for (i=1; i<=5; i++) {print i}}' |
1 | 1 |
逐个输出每行的字段
1 | echo 'this is a test' | awk '{for (i=1; i<=NF; i++) {print $i}}' |
输出同上面的do...while示例,但遇到空行时不会进入循环。
补充:格式化输出
printf可以控制列宽、小数位和换行。
1 | printf '%s\n' 'apple 3.5' 'banana 12' | awk '{printf "%-8s %6.2f\n",$1,$2}' |
1 | apple 3.50 |
%-8s:字符串按至少8个字符宽度左对齐%6.2f:浮点数按至少6个字符宽度输出,保留2位小数printf不会自动添加ORS,这里用\n手动换行
2.数组
awk中的数组是关联数组,可以用字符串作为下标,不需要提前声明大小。
例如,按服务名称保存状态,比记住“第1个是谁、第2个是谁”更直观。
1 | 下标(key) 值(value) |
创建数组
格式:数组名[下标]=值
访问:数组名[下标]
1 | awk 'BEGIN { |
1 | running |
删除数组元素
格式:delete 数组名[下标]
1 | awk 'BEGIN { |
1 | running |
"api" in service用于判断下标是否存在,返回0表示不存在。直接输出一个不存在的元素,通常只会显示空内容,不能清楚地说明是否已删除。
补充:按日志级别统计
1 | printf '%s\n' 'INFO' 'ERROR' 'INFO' | awk '{count[$1]++} END {for (key in count) print key,count[key]}' | sort |
1 | ERROR 1 |
for (key in count)遍历数组下标,遍历顺序不保证固定。这里通过sort让结果按名称排序。
3.函数
内置函数大致包括算术函数、字符串函数等;awk还支持自定义函数。
算术函数
rand()产生大于等于0、小于1的伪随机数。调用多次会得到一个序列,并不是每次调用都返回相同的值。
通常需要配合srand()设置种子。
1 | awk 'BEGIN {srand(); print int(rand()*100)}' |
输出为0~99之间的整数,实际结果不固定。
💡提示
不设种子时,同一awk实现的不同运行往往从同一个默认种子开始,因此容易得到相同的序列。
srand()常按时间设置种子,短时间内重复启动也可能得到相同序列。
截取整数部分
1 | awk 'BEGIN {print int(3.8),int(-3.8)}' |
1 | 3 -3 |
注意,截取整数部分的函数名是int()。
字符串函数
1.sub()和gsub()
sub(正则, 替换内容, 目标):替换第一个匹配gsub(正则, 替换内容, 目标):替换所有匹配- 省略目标时,默认修改
$0;返回值是替换次数
1 | echo 'this is a test' | awk '{gsub(/t/,"T",$1); print $0}' |
1 | This is a test |
这里只有第1列中的t被替换。对字段赋值后,awk会用OFS重新拼接$0,原有多余空白可能随之改变。
2.length()
1 | echo 'this is a test' | awk '{print $0,length($0)}' |
1 | this is a test 14 |
空格也计算在长度中,但记录末尾作为输入分隔符的换行不包含在$0里。
3.index()
1 | echo 'this is a test' | awk '{print index($0,"a")}' |
1 | 9 |
位置从1开始计数,没有找到时返回0。
4.substr()
1 | echo 'this is a test' | awk '{print substr($0,11,4)}' |
1 | test |
从第11个位置开始,取4个字符。
补充:自定义函数
1 | awk 'function square(x) {return x*x} BEGIN {print square(5)}' |
1 | 25 |
总结
(1)动作块可以包含输出、赋值、条件判断和循环
(2)awk数组无需预先声明大小,可以用字符串作为下标
(3)sub()替换第一个匹配,gsub()替换所有匹配
(4)print默认追加ORS,printf需要自己安排换行
(5)统计总和、平均值等结果,通常放在END中输出
练习
基础使用
为了让结果便于核对,先创建几份小文件。nums.txt里的数字可以看作请求耗时(毫秒),用它练习总和、平均值和最大值。
1 | printf '%s\n' '114' '191' '666' '888' > nums.txt |
1.统计文件中的行数
1 | awk 'END {print NR}' nums.txt |
1 | 4 |
默认情况下NR是行数;自定义RS后,应理解为记录数。
2.计算数字总和
1 | awk '{sum+=$1} END {print sum+0}' nums.txt |
1 | 1859 |
sum+0让空文件也能输出数字0。
3.查找包含关键字的行
1 | awk '/vim/' tools.txt |
1 | vim is a tool for editing |
4.替换文本
1 | awk '{gsub(/vim/,"vi")} 1' tools.txt |
1 | bash |
结尾的1是一个始终为真的模式,没有动作时默认打印当前记录。
5.删除指定行
1 | awk 'NR != 2' tools.txt |
1 | bash |
6.提取第1列
1 | awk '{print $1}' tools.txt |
1 | bash |
7.计算第1列的平均值
1 | awk '{sum+=$1} END {if (NR>0) print sum/NR; else print "没有数据"}' nums.txt |
1 | 464.75 |
空文件时不计算,避免除以0。这个练习假设每行都有一个有效数字。
8.找到第1列的最大值
1 | awk 'NR==1 || $1>max {max=$1} END {if (NR>0) print "最大值是:",max; else print "没有数据"}' nums.txt |
1 | 最大值是: 888 |
先使用第1条记录初始化,避免数据全为负数时错误地输出0。请求耗时通常非负,但换成余额或温度数据时,这个细节就会派上用场。
9.删除重复行,保留第一次出现的顺序
1 | awk '!seen[$0]++' words.txt |
1 | berry null |
seen[$0]以整行为下标。后置++先返回原值再递增:第一次出现时原值为0,取反后为真,因此输出;再次出现时就不会输出。
10.打印第5~10行
1 | awk 'NR>=5 && NR<=10' /proc/cpuinfo |
/proc/cpuinfo属于Linux系统文件,实际内容随机器变化。
11.计算每行字符串的长度
1 | awk '{print length($0)}' tools.txt |
1 | 4 |
12.使用冒号分隔字段,输出结果的最后10行
1 | awk -F ':' '{print $1,$3}' /etc/passwd | tail -n 10 |
13.将小写字母转换为大写
1 | awk '{print toupper($0)}' tools.txt |
1 | BASH |
14.打印用户名为root的行
1 | awk -F ':' '$1 == "root" {print}' /etc/passwd |
15.统计文本长度,以及文件字节数
1 | awk '{sum+=length($0)} END {print sum+0}' tools.txt |
1 | 32 |
这里统计的是每条记录的字符串长度之和,不包含记录分隔符。字符与字节的关系还受编码和awk实现影响,因此不能把这个结果当成文件字节大小。
如果需要准确的文件字节数,使用:
1 | wc -c < tools.txt |
1 | 35 |
这份文件全部是ASCII字符,另有3个换行字节,所以共35字节。wc输出数字前可能带有空格。
小应用
1.格式化空白
移除每行前后空白,并统一字段之间的分隔符。
1 | cat > 1.txt <<'EOF' |
1 | aaaa bbb ccc |
BEGIN设置输出字段分隔符为制表符$1=$1给第1个字段重新赋值,触发$0的重建print输出重建后的记录,字段之间使用OFS
💡提示
这会统一空白,但不保证视觉上各列一定对齐。制表符的显示与终端制表位有关;需要固定列宽时可使用
printf。
2.获取网卡的IPv4地址
如果直接写{print "ens33:"},没有模式条件,会对每行重复输出标题。可以将标题放入BEGIN,也可以把网卡名和地址放在同一行输出。
使用Linux的ip命令
1 | ip -4 -o addr show dev ens33 | awk '{split($4,addr,"/"); print $2 ":" addr[1]}' |
模拟输入
1 | printf '%s\n' '2: ens33 inet 192.168.10.20/24 brd 192.168.10.255 scope global ens33' | awk '{split($4,addr,"/"); print $2 ":" addr[1]}' |
1 | ens33:192.168.10.20 |
-4只查看IPv4,-o让每个地址占一条输出记录$4包含地址和前缀长度,例如192.168.10.20/24split()按斜线拆分,addr[1]就是地址ens33需要替换成实际网卡名称
如果使用Linux中常见的ifconfig输出格式,也可以写:
1 | ifconfig ens33 | awk '$1=="inet" {print "ens33:" $2}' |
旧版inet addr:...格式需要另外处理,不能假定所有环境的第2列都是纯IP地址。
3.统计TCP连接状态数量
先提取状态字段,再排序、计数。
1 | netstat -tna | awk '$1 ~ /^tcp/ {print $6}' | sort | uniq -c |
这条命令针对Linux中常见的netstat输出:第1列为协议,第6列为状态。
也可以直接使用awk数组计数。
1 | netstat -tna | awk '$1 ~ /^tcp/ {count[$6]++} END {for (state in count) print state,count[state]}' | sort |
Linux中使用ss时,状态通常是第1列,不能照搬$6。
1 | ss -tanH | awk '{count[$1]++} END {for (state in count) print state,count[state]}' | sort |
模拟状态统计
1 | printf '%s\n' 'ESTAB' 'LISTEN' 'ESTAB' 'TIME-WAIT' | awk '{count[$1]++} END {for (state in count) print state,count[state]}' | sort |
1 | ESTAB 2 |
4.生成随机数
1 | awk 'BEGIN { |
srand()初始化种子rand()返回[0,1)范围内的伪随机数- 乘以100,再通过
int()截去小数,得到0~99之间的整数
这个方法适合简单的随机演示,不适合生成密码或密钥。








