整理httpd日志中的信息

因为没有使用Google Analytics一类的流量统计服务,所以我并不清楚自己博客的具体访问情况。
但是,通过另一途径也是可以获取到博客访问记录的,那就是分析httpd日志中的信息。

准备

目标

先看看日志里有什么:
10.10.10.10 - - [08/May/2019:17:17:33 +0800] "GET / HTTP/1.1" 200 9584
我的日志里每一行都是这样的格式,在Apache官方文档中被叫做Common Log Format (CLF),具体含义为:
远端主机地址 远端登录名 远程用户名 时间 请求的第一行 状态码 传送的不包含HTTP头的字节数
也就是说,从日志中可以获取到的有用信息包括访问者的IP,访问时间,请求的具体网址,请求用的协议,服务器响应码。所以统计以下几点是可行的:

可惜之前服务器的一次滚挂,导致服务器上的日志只有2018年9月份之后的,猜测最终得到的结果会比较寒碜。😭

筛选

直接打开日志文件,约十四万行,但很明显,里面有许多无效请求。例如:

明显是各种自动化扫描工具的请求,试了一下cat whoisnian.com-access_log | grep '\.php' | wc -l,emmmmmm,36398行带.php的,推测扫描工具的访问就占了日志三分之一以上。

接下来根据我的链接格式,我只保留了对博客文章的访问且服务器返回非404/403状态码的日志,即:
cat whoisnian.com-access_log | grep -oP '.* - - .* \+0800] "GET /[0-9]{4}/[0-9]{2}/[0-9]{2}/.*" (?!404|403).*' > log1
好嘛,原来十多万行的日志就剩7624行了,可太真实了。

但是发现其中部分IP地址会在几十秒的间隔内发送两次内容相同的请求,再次进行合并,即:
cat log1 | awk '{ print $4,$5,$1,$2,$3,$6,$8,$9,$10,$7 }' | sed 's/\/$//g' | uniq -f 2 | awk '{ print $3,$4,$5,$1,$2,$6,$10,$7,$8,$9 }' > log2

先就这样进行一次初步筛选吧,还剩下7430行。理想情况下还可以只保留同时请求了css文件和博客文章的日志记录,应该可以进一步排除一部分爬虫,但是我怕日志那样筛就没多少剩的了。。。

统计

单个IP的访问次数排行:

cat log2 | awk '{ print $1 }' | sort | uniq -idc | sort -k1nr | head -n 15
将文件log2的内容每行只保留IP,排序合并,再按合并得到的次数降序,输出前15行,结果为:

次数 IP地址
221 5.45.207.10
169 66.249.70.30
161 66.249.70.28
160 66.249.66.81
153 66.249.70.3
121 66.249.66.80
111 66.249.66.79
108 111.202.100.130
103 23.100.232.xxx
83 78.46.161.xxx
72 159.65.177.xxx
61 66.249.66.62
60 5.45.207.44
59 66.249.66.60
59 66.249.66.83

emmmmmm,nslookup 5.45.207.10可以看到name = 5-45-207-10.spider.yandex.com.,同理,66.249.*.*是google的,111.202.100.130是sougou的,访问次数最多的果然是爬虫。

具体的网页访问量排行:

cat log2 | awk '{ print $1,$7 }' | sort | uniq | awk '{ print $2 }' | sort | uniq -dc | sort -k1nr | sed 's@+@ @g;s@%@\\x@g' | xargs -0 printf "%b" | head -n 15
将文件log2的内容每行只保留IP和访问的链接,排序合并,即一个IP访问一篇文章只计算一次,再将结果只保留文章链接,排序合并,最后按合并得到的次数降序,输出前15行,结果为:

次数 网页地址
301 2018/06/21/给服务器设置动态motd效果
250 2017/07/23/ArchLinux安装配置WineQQ
235 2018/02/09/template-is-undefined
230 2017/12/27/Apache反向代理Google
228 2018/03/10/Honor-8-From-EMUI-5-To-Lineage-OS-14.1
208 2017/08/15/网易云音乐白屏
195 2017/11/11/i3-WM
181 2017/04/07/ArchLinux使用记录
181 2018/06/13/WPS-Office使用记录
156 2018/11/18/通过酷Q小号转发信息
137 2018/08/23/在U盘上安装LFS
133 2017/04/02/ArchLinux-Installation-Guide
124 2017/10/26/Raspberry图形界面下鼠标移动缓慢
116 2017/04/15/Raspberry实现简单的DNS劫持
116 2017/06/01/Vultr搭建SS服务

访问者IP分布:

想要像各种流量统计服务那样生成一个访客地图,在网上搜到了好多都是让使用它的统计服务,它给你自动生成的,自己根据收集到的信息制作访客地图的相关内容却很少。

问了一下熟悉前端的同学,同学推荐了ECharts,看了一下官网给出的样例很不错,于是照着改了一个。那么接下来就需要将IP地址转换成地区,再改成{name: 'xxxxx',value: xxx}的格式。

先尝试了geoiplookup,发现部分IP地址识别不出来,还有部分“亚太地区”这样指向不明显的结果。查看/usr/share/GeoIP/目录下的数据库,发现除了geoiplookup使用的.dat之外还有另一种.mmdb,就又在源里找到了mmdblookup这个工具,试用了一下,感觉效果还可以,于是用了这个。

