前言

本文教程主要针对 Hexo 博客,对博客站点的的访问地图、每月访问量、访问来源的维度绘制统计图,使用的是 ECharts 开源可视化库。具体效果可以点击本站的 实验室--博客统计 页面查看。

  • 本文数据来源均为百度统计,请确保博客站点已加入百度统计,以 butterfly 主题为例,可参照 Butterfly 安装文档(四) 主题配置-2 的分析统计段落实现。
  • 本地主机访问(localhost)也会记录到百度统计,推荐在 【百度统计】--【管理】--【统计规则设置】--【过滤规则设置】--【受访域名统计规则】--【勾选排除 localhost(本地主机)】 排除本地主机访问(貌似在勾选后生效,但是以前的访问记录仍会统计)。
  • 绘制统计图的数据在生成后不会变化。
  • 2021-05-14 新增访客数的统计,统计指标 metrics 可以选择统计访问次数(PV)还是访客数(UV)。
  • 2021-05-17 新增主题“明暗模式”下统计图的颜色切换。
  • 2021-05-17 针对访问数据的实时性问题,发表了新教程 Hexo 博客实时访问统计图

如果想绘制博客文章发布统计图的可以参考文章 Hexo 博客文章统计图

新建 census 页面

1
hexo new page census

[Blogroot]\source\ 目录下新建 census 文件夹,并在新建的 census 文件夹下新建 index.md 文件,添加以下内容:

1
2
3
4
---
title: 博客统计
date: 2020-03-01 08:00:00
---

引入 ECharts.js

echarts.js 必须在渲染 echarts 实例的 JavaScript 前引入。

以 butterfly 主题为例,可以在 [Blogroot]\_config.butterfly.ymlinject 配置项中引入 echart.js 文件。

1
2
3
4
inject:
head:
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/map/js/china.js"></script> # 绘制地图需要另外添加 china.js

可以在 index.md 添加以下内容:

1
2
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/map/js/china.js"></script> <!-- 绘制地图需要另外添加 china.js -->

博客访问统计

获取网站统计数据

  1. 已经添加百度统计分析的小伙伴可以登录百度统计的官网网站,此处博主使用的是百度账号登录。

  2. 登录进入首页后可以点击基础报告查看网站访问统计数据(若无绑定网站,需要先在 管理页面--账户管理--网站列表 添加博客网址)。

  3. 此时我们想要获取这些数据可以调用百度统计 API,点击管理,进入管理页面后在左边侧边栏找到数据导出服务

  4. 登录百度开发者中心控制台,创建工程,应用名称任意。

  5. 点击刚刚创建的工程,记录下 API Key,Secret Key。

  6. 填写下面链接参数后打开链接获取授权码,具体步骤可以参考 Tongji API 用户手册

    http://openapi.baidu.com/oauth/2.0/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope=basic&display=popup

    • API Key:{CLIENT_ID}
    • 回调 URI:{REDIRECT_URI},可以填写 oob

  7. 复制第 6 步获取的授权码,填写下面链接参数后打开链接获取 ACCESS_TOKEN :。

    http://openapi.baidu.com/oauth/2.0/token?grant_type=authorization_code&code={CODE}&client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}&redirect_uri={REDIRECT_URI}

    • Auth Code:{CODE},第 6 步获取的授权码。
    • API Key:{CLIENT_ID}
    • Secret Key:{CLIENT_SECRET}
    • 回调 URI:{REDIRECT_URI},可以填写 oob

    注意:从上述步骤得到的数据中包含 ACCESS_TOKENREFRESH_TOKEN 两个值,其中 ACCESS_TOKEN 的有效期为一个月,REFRESH_TOKEN 的有效期为十年。REFRESH_TOKEN 的作用就是刷新获取新的 ACCESS_TOKENREFRESH_TOKEN , 如此反复操作来实现 ACCESS_TOKEN 有效期永久的机制。

    一旦 ACCESS_TOKEN 过期,可根据以下请求更换新的 ACCESS_TOKENREFRESH_TOKEN

    http://openapi.baidu.com/oauth/2.0/token?grant_type=refresh_token&refresh_token={REFRESH_TOKEN}&client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}

    • API Key:{CLIENT_ID}
    • Secret Key:{CLIENT_SECRET}
    • Refresh Token:{REFRESH_TOKEN}
  8. 调用百度统计 API

    第 7 步获取的 ACCESS_TOKEN 是所调用 API 的用户级参数,结合各 API 的应用级参数即可正常调用 API 获取数据,填写下面链接参数后打开链接获取网站 ID:

    https://openapi.baidu.com/rest/2.0/tongji/config/getSiteList?access_token={ACCESS_TOKEN}

    • Access Token:{ACCESS_TOKEN}

    也可以在 Tongji API 调试工具 输入 ACCESS_TOKEN 获取网址 ID:

  9. Tongji API 调试工具 选择需要获取的报告数据,填写 ACCESS_TOKEN 、必填参数、选填参数获取百度统计数据。

网站统计代码

