假设nginx的日志存储在nowcoder.txt里,内容如下:

192.168.1.20 - - [21/Apr/2020:14:12:49 +0800] "GET /1/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.21 - - [21/Apr/2020:15:00:49 +0800] "GET /2/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.22 - - [21/Apr/2020:21:21:49 +0800] "GET /3/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.23 - - [21/Apr/2020:22:10:49 +0800] "GET /1/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.24 - - [22/Apr/2020:15:00:49 +0800] "GET /2/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.25 - - [22/Apr/2020:15:26:49 +0800] "GET /3/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.20 - - [23/Apr/2020:08:05:49 +0800] "GET /2/index.php HTTP/1.1" 404 490 "-" "Baiduspider"
192.168.1.21 - - [23/Apr/2020:09:20:49 +0800] "GET /1/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.22 - - [23/Apr/2020:10:27:49 +0800] "GET /1/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.22 - - [23/Apr/2020:10:27:49 +0800] "GET /1/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.20 - - [23/Apr/2020:14:12:49 +0800] "GET /1/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.21 - - [23/Apr/2020:15:00:49 +0800] "GET /2/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.22 - - [23/Apr/2020:15:00:49 +0800] "GET /3/index.php HTTP/1.1" 404 490 "-" "Baiduspider"
192.168.1.25 - - [23/Apr/2020:16:15:49 +0800] "GET /1/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.24 - - [23/Apr/2020:20:27:49 +0800] "GET /2/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.25 - - [23/Apr/2020:20:27:49 +0800] "GET /3/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.20 - - [23/Apr/2020:20:27:49 +0800] "GET /1/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.21 - - [23/Apr/2020:20:27:49 +0800] "GET /1/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.22 - - [23/Apr/2020:20:27:49 +0800] "GET /1/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.22 - - [23/Apr/2020:22:10:49 +0800] "GET /1/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"
192.168.1.21 - - [23/Apr/2020:23:59:49 +0800] "GET /1/index.php HTTP/1.1" 404 490 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:45.0) Gecko/20100101 Firefox/45.0"

现在需要编写shell脚本统计每分钟的请求数,并且按照请求数降序排序。你的脚本应该输出:

5 20:27
4 15:00
2 22:10
2 14:12
2 10:27
1 23:59
1 21:21
1 16:15
1 15:26
1 09:20
1 08:05
相关命令学习sprintf()

sprintfprintf 在用法上几乎一样,只是打印的目的地不同而已,前者打印到字符串中,后者则直接在命令行上输出。这也导致 sprintfprintf 有用得多。

sprintf是个变参函数,定义如下:

int sprintf( char *buffer, const char *format [, argument] … );

除了前两个参数类型固定外,后面可以接任意多个参数。而它的精华,显然就在第二个参数:格式化字符串上。

printfsprintf 都使用格式化字符串来指定串的格式,在格式串内部使用一些以“%”开头的格式说明符(format specifications)来占据一个位置,在后边的变参列表中提供相应的变量,最终函数就会用相应位置的变量来替代那个说明符,产生一个调用者想要 的字符串。

格式化数字字符串

把整数123 打印成一个字符串保存在s 中。

sprintf(s, "%d", 123)   # 产生"123"
sprintf(s, "%8d%8d", 123, 4567); // 产生:" 123 4567"当然也可以左对齐:
sprintf(s, "%-8d%8d", 123, 4567); // 产生:"123 4567"也可以按照16进制打印:
  1. 可以指定宽度,不足的左边补空格:
sprintf(s, "%8d%8d", 123, 4567)  # 产生:" 123 4567"
  1. 当然也可以左对齐:
sprintf(s, "%-8d%8d", 123, 4567)  #  产生:"123 4567"
  1. 也可以按照16进制打印:
sprintf(s, "%8x", 4567)  # 小写16进制,宽度占8个位置

控制浮点数打印格式

  1. 浮点数的打印和格式控制是 sprintf 的又一大常用功能,浮点数使用格式符“%f”控制,默认保留小数点后6位数字,比如:
sprintf(s, "%f", 3.1415926)    # 产生 "3.141593"
  1. 使用%m.nf格式控制打印的宽度和小数位数,其中m表示打印的宽度,n表示小数点后的位数。比如:
sprintf(s, "%10.3f", 3.1415626)    #  产生:" 3.142"
sprintf(s, "%-10.3f", 3.1415626)   # 产生:"3.142 "
sprintf(s, "%.3f", 3.1415626);     # 不指定总宽度