将文件log2的内容只保留IP地址,并将相同的IP合并:
cat log2 | awk '{ print $1 }' | sort | uniq > log3
然后使用脚本按行处理:./geo.sh log3 > log4,脚本如下,注意要手动指定mmdb文件的位置。

#!/bin/bash
# geo.sh
while read line; do
  country=`mmdblookup -f /usr/share/GeoIP/GeoLite2-Country.mmdb -i "$line" country names en 2>/dev/null`
  if [ $? -ne 0 ]; then
    country=`mmdblookup -f /usr/share/GeoIP/GeoLite2-Country.mmdb -i "$line" registered_country names en 2>/dev/null`
  fi
  echo $country | awk -F '"' '{ print $2 }'
done < $1

得到的列表还需要修改,因为ECharts中使用的地图需要以国家为划分,而mmdblookup直接得到列表是以地区为划分的,两者之间还有部分名称不一致的情况,修改如下:

sed -i 's/^Hong\ Kong$/China/g;
        s/^Macao$/China/g;
        s/^Taiwan$/China/g;
        s/^South\ Korea$/Korea/g;
        s/^Czechia$/Czech\ Rep\./;' log4

将得到的国家列表排序合并,并转换为ECharts需要的形式:
cat log4 | sort | uniq -c | sort -k1nr | awk '{printf"{name: '\''%s", $2; for(i=3;i<=NF;i++){printf" %s", $i}; printf"'\'',value: %s},\n", $1;}'
得到:

{name: 'China',value: 1397},
{name: 'United States',value: 720},
{name: 'France',value: 77},
{name: 'Japan',value: 35},
{name: 'Russia',value: 28},
{name: 'Czech Rep.',value: 24},
{name: 'Canada',value: 16},
{name: 'Germany',value: 16},
{name: 'Singapore',value: 11},
{name: 'United Kingdom',value: 7},
{name: 'Malaysia',value: 6},
{name: 'Vietnam',value: 6},
{name: 'Ukraine',value: 5},
{name: 'Netherlands',value: 4},
{name: 'Australia',value: 3},
{name: 'India',value: 3},
{name: 'Cambodia',value: 2},
{name: 'Korea',value: 2},
{name: 'Poland',value: 2},
{name: 'Thailand',value: 2},
{name: 'Algeria',value: 1},
{name: 'Hungary',value: 1},
{name: 'Italy',value: 1},
{name: 'Malawi',value: 1},
{name: 'Norway',value: 1},
{name: 'Philippines',value: 1},
{name: 'Slovakia',value: 1},
{name: 'Switzerland',value: 1},

将数据填入改写的ECharts样例中,得到:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>ECharts</title>
    <script src="https://cdn.bootcss.com/echarts/4.2.1/echarts.min.js"></script>
    <script src="https://echarts-maps.github.io/echarts-countries-js/echarts-countries-js/world.js"></script>
</head>
<body>
    <div id="main" style="width:900px;height:500px;"></div>
    <script type="text/javascript">
        var myChart = echarts.init(document.getElementById('main'));

        var option = {
            title: {
                text: '访客地图'
            },
            tooltip: {
                trigger: 'item'
            },
            visualMap: {
                min: 0,
                max: 300,
                left: 'left',
                top: 'bottom',
                text:['','']
            },
            toolbox: {
                show: true,
                orient : 'horizontal',
                left: 'right',
                top: 'bottom',
                showTitle: false,
                feature : {
                    restore : {show: true},
                    saveAsImage : {show: true}
                }
            },
            series : [
                {
                    name: 'visitors',
                    type: 'map',
                    mapType: 'world',
                    roam: true,
                    scaleLimit: {
                        min: 1,
                        max: 10
                    },   
                    data:[
                        {name: 'China',value: 1397},
                        {name: 'United States',value: 720},
                        {name: 'France',value: 77},
                        {name: 'Japan',value: 35},
                        {name: 'Russia',value: 28},
                        {name: 'Czech Rep.',value: 24},
                        {name: 'Canada',value: 16},
                        {name: 'Germany',value: 16},
                        {name: 'Singapore',value: 11},
                        {name: 'United Kingdom',value: 7},
                        {name: 'Malaysia',value: 6},
                        {name: 'Vietnam',value: 6},
                        {name: 'Ukraine',value: 5},
                        {name: 'Netherlands',value: 4},
                        {name: 'Australia',value: 3},
                        {name: 'India',value: 3},
                        {name: 'Cambodia',value: 2},
                        {name: 'Korea',value: 2},
                        {name: 'Poland',value: 2},
                        {name: 'Thailand',value: 2},
                        {name: 'Algeria',value: 1},
                        {name: 'Hungary',value: 1},
                        {name: 'Italy',value: 1},
                        {name: 'Malawi',value: 1},
                        {name: 'Norway',value: 1},
                        {name: 'Philippines',value: 1},
                        {name: 'Slovakia',value: 1},
                        {name: 'Switzerland',value: 1}
                    ]
                }
            ]
        };

        myChart.setOption(option);
    </script>
</body>
</html>

用浏览器打开就可以看到访客地图,还可以点击保存按钮直接保存为png。
vistor_map