以 butterfly 主题为例,可以在 [Blogroot]\themes\butterfly\scripts\helpers\ 目录下新建 census.js 文件,然后添加以下内容:

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
const cheerio = require('cheerio')
const moment = require('moment')
const fetch = require('node-fetch')

hexo.extend.filter.register('after_render:html', async function (locals) {
const $ = cheerio.load(locals)
const map = $('#map-chart')
const trend = $('#trends-chart')
const source = $('#sources-chart')
let htmlEncode = false

if (map.length > 0 || trend.length > 0 || source.length > 0) {
if (map.length > 0 && $('#mapChart').length === 0) {
map.after(await mapChart())
}
if (trend.length > 0 && $('#trendsChart').length === 0) {
trend.after(await trendsChart())
}
if (source.length > 0 && $('#sourcesChart').length === 0) {
source.after(await sourcesChart())
}

if (htmlEncode) {
return $.root().html().replace(/&amp;#/g, '&#')
} else {
return $.root().html()
}
} else {
return locals
}
}, 15)

const startDate = '20210101' // 开始日期
const endDate = moment().format('YYYYMMDD') // 结束日期
const accessToken = '121.c644d8c4*****' // accessToken
const siteId = '16****' // 网址 id
const dataUrl = 'https://openapi.baidu.com/rest/2.0/tongji/report/getData?access_token=' + accessToken + '&site_id=' + siteId;
const metrics = 'pv_count' // 统计访问次数 PV 填写 'pv_count',统计访客数 UV 填写 'visitor_count',二选一
const metricsName = (metrics === 'pv_count' ? '访问次数' : (metrics === 'visitor_count' ? '访客数' : ''))

// 访问地图
function mapChart () {
return new Promise(resolve => {
const paramUrl = '&start_date=' + startDate + '&end_date=' + endDate + '&metrics=' + metrics + '&method=visit/district/a';
fetch(dataUrl + paramUrl)
.then(data => data.json())
.then(data => {
monthArr = [];
let mapName = data.result.items[0]
let mapValue = data.result.items[1]
let mapArr = []
let max = mapValue[0][0]
for (let i = 0; i < mapName.length; i++) {
mapArr.push({ name: mapName[i][0].name, value: mapValue[i][0] })
}
const mapArrJson = JSON.stringify(mapArr)
resolve(`
<script id="mapChart">
var color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
var mapChart = echarts.init(document.getElementById('map-chart'), 'light');
var mapOption = {
title: {
text: '博客访问来源地图',
x: 'center',
textStyle: {
color: color
}
},
tooltip: {
trigger: 'item'
},
visualMap: {
min: 0,
max: ${max},
left: 'left',
top: 'bottom',
text: ['高','低'],
color: ['#1E90FF', '#AAFAFA'],
textStyle: {
color: color
},
calculable: true
},
series: [{
name: '${metricsName}',
type: 'map',
mapType: 'china',
showLegendSymbol: false,
label: {
emphasis: {
show: false
}
},
itemStyle: {
normal: {
areaColor: 'rgba(255, 255, 255, 0.1)',
borderColor: '#20232a'
},
emphasis: {
areaColor: 'gold'
}
},
data: ${mapArrJson}
}]
};
mapChart.setOption(mapOption);
window.addEventListener("resize", () => {
mapChart.resize();
});
</script>`);
}).catch(function (error) {
console.log(error);
});
})
}

// 访问趋势
function trendsChart () {
return new Promise(resolve => {
const paramUrl = '&start_date=' + startDate + '&end_date=' + endDate + '&metrics=' + metrics + '&method=trend/time/a&gran=month'
fetch(dataUrl + paramUrl)
.then(data => data.json())
.then(data => {
const monthArr = []
const monthValueArr = []
const monthName = data.result.items[0]
const monthValue = data.result.items[1]
for (let i = Math.min(monthName.length, 12) - 1; i >= 0; i--) {
monthArr.push(monthName[i][0].substring(0, 7).replace('/', '-'))
if (monthValue[i][0] !== '--') {
monthValueArr.push(monthValue[i][0])
} else {
monthValueArr.push(null)
}
}
const monthArrJson = JSON.stringify(monthArr)
const monthValueArrJson = JSON.stringify(monthValueArr)
resolve(`
<script id="trendsChart">
var color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
var trendsChart = echarts.init(document.getElementById('trends-chart'), 'light');
var trendsOption = {
textStyle: {
color: color
},
title: {
text: '博客访问统计图',
x: 'center',
textStyle: {
color: color
}
},
tooltip: {
trigger: 'axis'
},
xAxis: {
name: '日期',
type: 'category',
axisTick: {
show: false
},
axisLine: {
show: true,
lineStyle: {
color: color
}
},
data: ${monthArrJson}
},
yAxis: {
name: '${metricsName}',
type: 'value',
splitLine: {
show: false
},
axisTick: {
show: false
},
axisLine: {
show: true,
lineStyle: {
color: color
}
}
},
series: [{
name: '${metricsName}',
type: 'line',
smooth: true,
lineStyle: {
width: 0
},
showSymbol: false,
itemStyle: {
opacity: 1,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(128, 255, 165)'
},
{
offset: 1,
color: 'rgba(1, 191, 236)'
}])
},
areaStyle: {
opacity: 1,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(128, 255, 165)'
}, {
offset: 1,
color: 'rgba(1, 191, 236)'
}])
},
data: ${monthValueArrJson},
markLine: {
data: [{
name: '平均值',
type: 'average'
}]
}
}]
};
trendsChart.setOption(trendsOption);
window.addEventListener("resize", () => {
trendsChart.resize();
});
</script>`)
}).catch(function (error) {
console.log(error);
});
})
}

// 访问来源
function sourcesChart () {
return new Promise(resolve => {
const paramUrl = '&start_date=' + startDate + '&end_date=' + endDate + '&metrics=' + metrics + '&method=source/all/a';
fetch(dataUrl + paramUrl)
.then(data => data.json())
.then(data => {
monthArr = [];
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] })
}
const sourcesArrJson = JSON.stringify(sourcesArr)
resolve(`
<script id="sourcesChart">
var color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
var sourcesChart = echarts.init(document.getElementById('sources-chart'), 'light');
var sourcesOption = {
textStyle: {
color: color
},
title: {
text: '博客访问来源统计图',
x: 'center',
textStyle: {
color: color
}
},
legend: {
top: 'bottom',
textStyle: {
color: color
}
},
tooltip: {
trigger: 'item',
formatter: "{a} <br/>{b} : {c} ({d}%)"
},
series: [{
name: '${metricsName}',
type: 'pie',
radius: [30, 80],
center: ['50%', '50%'],
roseType: 'area',
label: {
formatter: "{b} : {c} ({d}%)"
},
data: ${sourcesArrJson},
itemStyle: {
emphasis: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(255, 255, 255, 0.5)'
}
}
}]
};
sourcesChart.setOption(sourcesOption);
window.addEventListener("resize", () => {
sourcesChart.resize();
});
</script>`);
}).catch(function (error) {
console.log(error);
});
})
}

