数据分析的七种武器-shell

2018-09-08 Mithrilwoodrat 更多博文 » 博客 » GitHub »

原文链接 http://woodrat.xyz/2018/09/10/seven-weapons-of-data-analysis-shell/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


假设我们现在有这样一个任务,需要快速从 Nignx logs 中统计出访问量前10的 ip 及其访问次数。

以 github 上的nginx_logs 为例 (格式参考官方文档)

首先我们可以使用 head 命令取出该日志的前10条,观察其格式。

root:Desktop# head -n 10 nginx_logs.txt
93.180.71.3 - - [17/May/2015:08:05:32 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)"
93.180.71.3 - - [17/May/2015:08:05:23 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)"
80.91.33.133 - - [17/May/2015:08:05:24 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.17)"
217.168.17.5 - - [17/May/2015:08:05:34 +0000] "GET /downloads/product_1 HTTP/1.1" 200 490 "-" "Debian APT-HTTP/1.3 (0.8.10.3)"
217.168.17.5 - - [17/May/2015:08:05:09 +0000] "GET /downloads/product_2 HTTP/1.1" 200 490 "-" "Debian APT-HTTP/1.3 (0.8.10.3)"
93.180.71.3 - - [17/May/2015:08:05:57 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)"
217.168.17.5 - - [17/May/2015:08:05:02 +0000] "GET /downloads/product_2 HTTP/1.1" 404 337 "-" "Debian APT-HTTP/1.3 (0.8.10.3)"
217.168.17.5 - - [17/May/2015:08:05:42 +0000] "GET /downloads/product_1 HTTP/1.1" 404 332 "-" "Debian APT-HTTP/1.3 (0.8.10.3)"
80.91.33.133 - - [17/May/2015:08:05:01 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.17)"
93.180.71.3 - - [17/May/2015:08:05:27 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)"

awk

可以看到,访问的源 IP 是处于每行日志的第一列,并且和其他日志内容是以空格隔开的。处理这种场景刚好可以用上 awk 。 awk 是一个复杂的工具,我们只需要使用分割字符串和执行 print 或者 printf 进行格式化的功能。在这里为 awk -F ' ' '{print $1}',其中 -F 用来指定分割符,单引号中是 awk 的表达式。

关于 awk 更复杂的使用可以参考陈浩的博客AWK 简明教程

root:Desktop# head -n 10 nginx_logs.txt |awk -F ' ' '{print $1}'
93.180.71.3
93.180.71.3
80.91.33.133
217.168.17.5
217.168.17.5
93.180.71.3
217.168.17.5
217.168.17.5
80.91.33.133
93.180.71.3

使用 awk 提取出第一列的 IP 后,我们需要使用 sort 和 uniq 将相同的 ip 合并并且统计个数。在 Shell 中,我们通常使用 | 管道符号将上一个命令的输出作为下一个命令的输入。先使用 sort 将相同的 ip 排列到一起,再使用 uniq -c 合并相同的 ip 并计数。

root:Desktop# head -n 10 nginx_logs.txt |awk -F ' ' '{print $1}' | sort |uniq -c
      4 217.168.17.5
      2 80.91.33.133
      4 93.180.71.3

得到了每个 ip 的计数后,使用 sort -nr 即可按每个 ip 计算降序排列。其中 -n 是按照数字顺序排序,-r 是降序排序。

root:Desktop# head -n 10 nginx_logs.txt |awk -F ' ' '{print $1}' | sort |uniq -c |sort -nr
      4 93.180.71.3
      4 217.168.17.5
      2 80.91.33.133

有了上面的过程,我们只需要把用来验证正确性的 head -n 10 替换为 cat ,并再最后加上 head -n 10 限制只输出前10,即可得到最后的结果。

root:Desktop# cat nginx_logs.txt |awk -F ' ' '{print $1}' | sort |uniq -c |sort -nr | head -n 10
   2350 216.46.173.126
   1720 180.179.174.219
   1439 204.77.168.241
   1365 65.39.197.164
   1202 80.91.33.133
   1120 84.208.15.12
   1084 74.125.60.158
   1064 119.252.76.162
    628 79.136.114.202
    532 54.207.57.55

grep

当我们有多个日志文件时,就需要用到 grep 去搜索哪个文件中有我们需要的内容。

例如有多天的 nginx access log: nginx_logs.txt.1 nginx_logs.txt.2 nginx_logs.txt.3,我们需要找到有对应 /admin.php 后台管理页面访问的日志, 可以使用 grep -Rl "/admin.php" . 在当前目录递归搜索包含 /admin.php 字符串的文件名。

如果需要统计某天的 nginx 日志中 status code 为 404 的数量,这时使用 awk 不太容易分离出 status code 字段,我们可以使用 grep 来提取对应的字段。

单行日志格式如下:

93.180.71.3 - - [17/May/2015:08:05:32 +0000] "GET /downloads/product_1 HTTP/1.1" 304 0 "-" "Debian APT-HTTP/1.3 (0.8.16~exp12ubuntu10.21)"

对应的 grep 命令如下:

root:Desktop# head -n 10 nginx_logs.txt |grep -Po "\".*\"\K \d{3}"
 304
 304
 304
 200
 200
 304
 404
 404
 304
 304

其中 -P 指定了使用 pcre 语法,-o 指定只输出match的部分。

因为日志内容中有多个地方都有连续 3 位的数字,仅仅使用 \d{3} 无法正确匹配到 status code。需要先识别到前面的 request line(关于 HTTP 协议字段可以参考我以前的文章)。

使用ignore-pattern\K 的语法可以指定前面需要匹配的串并忽略,然后只提取后面的 pattern。(某些情况下也可以用 (?>=pattern) 来忽略前面的串,具体参考 StackOverflow )

最后使用 wc count 一下就能得到结果了

root:Desktop# cat nginx_logs.txt |grep -Po "\".*\"\K \d{3}" |grep 404 |wc -l
33876

sed

sed 一般用于将文件中正则搜索到的字符串替换为另外一个字符串。例如,在一个文件夹中将 std::cout 替换为 LOG(INFO)

sed -i "s/std::cout/LOG(INFO)/g" `grep -rl "std::cout" . 

Bash Script

参考阅读:

Shell 环境 http://www.tldp.org/LDP/Bash-Beginners-Guide/html/

Bash Script 编写 https://mywiki.wooledge.org/BashGuide/