博客成长日志 | 准实时访问统计

前言

  通过百度统计、CNZZ等服务,我们可以记录站点的访问地图、访问量和来源,如果想要展示数据,可以选择直接导向服务商提供的公开页面,但是样式丑陋,所以本文通过百度统计API,使用Echarts制作站点访问准实时统计页面,效果可以参考统计

  之所以说是准实时统计,是因为为了解决跨域问题(CROS error),本文采用的方法是定时通过百度统计API将数据下载保存为json文件,放置在网站目录下(后续可能会发展为vercel api,挖坑)。不过作为个人博客,方式访问量不会很大,没有必要实时更新,目前本站设置是每隔6小时更新一次。

数据获取

百度统计

  在设置样式之前首先需要获取统计数据,使用百度账号登陆百度统计,根据参考资料4进行操作,获取token与site_id,具体教程可以查看参考资料1

下载文件

  通过6个链接,我们可以获取:一年内每日访问统计、访问地图数据、月度访问统计、来源分类统计、搜索引擎访问统计和外部链接访问统计;通过python或者nodejs都可以很方便的下载文件保存,以下为python的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import requests
import time, datetime

# 开始统计的日期
start_date = '20201218'
date = datetime.datetime.now()
# 结束统计的日期
end_date = str(date.year) + (str(date.month) if date.month > 9 else ('0' + str(date.month))) + (str(date.day) if date.day > 9 else ('0' + str(date.day)))
# token和siteid
access_token = '121.6e...'
site_id = '16******'
# 百度统计API
dataUrl = 'https://openapi.baidu.com/rest/2.0/tongji/report/getData?access_token=' + access_token + '&site_id=' + site_id
# 统计访问次数 PV 填写 'pv_count',统计访客数 UV 填写 'visitor_count',二选一
metrics = 'pv_count'


def downFile(url, fileName, prefix='/home/API/data/'):
print('downloading :', url)
down_res = requests.get(url)
with open(prefix+fileName, 'wb') as f:
f.write(down_res.content)
print('writing :', prefix+fileName)

# 访客地图
downFile(dataUrl + '&start_date=' + start_date + '&end_date=' + end_date + '&metrics=' + metrics + '&method=visit/district/a',
'map.json')

# 访问趋势
downFile(dataUrl + '&start_date=' + start_date + '&end_date=' + end_date + '&metrics=' + metrics + '&method=trend/time/a&gran=month',
'trends.json')

# 访问来源
downFile(dataUrl + '&start_date=' + start_date + '&end_date=' + end_date + '&metrics=' + metrics + '&method=source/all/a',
'sources.json')

## 搜索引擎
downFile(dataUrl + '&start_date=' + start_date + '&end_date=' + end_date + '&metrics=' + metrics + '&method=source/engine/a',
'engine.json')

## 外部链接
downFile(dataUrl + '&start_date=' + start_date + '&end_date=' + end_date + '&metrics=' + metrics + '&method=source/link/a',
'link.json')

# 访问日历
'''
访问日历需要获取一年内的数据,按照一年365天计算,大概为52周多一点,所以前面有完整的52排,获取方式只要通过开始日期年份-1即可
然后就是第53排的处理,python中的date.weekday()获取的星期几是0对应周一,所以通过(date.weekday()+1)%7即可转换到0对应周日
于是在52周的基础上,减去星期数,就可以得到新的start_date
'''
date = datetime.datetime(date.year-1, date.month, date.day)
date = datetime.datetime.fromtimestamp(date.timestamp()-3600*24*((date.weekday()+1)%7))
start_date = str(date.year) + (str(date.month) if date.month > 9 else ('0' + str(date.month))) + (str(date.day) if date.day > 9 else ('0' + str(date.day)))
downFile(dataUrl + '&method=overview/getTimeTrendRpt' + '&metrics=' + metrics + '&start_date=' + start_date + '&end_date=' + end_date,
'calendar.json')

自动更新

  在Linux中可以通过crontab设置定时任务,以下为每整6小时的0点执行一次任务:

1
2
3
4
5
6
7
8
0 0 * * * /usr/bin/python /home/API/data/get.py >> /home/API/data/get.log
0 6 * * * /usr/bin/python /home/API/data/get.py >> /home/API/data/get.log
0 12 * * * /usr/bin/python /home/API/data/get.py >> /home/API/data/get.log
0 18 * * * /usr/bin/python /home/API/data/get.py >> /home/API/data/get.log
# 或者
0 0,6,12,18 * * * /usr/bin/python /home/API/data/get.py >> /home/API/data/get.log
# 或者
0 */6 * * * /usr/bin/python /home/API/data/get.py >> /home/API/data/get.log

数据展示

统计图容器

  在html代码中插入:

