cut、awk 使用总结

在 awk、sed、cut 三个命令中,awk 是功能最强大的,基本能实现所有字符串操作,平时常用于较复杂的日志分析,不过比起别的命令来也会相对复杂一点。

为什么要用 awk —— 比较 awk、sed 和 cut

sed 是流编辑器,它逐行取出文本内容再进行处理,和我们平时使用的交互式文本编辑器差不多,比如插入新行、修改某行、根据正则匹配某行的同时执行修改。
awk 是报表生成工具,同样是逐行取出文件,但是取出的目的是对内容进行二次加工,然后将有用的数据单独格式化输出、或进行归纳统计得到统计结果等。
比如,统计日志中某 IP 出现的次数,awk 很方便做到但是 sed 就很困难;再如,要在文本某一行之前插入一行,sed 很容易做到但是 awk 就比较麻烦。
cut 有点像一个简化版的 awk,专门用来分割文本并取需要的列进行显示,如果只用到这个功能,使用 cut 写起来会更简练方便。

使用 cut

cut 命令用于按“列”来提取文本字符,格式为:cut [参数] 文本

1
2
# 以:作为分隔符,且仅看第一列
cut -d: -f1 /etc/passwd

awk 命令参数

1
2
3
$ awk '{print 0}' test.txt
# 读取脚本文件
$ awk -f test.awk 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
  • 预处理和结束处理
    有时候需要在命令执行前初始化一些全局变量,或者需要在文件的全部行遍历完毕后输出一些结果,可以考虑使用BEGINEND语句块
    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
    2
    echo "123" | awk '{print length}'
    echo "123 4567" | awk -F ' ' '{print length($2)}'
  • substr
    返回从起始位置起,指定长度之子字符串;若未指定长度,则返回从起始位置到字符串末尾的子字符串。
    格式:

    1
    2
    substr(s,p) # 返回字符串s中从p开始的后缀部分
    substr(s,p,n) # 返回字符串s中从p开始长度为n的后缀部分
  • split
    split 允许把一个字符串分割为单词存储在数组中,可以自己定义域分隔符,或者使用现在 FS(域分隔符)的值。
    格式:

    1
    2
    split(string, array, field separator)
    split(string, array) # 如果第三个参数没有提供,awk就默认使用当前FS值

    示例:

    1
    2
    3
    awk -F ',' '{print substr($3,6)}'    --->  表示是从第3个字段里的第6个字符开始,一直到设定的分隔符","结束.
    substr($3,10,8) ---> 表示是从第3个字段里的第10个字符开始,截取8个字符结束.
    substr($3,6) ---> 表示是从第3个字段里的第6个字符开始,一直到结尾
  • gsub
    gsub 函数则使得在所有正则表达式被匹配的时候都发生替换。
    格式:

    1
    2
    gsub(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.txt
  • match
    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
    9
    BEGIN {
    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)
    }

    AB1BA
  • system
    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
2
# 示例请求日志格式为:request:...; response:...; cost:[123 ms]
grep "过滤请求" request.log | awk '{match($0, "cost:\\[(.+) ms\\]", a); if(a[1] >= 100) print a[1];}' | wc -l

统计某个时间段内某个 userCode 的请求数

1
2
# 内容格式:QTrace[链路标识] 19:01:12.123 INFO userCode=abc123, request=..., response=...
grep "过滤信息" request.log | awk '{match($0, "2019-08-15 (..):(..):(..).+ INFO.+userCode=(.+), req", a); if(a[2]>=20&&a[2]<35) print a[4];}' | sort | uniq -c

按某一列去重并求和

1
2
3
4
5
6
# 内容格式:
2 小航分数
1 小绿分数
5 小航分数
# 按其中第二列去重,并将第一列的分数加起来,结果使用','分割是为了方便在csv中显示
awk '{a[$2]=a[$2]+$1;}END{for(i in a){print i, ",", a[i];}}' 0820-0835期间.txt

过滤行

1
2
3
4
5
# 内容格式:1 3
# $NF表示当前行有多少个字段,因此$NF可以表示最后一个字段,$(NF-1)就表示倒数第二个字段,以此类推...
awk -F ' ' 'NF>2{print NF}' test.txt
# 打印奇数行,并且匹配某个正则表达式,NR表示当前处理的是第几行
awk 'NR%2==1 && /你好/ {print NR ") " $1}' test.txt

计算每个人一月份工资之和

1
2
3
4
5
6
# 内容格式:
# John   2013-01-13 bike 41000
# vivi 2013-01-18 car 42800
# Tom   2013-01-20 car 32500
# John   2013-01-28 bike 63500
awk '{split($2,a,"-");if(a[2]==01){b[$1]+=$4}}END{for(i in b)print i,b[i]}' test.txt

将大文件按规则拆成小文件

1
awk -F ',' '{print $3 > $2".txt"}' input.csv

输入内容格式:

1
2
3
1,Mike,50$
2,Mike,100$
3,Joe,175$

参考

  1. awk 入门教程
  2. Why you should learn just a little Awk: An Awk tutorial by Example
  3. How To Use the AWK language to Manipulate Text in Linux
  4. Learn X in Y minutes
  5. 30 Examples For Awk Command In Text Processing

进阶

  1. 《AWK 程序设计语言》中文翻译
  2. shell-正则表达式
    正则表达式分为三种:BREs、EREs、PREs,如果发现文本工具、语言中的正则语法和自己理解的不同,有可能是因为它们选择实现的正则。
  3. awk-模式匹配
  4. The GNU Awk User’s Guide
    String Functions