说起AWK,我刚开始接触的时候真的是又爱又恨。爱它是因为处理文本数据简直太方便了,恨它是因为语法看起来就像天书一样。记得第一次用AWK的时候,我盯着屏幕上那些花括号和美元符号,心想这玩意儿到底是个啥…
不过现在回头看,AWK真的是运维工作中不可缺少的神器。处理日志、分析数据、格式化输出,基本上只要涉及到文本处理的地方,AWK都能派上用场。今天就把我这些年使用AWK的经验整理出来,保证让你看完就能上手。
顺便分享一个学习地址:https://www.bookstack.cn/books/junmajinlong-awk
AWK到底是个什么东西
AWK其实是三个人名字的缩写:Aho、Weinberger、Kernighan。这三位大神在1977年开发了这个工具,专门用来处理结构化的文本数据。
简单来说,AWK就是一个模式扫描和处理语言。它会逐行读取文件内容,然后根据你设定的规则来处理每一行。听起来很抽象对吧?我们直接看例子。
假设我有一个文件叫做users.txt
,内容是这样的:
张三 25 北京 工程师
李四 30 上海 产品经理
王五 28 深圳 设计师
如果我想打印出所有人的姓名和年龄,用AWK就是:
awk '{print $1, $2}' users.txt
输出结果:
张三 25
李四 30
王五 28
看到没?AWK默认用空格或制表符来分割每一行,$1
表示第一个字段,$2
表示第二个字段,以此类推。$0
表示整行内容。
基础语法和工作原理
AWK的基本语法结构是这样的:
awk 'pattern { action }' filename
其中pattern是匹配模式,action是要执行的动作。如果省略pattern,就对所有行执行action;如果省略action,就打印匹配的行。
AWK的工作流程大概是这样:
- 读取一行数据
- 检查是否匹配pattern
- 如果匹配,执行对应的action
- 继续下一行,直到文件结束
我刚开始学的时候总是搞不清楚这个流程,后来发现其实就像流水线一样,数据一行一行地过,AWK在旁边按规则处理。
内置变量,这些你必须知道
AWK有很多内置变量,掌握了这些变量,你的AWK水平就能上一个台阶。
NR(Number of Records):当前处理的行号
awk '{print NR, $0}' users.txt
这会在每行前面加上行号。
NF(Number of Fields):当前行的字段数量
awk '{print NF, $0}' users.txt
FS(Field Separator):字段分隔符,默认是空格
awk 'BEGIN{FS=","} {print $1}' data.csv
这个在处理CSV文件的时候特别有用。
RS(Record Separator):记录分隔符,默认是换行符
OFS(Output Field Separator):输出字段分隔符
awk 'BEGIN{OFS="|"} {print $1, $2}' users.txt
输出的时候字段之间就用竖线分隔了。
我记得有一次处理nginx日志,日志格式比较特殊,字段之间用的是制表符。当时我还傻乎乎地用空格来分割,结果数据全乱了。后来才知道要设置FS=“\t”。
模式匹配,让AWK更智能
AWK的模式匹配功能非常强大,可以根据不同的条件来处理数据。
正则表达式匹配:
awk '/工程师/ {print $1}' users.txt
这会打印出所有包含"工程师"的行中的姓名。
条件匹配:
awk '$2 > 27 {print $1, $2}' users.txt
打印年龄大于27的人的姓名和年龄。
范围匹配:
awk '/start/,/end/ {print}' logfile
打印从包含"start"的行到包含"end"的行之间的所有内容。
BEGIN和END:
awk 'BEGIN{print "姓名 年龄"} {print $1, $2} END{print "总共", NR, "条记录"}' users.txt
BEGIN在处理第一行之前执行,END在处理完所有行之后执行。这个功能我经常用来做统计,比如计算日志文件的总行数、平均值什么的。
实战案例,这些场景你肯定遇到过
处理Apache访问日志
假设我们有一个Apache访问日志,格式是这样的:
192.168.1.100 - - [25/Dec/2023:10:00:01 +0800] "GET /index.html HTTP/1.1" 200 1234
192.168.1.101 - - [25/Dec/2023:10:00:02 +0800] "POST /api/login HTTP/1.1" 404 567
我想统计每个IP的访问次数:
awk '{ip[$1]++} END{for(i in ip) print i, ip[i]}' access.log
这里用到了AWK的关联数组,ip[$1]++
表示以第一个字段(IP地址)为键,每次出现就加1。
计算文件大小总和
如果我想计算某个目录下所有文件的大小总和:
ls -l | awk '{sum += $5} END{print "总大小:", sum, "字节"}'
统计httpd服务访问次数,增加sort排序功能
[root@localhost shell_xuexi]# awk '{a[$1]++}END{for(i in a){print i"\t\t"a[i]}}' /var/log/httpd/access_log|sort -nr -k 2 |awk 'BEGIN{print"源IP""\t\t\t""访问次数"}{print $0}'
源IP 访问次数
192.168.196.1 131
192.168.196.128 107
[root@localhost shell_xuexi]#
查看ssh爆破
[root@localhost shell_xuexi]# awk '/Failed password/{ip[$11]++}END{for(a in ip)print a,ip[a]}' /var/log/secure
192.168.196.1 5
处理CSV文件
有一个员工信息的CSV文件:
姓名,年龄,部门,工资
张三,25,技术部,8000
李四,30,产品部,12000
我想计算平均工资:
awk -F',' 'NR>1 {sum+=$4; count++} END{print "平均工资:", sum/count}' employees.csv
NR>1
是为了跳过标题行,这个小技巧在处理带标题的数据文件时特别有用。
高级功能,让你的AWK更上一层楼
函数的使用
AWK内置了很多有用的函数:
length()
:返回字符串长度
awk '{if(length($1) > 5) print $1}' users.txt
substr()
:截取子字符串
awk '{print substr($1, 1, 2)}' users.txt # 打印姓名的前两个字符
gsub()
:全局替换
awk '{gsub(/工程师/, "Engineer"); print}' users.txt
split()
:分割字符串
awk '{split($4, arr, "/"); print arr[1]}' logfile
多文件处理
AWK可以同时处理多个文件,通过FILENAME变量可以知道当前处理的是哪个文件:
awk '{print FILENAME, NR, $0}' file1.txt file2.txt
自定义函数
AWK还支持自定义函数,虽然用得不多,但在复杂场景下很有用:
awk '
function max(a, b) {
return a > b ? a : b
}
{
result = max($1, $2)
print result
}' numbers.txt
常见的坑和注意事项
用AWK这么多年,我踩过不少坑,这里分享几个常见的:
字符串和数字的比较
# 错误的做法
awk '$2 > "25" {print}' users.txt
# 正确的做法
awk '$2 > 25 {print}' users.txt
AWK会自动进行类型转换,但有时候结果可能不是你想要的。我之前就因为这个问题调试了半天,明明数据看起来没问题,结果就是不对。
字段分隔符的陷阱
如果你的数据字段之间有多个空格,AWK默认会把连续的空格当作一个分隔符。但如果你手动设置了FS=" "(单个空格),那每个空格都会被当作分隔符,可能会产生空字段。
我记得有次处理一个格式不太规范的日志文件,字段之间的空格数量不一致,结果用默认分隔符处理得好好的,我非要画蛇添足设置FS,结果数据全乱了。
浮点数精度问题
awk 'BEGIN{print 0.1 + 0.2}' # 结果可能不是0.3
这不是AWK的问题,是计算机浮点数运算的通病。遇到这种情况,最好用整数运算或者设置合适的精度。
AWK的处理时机
awk处理时机,可以执行额外任务
- BEGIN 任务 执行1次,读取文档之前执行
- 逐行任务执行n次,读取文档时执行
- END 任务执行1次,读取文档之后执行
#实战使用案例
[root@localhost shell_xuexi]# awk -F: 'BEGIN{print "User\tUID\tHome"}{print $1"\t"$3"\t"$6}END{print"总计"NR"行"}' user
User UID Home
root 0 /root
bin 1 /bin
daemon 2 /sbin
adm 3 /var/adm
lp 4 /var/spool/lpd
总计5行
[root@localhost shell_xuexi]#
性能优化的一些小技巧
处理大文件的时候,性能就变得很重要了。我总结了几个优化技巧:
避免不必要的正则表达式
如果能用字符串比较就不要用正则:
# 慢
awk '/^error/ {print}' logfile
# 快
awk '$1 == "error" {print}' logfile
正则表达式虽然强大,但确实比字符串比较要慢一些。
合理使用BEGIN和END
把只需要执行一次的操作放在BEGIN或END中:
awk 'BEGIN{FS=","} {sum+=$3} END{print sum}' data.csv
处理大文件时的内存管理
如果要处理的文件很大,避免把所有数据都存在数组中,尽量边读边处理。我之前处理过一个几十GB的日志文件,一开始想把所有数据都加载到数组里统计,结果直接把服务器内存撑爆了。
实际工作中的AWK应用场景
说了这么多理论,我来分享几个实际工作中经常用到的场景。
监控系统资源
我经常用AWK来处理top命令的输出:
top -bn1 | awk '/^%Cpu/ {print "CPU使用率:", $2}'
分析nginx访问日志
统计访问最多的IP:
awk '{print $1}' access.log | sort | uniq -c | sort -nr | head -10
不过这个命令组合用了sort和uniq,纯AWK的写法是:
awk '{ip[$1]++} END{for(i in ip) print ip[i], i}' access.log | sort -nr | head -10
处理配置文件
有时候需要从配置文件中提取特定的配置项:
awk -F'=' '/^database_host/ {print $2}' config.ini
生成报表
我经常用AWK来生成简单的统计报表:
awk 'BEGIN{
print "日期\t\t访问量\t\t错误数"
print "================================"
}
{
date = substr($4, 2, 11)
total[date]++
if($9 >= 400) error[date]++
}
END{
for(d in total) {
printf "%s\t%d\t\t%d\n", d, total[d], error[d]+0
}
}' access.log
AWK vs 其他工具
很多人会问,AWK和sed、grep有什么区别?什么时候用哪个?
grep主要用于搜索和过滤,功能相对简单。sed主要用于文本替换和简单的编辑操作。AWK则更像一个编程语言,可以做复杂的数据处理和计算。
如果只是简单的搜索,用grep;如果是替换操作,用sed;如果需要做计算、统计或者复杂的数据处理,那AWK就是最佳选择。
当然,这三个工具经常组合使用。我有个同事特别喜欢写一行命令解决所有问题,经常能看到他写出grep | awk | sed这样的组合拳。
调试AWK脚本的技巧
AWK脚本复杂了之后,调试就成了问题。我总结了几个调试技巧:
使用print语句
最简单的方法就是在关键位置加print语句:
awk '{
print "处理第" NR "行:", $0
if($2 > 25) {
print "年龄大于25:", $1
}
}' users.txt
分步骤测试
复杂的AWK脚本可以分步骤测试,先确保每个部分都工作正常。
使用-v参数传递调试标志
awk -v debug=1 '{
if(debug) print "调试信息:", $0
# 正常处理逻辑
}' data.txt
一些实用的AWK一行命令
这些年积累了不少实用的AWK一行命令,分享给大家:
计算文件行数
awk 'END{print NR}' filename
打印特定行
awk 'NR==5' filename # 打印第5行
awk 'NR>=5 && NR<=10' filename # 打印5-10行
去重
awk '!seen[$0]++' filename
计算平均值
awk '{sum+=$1; count++} END{print sum/count}' numbers.txt
转置矩阵
awk '{for(i=1;i<=NF;i++) a[i,NR]=$i; max=(NF>max?NF:max)} END{for(i=1;i<=max;i++) {for(j=1;j<=NR;j++) printf "%s ", a[i,j]; print ""}}' matrix.txt
这个转置矩阵的命令看起来很复杂,但在处理某些数据格式转换的时候特别有用。
写在最后
AWK确实是个强大的工具,但也不要把它想得太复杂。我的建议是先掌握基本语法,然后在实际工作中遇到文本处理的需求时就想想能不能用AWK来解决。
刚开始可能会觉得语法有点奇怪,特别是那些美元符号和花括号。但用多了就习惯了,现在我看到结构化的文本数据,第一反应就是用AWK来处理。
记住几个关键点:AWK是逐行处理的,字段用$1、$2来表示,内置变量NR、NF、FS很有用,关联数组是AWK的杀手锏。掌握了这些,基本的文本处理就没问题了。
最重要的是多练习。找一些实际的数据文件,试着用AWK来处理,遇到问题就查文档或者搜索。我当初就是这样一点点摸索出来的,现在AWK已经成了我日常工作中不可缺少的工具。
有时候我会想,如果没有AWK,我的工作效率可能要降低一半。处理日志、分析数据、生成报表,这些原本需要写复杂脚本的工作,现在一行AWK命令就能搞定。
下次再遇到需要处理文本数据的场景,不妨试试AWK。说不定你会发现,原来文本处理可以这么简单!
如果这篇文章对你有帮助,别忘了点赞转发支持一下!想了解更多运维实战经验和技术干货,记得关注微信公众号@运维躬行录,领取学习大礼包!!!我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习,共同进步!
公众号:运维躬行录
个人博客:躬行笔记