cut、awk 使用总结
在 awk、sed、cut 三个命令中,awk 是功能最强大的,基本能实现所有字符串操作,平时常用于较复杂的日志分析,不过比起别的命令来也会相对复杂一点。
为什么要用 awk —— 比较 awk、sed 和 cut
sed 是流编辑器,它逐行取出文本内容再进行处理,和我们平时使用的交互式文本编辑器差不多,比如插入新行、修改某行、根据正则匹配某行的同时执行修改。
awk 是报表生成工具,同样是逐行取出文件,但是取出的目的是对内容进行二次加工,然后将有用的数据单独格式化输出、或进行归纳统计得到统计结果等。
比如,统计日志中某 IP 出现的次数,awk 很方便做到但是 sed 就很困难;再如,要在文本某一行之前插入一行,sed 很容易做到但是 awk 就比较麻烦。
cut 有点像一个简化版的 awk,专门用来分割文本并取需要的列进行显示,如果只用到这个功能,使用 cut 写起来会更简练方便。
使用 cut
cut 命令用于按“列”来提取文本字符,格式为:cut [参数] 文本
。
1 | # 以:作为分隔符,且仅看第一列 |
awk 命令参数
1 | $ awk '{print 0}' test.txt |
awk 语法
- 基本用法动作一般会用
1
2
3$ awk 动作 文件名
例子:
$ awk '{print 0}' test.txt{
和}
包起来,awk 会读取文件中的每一行,然后对每一行执行一次动作 - 转义注意这里使用到了一对反斜杠
1
2$ awk '{match($0, "\\[(.+)\\]", a); print a[1];}' test.txt
$ awk '{ if($0 ~ "^\\[TraceId.*") print $0 }' catalina.out\\
,是因为 awk 本身是一个语言,它会先有一个编译的过程,然后再用正则表达式引擎去解释,如果是\.
,则会在第一步将其解释为.
,在第二步时就会匹配任意字符 - 条件
1
2
3
4
5
6
7$ awk '条件 动作' 文件名
或者
$ awk '{if (条件) 动作}' 文件名
$ awk '{if (条件) 动作1; else 动作2}' 文件名
例子:
$ awk -F ':' '/usr/ {print $1}' test.txt
$ awk -F ':' '{if ($1 > "m") print $1; else print "---"}' test.txt - 预处理和结束处理
有时候需要在命令执行前初始化一些全局变量,或者需要在文件的全部行遍历完毕后输出一些结果,可以考虑使用BEGIN
、END
语句块1
$ awk 'BEGIN{a=0}{a+=1; print NR}END{print "行数=", a}' test.txt
内置变量
- NF:NF 表示目前的记录被分割的字段的数目,NF 可以理解为 Number of Field。
- NR:NR 表示从 awk 开始执行后,按照记录分隔符读取的数据次数,默认的记录分隔符为换行符,因此默认的就是读取的数据行数,NR 可以理解为 Number of Record 的缩写。
- FNR:在 awk 处理多个输入文件的时候,在处理完第一个文件后,NR 并不会从 1 开始,而是继续累加,因此就出现了 FNR,每当处理一个新文件的时候,FNR 就从 1 开始计数,FNR 可以理解为 File Number of Record。
1
2
3# 从第二个文件中排除掉第一个文件中已出现的数据
# https://blog.csdn.net/stanjiang2010/article/details/6184458
awk -F ':' 'NR==FNR{a[$1]=1;}NR>FNR{if(a[$1]!=1){print $0;}}' a.txt b.txt - FILENAME:当前文件名
- FS:字段分隔符,默认是空格和制表符。
- RS:行分隔符,用于分割每一行,默认是换行符。
- OFS:输出字段的分隔符,用于打印时分隔字段,默认为空格。
- ORS:输出记录的分隔符,用于打印时分隔记录,默认为换行符。
- OFMT:数字输出的格式,默认为%.6g。
内建函数
toupper():字符转为大写
tolower():字符转为小写。
sin():正弦。
cos():余弦。
sqrt():平方根。
rand():随机数。
length
length 函数返回整个记录中的字符数。1
2echo "123" | awk '{print length}'
echo "123 4567" | awk -F ' ' '{print length($2)}'substr
返回从起始位置起,指定长度之子字符串;若未指定长度,则返回从起始位置到字符串末尾的子字符串。
格式:1
2substr(s,p) # 返回字符串s中从p开始的后缀部分
substr(s,p,n) # 返回字符串s中从p开始长度为n的后缀部分split
split 允许把一个字符串分割为单词存储在数组中,可以自己定义域分隔符,或者使用现在 FS(域分隔符)的值。
格式:1
2split(string, array, field separator)
split(string, array) # 如果第三个参数没有提供,awk就默认使用当前FS值示例:
1
2
3awk -F ',' '{print substr($3,6)}' ---> 表示是从第3个字段里的第6个字符开始,一直到设定的分隔符","结束.
substr($3,10,8) ---> 表示是从第3个字段里的第10个字符开始,截取8个字符结束.
substr($3,6) ---> 表示是从第3个字段里的第6个字符开始,一直到结尾gsub
gsub 函数则使得在所有正则表达式被匹配的时候都发生替换。
格式:1
2gsub(regular expression, subsitution string, target string);
简称 gsub(r,s,t)。示例:
1
2
3# 把一个文件里面所有包含 abc 的行里面的 abc 替换成 defg,然后输出第一列和第三列
# ~是包含,!~是不包含
awk '$0 ~ /abc/ {gsub("abc", "defg", $0); print $1, $3}' abc.txtmatch
match 匹配给定字符串并提取部分内容,并将第一次匹配的位置下标和子串长度分别赋给内置变量 RSTART 和 RLENGTH。
格式:1
match(string, regexp [, array])
在正则表达式中,可以使用’()’将要提取的成分标出,然后就可以在结果数组里面取到了。
示例:1
echo foooobazbarrrrr | awk '{ match($0, /(fo+).+(bar*)/, arr); print arr[1], arr[2];}'
经试验,awk 的正则表达式似乎不支持惰性匹配:
1
2
3$ echo '123,123,123' | awk '{match($0, "(.*?),", a); print a[1];}'
123,123理想的输出结果是”123”,但是结果却输出了”123,123”,这是贪婪匹配的结果。
为了让 awk 能正确匹配,需要关注内容的格式,真实环境中像”123,123,123”这样重复的是比较少的,比如下面的脚本利用文本内容中”1”、”2”、”3”不重复的特征,匹配中第一对”AB”、”BA”中间的序列:1
2
3
4
5
6
7
8
9BEGIN {
str="AB1BA2AB3BA"
regex="AB([^高迟,30,,30,晚餐
2019.11.12,10:07:29,22:04:50,黄AB])*BA"
if (match(str,regex))
print substr(str,RSTART,RLENGTH)
}
AB1BAsystem
awk 可以调用 shell 的其他命令,不过拿不到命令执行结果,只能得到脚本执行结果的 state:1
awk '{system("echo "$0)}' result.csv
一些 stupid 操作
- 在遍历每一行时维持一个全局变量
全局变量不管在哪个语言内都需要谨慎对待。1
awk '{a+=$(NF-2); print "Total so far:", a}' logs.txt
应用
去掉空行
1 | awk '{if($0 != "") print $0;}' test.txt |
花式分割字符串
awk 命令默认使用空格分割字符串,引用这些分割后的字符串可以使用$1、$2、$3…引用,也可以使用-F 选项设置分隔符:
1 | awk -F ':' '{print e$2}' test.txt |
也可以在BEGIN
块里修改FS
变量:
1 | awk 'BEGIN{FS=":"}{print $1}' test.txt |
复杂格式化
1 | awk -F ' ' '{print "insert into obfuscate_dictionary (dic_key, code) values('\''"$2"'\'', '\''"$3"'\'');"}' dics.txt > result.txt |
统计请求量
- 统计请求日志中某个时间段内每秒的请求量:
1
2# 内容格式:QTrace[链路标识] 19:01:12.123 content[日志内容]
sudo grep "19:0" request.log | awk -F ' ' '{print $2}' | awk -F. '{print$1}' | sort | awk -F: '{a[$1":"$2":"$3]++}END{for(i in a){split(i,t);print t[1]":"t[2]":"t[3]"访问"a[i]"次"}}' > result.txt - 统计 request.log 所有 url 请求数,按照次数降序排列
1
2
3
4
5
6# 请求日志request.log每一行的内容为"xxxx uri=xxxx, xxxx"
# [^,]表示匹配一个除','之外的字符
# substr($0, RSTART, RLENGTH)提取子串起始下标为RSTART,长度为RLENGTH,这里+4和-5是为了去掉结果中的"uri"前缀和","后缀,如果有必要,还可以接上`| awk '{ sub("^ *", ""); sub(" *$", ""); print }'`这样的命令来去掉开头和末尾的空格
# uniq可以用于去重,但是当重复的行不相邻时,uniq命令是不起作用的,所以要先用sort进行排序
# 最后使用sort排序输出含有比较多的选项,-r: 逆序,-n: 当做数字进行排序,-k 1: 按第一列排序
$ awk '{ match($0, "uri[^,]*,"); print substr($0, RSTART + 4, RLENGTH - 5) }' request.log | sort | uniq -c | sort -r -n -k 1 - 统计每 10 秒的请求量
原理其实都是差不多的,剩下的就是格式化了。1
2# 假设格式为"[2019-09-26 11:11:11.111] code=123,xxxxx"
grep "过滤条件" request.log | grep "H114801" | awk '{match($0, "\\[....-..-.. (..:..:.).\\....\\].*code=(.*),.*", a); b[a[1]"-"a[2]]++}END{for(i in b){split(i, t); print "时间:"t[1]" 编码:"t[2]" 请求量:"a[i]}}'
统计延时大于 100ms 的请求数
1 | # 示例请求日志格式为:request:...; response:...; cost:[123 ms] |
统计某个时间段内某个 userCode 的请求数
1 | # 内容格式:QTrace[链路标识] 19:01:12.123 INFO userCode=abc123, request=..., response=... |
按某一列去重并求和
1 | # 内容格式: |
过滤行
1 | # 内容格式:1 3 |
计算每个人一月份工资之和
1 | # 内容格式: |
将大文件按规则拆成小文件
1 | awk -F ',' '{print $3 > $2".txt"}' input.csv |
输入内容格式:
1 | 1,Mike,50$ |
参考
- awk 入门教程
- Why you should learn just a little Awk: An Awk tutorial by Example
- How To Use the AWK language to Manipulate Text in Linux
- Learn X in Y minutes
- 30 Examples For Awk Command In Text Processing
进阶
- 《AWK 程序设计语言》中文翻译
- shell-正则表达式
正则表达式分为三种:BREs、EREs、PREs,如果发现文本工具、语言中的正则语法和自己理解的不同,有可能是因为它们选择实现的正则。 - awk-模式匹配
- The GNU Awk User’s Guide
String Functions