使用 sprintf 的常见问题

sprintf是个变参函数,使用时经常出问题,而且只要出问题通常就是能导致程序崩溃的内存访问错误,但好在由sprintf误用导致的问题虽然严重,却很容易找出,无非就是那么几种情况,通常用眼睛再把出错的代码多看几眼就看出来了。

  • 缓冲区溢出:第一个参数的长度太短了,当然也可能是后面的参数的问题,建议变参对应一定要细心,而打印字符串时,尽量指定最大字符数。
  • 忘记了第一个参数:低级得不能再低级问题,用 printf 用得太惯了。
  • 变参对应出问题:通常是忘记了提供对应某个格式符的变参,导致以后的参数统统错位

awk:文本和数据进行处理的编程语言

awk命令来自于三位创始人”Alfred Aho,Peter Weinberger, Brian Kernighan “的姓氏缩写,其功能是用于对文本和数据进行处理的编程语言。使用awk命令可以让用户自定义函数或正则表达式对文本内容进行高效管理,与sedgrep并称为Linux系统中的文本三剑客。

语法格式awk 参数 文件

常用参数

参数

功能

-F

指定输入时用到的字段分隔符

-v

自定义变量

-f

从脚本中读取awk命令

-m

val值设置内在限制

常用的awk内置变量

awk语法由一系列条件和动作组成,在花括号内可以有多个动作,多个动作之间用分号分隔,在多个条件和动作之间可以有若干空格,也可以没有。

变量名称

说明

FILENAME

当前输入文档的文件名

FNR

当前输入文档的当前行号,尤其当多个输入文档时有用

FS

设置字段分隔符,默认为空格或制表符

NF

当前记录(行)的字段(列)个数

NR

输入数据流的当前记录数(行号)

OFS

输出字段分隔符,默认为空格

ORS

输出记录分隔符,默认为换行符

RS

输入记录分隔符,默认为换行符

awk是一种处理文本文件的编程语言,文件的每行数据都被称为记录默认以空格或制表符为分隔符每条记录被分成若干字段(列)awk每次从文件中读取一条记录

sort:对文件内容进行排序

sort命令的功能是对文件内容进行排序。有时文本中的内容顺序不正确,一行行地手动修改实在太麻烦了。此时使用sort命令就再合适不过了,它能够对文本内容进行再次排序。

语法格式:sort [参数] 文件

常用参数:

-b

忽略每行前面开始出的空格字符

-c

检查文件是否已经按照顺序排序

-d

除字母、数字及空格字符外,忽略其他字符

-f

将小写字母视为大写字母

-i

除040至176之间的ASCII字符外,忽略其他字符

-m

将几个排序号的文件进行合并

-M

将前面3个字母依照月份的缩写进行排序

-n

依照数值的大小排序

-o <输出文件>

将排序后的结果存入制定的文件

-r

以相反的顺序来排序

-t <分隔字符>

指定排序时所用的栏位分隔字符

-k

指定需要排序的栏位

-s

通过禁用最后的比较来稳定排序

substr() :截取子串

第一种方法substr()配合awk命令

其语法格式:(开始索引以0或1开始)

substr(源字符串, 开始索引, 长度)

例子:

lucky@DESKTOP-VQ8KID4:~$ cat nowcoder.txt
a12b8
10ccc
2521abc
lucky@DESKTOP-VQ8KID4:~$ awk -F" " '{print substr($1,2,3)}' nowcoder.txt
12b
0cc
521

正如上面提到的,开始索引以1开始和从0开始都一样:

lucky@DESKTOP-VQ8KID4:~$ awk -F" " '{print substr($1,0,3)}' nowcoder.txt
a12
10c
252
lucky@DESKTOP-VQ8KID4:~$ awk -F" " '{print substr($1,1,3)}' nowcoder.txt
a12
10c
252

第二种方法substr()配合expr命令

其语法格式为:(开始索引以1开始)

expr substr 字符串 开始索引 长度

例子:

lucky@DESKTOP-VQ8KID4:~$ expr substr "sdfsdf" 2 2
df

第三种方法:可以根据特定字符偏移和长度,使用另一种形式的变量扩展,来选择特定子字符串。【开始索引为0】

其语法格式为:

  1. 截取变量从n1到最后的字符串,其语法格式为:(只提供一个参数的话,默认截取到最后)
echo ${str:n1}
  1. 截取变量从n1n2之间的字符串。
echo ${str:n1:n2-n1}

uniq:去除文件中的重复内容行