1
2
3
4
5
6
7
8
9
10
11
<script src="https://cdn.jsdelivr.net/npm/echarts@4.7.0/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@4.7.0/map/js/china.js"></script>

<div style="max-width: 90%;margin: 0 auto;">
<div id="calendar_container" class="data-container"></div>
<div id="map_container" class="data-container" style="height: 500px;"></div>
<div id="trends_container" class="data-container" style="height: 350px;"></div>
<div id="sources_container" class="data-container" style="height: 450px;"></div>
</div>
<script src="/js/calendar.js"></script>
<script src="/js/census.js"></script>

访问日历

  访问日历类似于Github贡献日历,有两种方法实现。

  1. 源自于hexo-githubcalendar 插件,由Eurkon修改,原文见参考资料3。点击链接下载calendar.js文件,需要修改的地方有:
1
2
3
4
5
6
7
...
// 下面的链接是本站点的访问数据,需要修改为自己的,为了防止浏览器缓存的干扰,在url后添加了时间戳,可以进一步优化为时间的小时数对6取模
fetch('https://api.foolishfox.cn/data/calendar.json?date'+new Date()).then(data => data.json()).then(res => {
...
// 统计访问次数 PV 填写 'pv_count',统计访客数 UV 填写 'visitor_count',二选一
visit_calendar('pv_count', visit_color);
...

  另外,需要更改容器id可以直接搜索替换即可。

  1. 通过census.js进行设置,包括后续的访问地图、访问趋势(访问次数)和访问来源都是基于此文件通过Echarts实现的。
1
2
3
4
5
6
7
8
9
10
11
// 统计访问次数 PV 填写 'pv_count',统计访客数 UV 填写 'visitor_count',二选一
var metrics = 'pv_count'
var metricsName = (metrics === 'pv_count' ? '访问次数' : (metrics === 'visitor_count' ? '访客数' : ''))
...
function calChart () {
let script = document.createElement("script")
// 下面的链接是本站点的访问数据,需要修改为自己的,为了防止浏览器缓存的干扰,在url后添加了时间戳,可以进一步优化为时间的小时数对6取模
fetch('https://api.foolishfox.cn/data/calendar.json?date'+new Date()).then(data => data.json()).then(data => {
...
// 颜色盒,用于设置不同数据的展示颜色,可更改
let colorBox = ['#EBEDF0', '#FFE9BB', '#FFD1A7', '#FFBB95', '#FFA383', '#FF8D70', '#FF745C', '#FF5C4A', '#FF4638', '#FF2E26', '#FF1812'];

访问地图

  最简单的就是访问地图,直接修改json文件的请求url即可:

1
2
3
4
5
// 访问地图
function mapChart () {
let script = document.createElement("script")
// 下面的链接是本站点的访问数据,需要修改为自己的,为了防止浏览器缓存的干扰,在url后添加了时间戳,可以进一步优化为时间的小时数对6取模
fetch('https://api.foolishfox.cn/data/map.json?date'+new Date()).then(data => data.json()).then(data => {

访问趋势

  访问趋势中按照年份,将12个月的数据展开,需要从json中获取到年份和月份信息,由于时间日期格式是固定的,所以可以直接截取。本部分代码以下列展示的为准:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 获取年份和月份
function get_year(s) {
return parseInt(s.substr(0, 4))
}
function get_month(s) {
return parseInt(s.substr(5, 2))
}
// 访问趋势
function trendsChart () {
let script = document.createElement("script")
// 下面的链接是本站点的访问数据,需要修改为自己的,为了防止浏览器缓存的干扰,在url后添加了时间戳,可以进一步优化为时间的小时数对6取模
fetch('https://api.foolishfox.cn/data/trends.json?date'+new Date()).then(data => data.json()).then(data => {
let date = new Date();
let monthValueArr = {};
let monthName = data.result.items[0];
let monthValue = data.result.items[1];
// 2020是起始年份,改为你自己的
for (let i =2020; i <= date.getFullYear(); i++) monthValueArr[String(i)] = [ , , , , , , , , , , , ];
monthValueArr
for (let i = 0; i < monthName.length; i++) {
let year = get_year(monthName[i][0]);
let month = get_month(monthName[i][0]);
monthValueArr[String(year)][String(month-1)] = monthValue[i][0];
}
// 以后每过一年,需要手动添加以下legend和series
script.innerHTML = `
var trendsChart = echarts.init(document.getElementById('trends_container'), 'light');
var trendsOption = {
title: { text: '访问趋势', x: 'center' },
tooltip: { trigger: 'axis' },
legend: { data: ['2020', '2021'], x: 'right' },
xAxis: {
name: '日期', type: 'category', boundaryGap: false,
data: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
},
yAxis: { name: '${metricsName}', type: 'value' },
series: [
{
name: '2020', type: 'line', smooth: true,
data: [${monthValueArr["2020"]}],
markLine: { data: [{type: 'average', name: '平均值'}] }
},
{
name: '2021', type: 'line', smooth: true,
data: [${monthValueArr["2021"]}],
markLine: { data: [{type: 'average', name: '平均值'}] }
}
]
};
trendsChart.setOption(trendsOption);
window.addEventListener("resize", () => {
trendsChart.resize();
});`
document.getElementById('trends_container').after(script);
}).catch(function (error) {
console.log(error);
});
}

访问来源

  留在最后的是最复杂的访问来源,实际上百度的全部来源(source/all/a)API可以将来源分为:直达、外部链接和搜索引擎三个部分,直接使用并不困难。但是百度统计会将必应(cn.bing.comwww.bing.com)的来源归类到外部链接而不是搜索引擎,而且我自己还想统计来自于Github、十年之约等网站的流量,所以需要获取多个数据文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
...
// 下面的链接是本站点的访问数据,需要修改为自己的,为了防止浏览器缓存的干扰,在url后添加了时间戳,可以进一步优化为时间的小时数对6取模
fetch('https://api.foolishfox.cn/data/sources.json?date'+new Date()).then(data => data.json()).then(data => {
let sourcesName = data.result.items[0];
let sourcesValue = data.result.items[1];
let sourcesArr = [];
for (let i = 0; i < sourcesName.length; i++)
sourcesArr.push({ name: sourcesName[i][0].name, value: sourcesValue[i][0] });
// 记录总体数据,方便后续调用(使用var定义在fetch外面)
link = sourcesArr[1]['value'] ;
search = sourcesArr[2]['value'];
direct = sourcesArr[0]['value'];
...
// 下面的链接是本站点的访问数据,需要修改为自己的,为了防止浏览器缓存的干扰,在url后添加了时间戳,可以进一步优化为时间的小时数对6取模
fetch('https://api.foolishfox.cn/data/link.json?date'+new Date()).then(data => data.json()).then(data => {
let linksName = data.result.items[0];
let linksValue = data.result.items[1];
let linksArr = {};
for (let i = 0; i < linksName.length; i++)
linksArr[linksName[i][0].name] = linksValue[i][0];
// 除必应外,其他的都是自己想要统计的网站,都是http链接
let sum = data.result.sum[0][0];
let bing = linksArr['http://cn.bing.com']+linksArr['http://www.bing.com'];
let github = linksArr['http://github.com'];
let travel = linksArr['http://travellings.now.sh']+linksArr['http://travellings.vercel.app']+linksArr['http://www.foreverblog.cn'];
innerHTML += `
{value: ${bing}, name: '必应'},
{value: ${direct}, name: '直达'},
{value: ${github}, name: 'Github'},
{value: ${travel}, name: '开往/十年之约'},`+
// 将来自于必应的访问减去
`{value: ${sum-bing-github-travel+65}, name: '其他'}
]
},
{
name: '访问来源', type: 'pie', selectedMode: 'single', radius: [0, '30%'],
label: { position: 'inner', fontSize: 14},
labelLine: { show: false },
data: [`+
// 将来自于必应的访问从外链中减去,加入搜索中
`{value: ${search+bing}, name: '搜索', itemStyle: { color : 'green' }},
{value: ${direct}, name: '直达', itemStyle: { color : '#FFDB5C' }},
{value: ${link-bing}, name: '外链', itemStyle: { color : '#32C5E9' }}
]
},
]
};
...

Q&A

  1. 不想使用python下载文件保存,想要实时统计
    根据参考资料2中4.3节——自建 Vercel API(可选)进行设置,替换上述文件中的url即可

  2. 依然出现跨域问题?
    请务必保证通过2.2节获取的文件保存在博客的网站目录下,并通过url可以访问;如果想要向我一样通过其他域名(例如api.foolishfox.cn/data/*.json)访问,需要对服务器进行设置,以Nginx为例:

configuration
1
2
3
4
5
6
7
8
9
#data CROS
location ~ ^/data/ {
if ($http_origin ~* (http://localhost:4000|https://foolishfox.cn)) {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}
}
  1. 地图显示错误
    务必注意,Echarts目前已经不提供地图js/json文件的下载,所以要通过npm获取到旧版本中的China.js文件,因此Echarts的版本最好也保持一致

  2. Echarts绘制的访问日历无法自动适应窗口大小
    正在积极解决中…

更新

  1. 2021.6.1:访问趋势中第23行代码应改为:
1
monthValueArr[String(year)][String(month-1)] = monthValue[i][0] === '--' ? 0 : monthValue[i][0];

参考资料

  1. Hexo 博客访问统计图
  2. Hexo 博客实时访问统计图
  3. Hexo 博客访问日历图
  4. 百度统计用户手册
  5. Echarts文档