上面是博主自己三个统计图的代码,小伙伴们可以用以参考,根据自己的需求绘制百度分析统计图。

更多统计图的自定义属性可以查看 ECharts 配置项文档,根据自行喜好对 ECharts 统计图进行修改。

使用统计图

在上文新建的 [Blogroot]\source\census\index.md 文件中添加以下内容:

1
2
3
4
5
6
<!-- 访问地图 -->
<div id="map-chart" style="border-radius: 8px; height: 600px; padding: 10px;"></div>
<!-- 访问趋势 -->
<div id="trends-chart" style="border-radius: 8px; height: 300px; padding: 10px;"></div>
<!-- 访问来源 -->
<div id="sources-chart" style="border-radius: 8px; height: 300px; padding: 10px;"></div>

当然也可以在其他页面引入博客访问统计图。

适配明暗模式

统计图内部文字的颜色通过 census.js 文件中的 var color = '#000' 设置,如果需要适配博客明暗模式更改统计图文字颜色,以 butterfly 主题为例,可以修改三处的 color:

1
2
- var color = '#000'
+ var color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'

同时引入自定义 js 文件并添加以下代码:

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
function switchVisitChart () {
// 这里为了统一颜色选取的是“明暗模式”下的两种字体颜色,也可以自己定义
let color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
if (document.getElementById('map-chart')) {
let mapOptionNew = mapOption
mapOptionNew.title.textStyle.color = color
mapOptionNew.visualMap.textStyle.color = color
mapChart.setOption(mapOptionNew)
}
if (document.getElementById('trends-chart')) {
let trendsOptionNew = trendsOption
trendsOptionNew.title.textStyle.color = color
trendsOptionNew.xAxis.axisLine.lineStyle.color = color
trendsOptionNew.yAxis.axisLine.lineStyle.color = color
trendsOptionNew.textStyle.color = color
trendsChart.setOption(trendsOptionNew)
}
if (document.getElementById('sources-chart')) {
let sourcesOptionNew = sourcesOption
sourcesOptionNew.title.textStyle.color = color
sourcesOptionNew.legend.textStyle.color = color
sourcesOptionNew.textStyle.color = color
sourcesChart.setOption(sourcesOptionNew)
}
}

document.getElementById("mode-button").addEventListener("click", function () { setTimeout(switchVisitChart, 100) })

Hexo 三连

执行 Hexo 三连

1
hexo clean && hexo g && hexo s

可能遇到的问题

  • 控制台报错 Uncaught ReferenceError: require is not defined

    解决方案:

    不能直接在页面引用 charts.js,是放在主题文件夹 [Blogroot]\themes\butterfly\scripts\helpers\ 里面,hexo g 的时候会在 div 后面追加 echarts 图的 js。页面不需要引入 charts.js

  • 控制台报错 Uncaught ReferenceError: echarts is not defined

    解决方案:

    需要在统计图的前引入 echarts.js 文件,最好是在页面的头部引入。