uniq命令来自于英文单词unique的缩写,中文译为独特的、唯一的,其功能是用于去除文件中的重复内容行uniq命令能够去除掉文件中相邻的重复内容行,如果两端相同内容中间夹杂了其他文本行,则需要先使用sort命令进行排序后再去重复,这样保留下来的内容就都是唯一的了。

划重点:去除相邻重复内容行!

语法格式:uniq [参数] 文件

常用参数:

-c

打印每行在文本中重复出现的次数

-d

每个重复纪录只出现一次

-u

只显示没有重复的纪录

题目解决方案

方法一:awk+substr()+sort+uniq

awk命令默认是以空格做为分隔符分割每一列的,在nowcoder.txt文件中,第四列为:

lucky@LAPTOP-G2DIO3FV:~$ cat nowcoder.txt | awk '{print $4}'
[21/Apr/2020:14:12:49
[21/Apr/2020:15:00:49
[21/Apr/2020:21:21:49
[21/Apr/2020:22:10:49
[22/Apr/2020:15:00:49
[22/Apr/2020:15:26:49
[23/Apr/2020:08:05:49
[23/Apr/2020:09:20:49
[23/Apr/2020:10:27:49
[23/Apr/2020:10:27:49
[23/Apr/2020:14:12:49
[23/Apr/2020:15:00:49
[23/Apr/2020:15:00:49
[23/Apr/2020:16:15:49
[23/Apr/2020:20:27:49
[23/Apr/2020:20:27:49
[23/Apr/2020:20:27:49
[23/Apr/2020:20:27:49
[23/Apr/2020:20:27:49
[23/Apr/2020:22:10:49
[23/Apr/2020:23:59:49

题目要求统计每分钟的请求数,那么只需提出小时和分钟就行了。awk命令的substr()函数可以提取出子串:

lucky@LAPTOP-G2DIO3FV:~$ cat nowcoder.txt | awk '{print substr($4,14,5)}'
14:12
15:00
21:21
22:10
15:00
15:26
08:05
09:20
10:27
10:27
14:12
15:00
15:00
16:15
20:27
20:27
20:27
20:27
20:27
22:10
23:59

然后就很简单了,排序–>去重–>排序

lucky@LAPTOP-G2DIO3FV:~$ cat nowcoder.txt | awk '{print substr($4,14,5)}' | sort | uniq -c | sort -rn -k 1
      5 20:27
      4 15:00
      2 22:10
      2 14:12
      2 10:27
      1 23:59
      1 21:21
      1 16:15
      1 15:26
      1 09:20
      1 08:05

然后用awk把这两列提出取来就行了(去掉空格)。所以最终的解决方案的代码如下:

cat nowcoder.txt | awk '{print substr($4,14,5)}' | sort | uniq -c | sort -rn -k 1 | awk '{print $1,$2}'

方法二:awk+sort

在前面,我们提到awk命令默认以空格作为分隔符分割出每一列,现在关注nowcoder.txt文件的内容,我们发现只有在时间那里才有:,那么我们直接使用:作为分隔符,这样第二列和第三列就是我们想要的小时和分钟了。事实上,这个方法更简单。

lucky@LAPTOP-G2DIO3FV:~$ awk -F ":" '{print $2,$3}' nowcoder.txt
14 12
15 00
21 21
22 10
15 00
15 26
08 05
09 20
10 27
10 27
14 12
15 00
15 00
16 15
20 27
20 27
20 27
20 27
20 27
22 10
23 59

接下来,在awk中,用一个数组来进行时间上的计数:

lucky@LAPTOP-G2DIO3FV:~$ awk -F ":" '{a[($2":"$3)]++}END{for (i in a) {print a[i], i}}' nowcoder.txt
4 15:00
2 22:10
1 21:21
2 14:12
1 23:59
5 20:27
1 16:15
2 10:27
1 09:20
1 15:26
1 08:05

最后,排序就行了。最终的代码如下:

awk -F ":" '{a[($2":"$3)]++}END{for (i in a) {print a[i], i}}' nowcoder.txt | sort -h -r

方法三:awk+fprintf+sort

:分割,使用sprintf()提取相关内容

awk -F ":" '{
    a[sprintf("%s:%s", $2, $3)]++
} END {
    for (i in a) {
        printf("%d %s\n", a[i], i)
    }
}' | sort -r

方法四:awk+substr()+数组+sort

awk '{
    a[substr($4,14,5)]++ 
}END{
    for(i in a) {
        print a[i]" "i
    }
}' nowcoder.txt | sort -nrk1