在一年多之前,我写了一篇博客(博客成长日志 | 准实时访问统计)介绍如何使用百度统计的API实现准实时的访问统计与展示。然而今年百度统计宣布个人版只允许保存一年的数据,而且很多功能会被关闭(例如OS统计等),再加上其API使用也不方便,因此我开始谋求其他的站点统计系统。

与百度统计同类型的竞品还有谷歌统计、51La、CNZZ等,但是这些网站与百度统计也或多或少存在类似类似的问题,同时作为个人小站,也不需要收集过于精细的用户信息(如年龄、详细地区等),所以我开始寻找自建的统计工具。

目前常用的一些开源统计工具可以查看:5 款免费开源的网站流量分析统计工具,在这其中UmamiPlausible是我认为不错的选择,再结合枋柚梓的自建个人网站数据统计分析系统,最终决定采用Umami

Umami也存在问题:

  1. 只记录了 country,无法精确到省份
  2. 地图存在问题,如果使用要避免直接展示地图

介绍

Umami is an open source, privacy-focused alternative to Google Analytics. Umami provides you with a powerful web analytics solution that does not violate the privacy of your users. Additionally, when you self-host Umami you are in complete control of your data.

Umami是一个开源的,关注隐私的谷歌统计替代品,由Nodejs编写,我们可以使用多种方式部署,包括Server自部署、Docker部署以及SeverLess服务部署。

在本文中,我们会在服务器上直接部署服务,使用MySQL作为数据库。

安装

环境要求:

  • Nodejs
  • yarn
  1. 源代码
1
2
3
git clone https://github.com/umami-software/umami.git
cd umami
yarn install
  1. 环境变量
    在目录下创建.env文件,写入内容:
1
2
PORT=8000
DATABASE_URL=mysql://username:mypassword@localhost:3306/mydb
  1. 构建与运行
1
2
yarn build
yarn start-dev

随后即可在localhost:8000上打开网页,初始默认用户名为admin,密码为umami

统计展示

API

Umami将通过https://<your-umami>/api调用API,并返回json格式的数据。在构建过程中,我们需要使用到以下三个API:

  1. POST /api/auth/login
    通过这个API我们可以获取TOKEN,作为后续API的认证,使用Python获取:
1
2
3
4
5
6
7
8
9
10
import json
import request
url = "https://<your-umami>/api/auth/login"
data = {
"username": "your-username",
"password": "your-password"
}
res = requests.post(url, data=data)
res = json.loads(res.content)
print(res)
  1. GET /api/website/{id}/pageviews
    通过这个API,我们可以获取选定时段的访问量和访问人数等信息,时间颗粒度可以选择为月、天、小时等。

  2. GET /api/website/{id}/metrics
    通过这个API,我们可以进一步获取国家/地区、来源等信息。

但是由于以下个原因,我们尽量避免直接使用API:

  1. 保密token:使用API需要将token作为请求header,容易暴露
  2. 地图需要转换:Umami中国家/地区使用二字代码存储,而Echarts需要用到其他格式,需要一步转换
  3. 地图问题:涉及香港、澳门、台湾等地

因此最终我们可以自建一个API,使用python的FastAPI库,代码如下:

main.py
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
import json
import requests
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()
origins = [
"http://localhost",
"example.com"
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["GET"],
allow_headers=["*"],
)

root_path = "https://<your-umami>/api/website/1"
header = {
"accept": "application/json",
"authorization": "Bearer YOUR TOKEN",
"content-type": "application/json"
}

namemap = json.load(open("world.json", "r"))

@app.get("/day_view")
async def root(start_at, end_at):
url = root_path + "/pageviews?start_at={:d}&end_at={:d}&unit=day&tz=Asia/Shanghai".format(
int(start_at),
int(end_at)
)
res = requests.get(url, headers=header)
res = json.loads(res.content)
return res

@app.get("/month_view")
async def root(start_at, end_at):
url = root_path + "/pageviews?start_at={:d}&end_at={:d}&unit=month&tz=Asia/Shanghai".format(
int(start_at),
int(end_at)
)
res = requests.get(url, headers=header)
res = json.loads(res.content)
return res

@app.get("/referrer")
async def root(start_at, end_at):
url = root_path + "/metrics?start_at={:d}&end_at={:d}&type=referrer".format(
int(start_at),
int(end_at)
)
res = requests.get(url, headers=header)
res = json.loads(res.content)
return res

@app.get("/country")
async def root(start_at, end_at):
url = root_path + "/metrics?start_at={:d}&end_at={:d}&type=country".format(
int(start_at),
int(end_at)
)
res = requests.get(url, headers=header)
res = json.loads(res.content)
ans = []
for item in res:
name = namemap[item["x"]]
ans.append({
"x": name,
"y": item["y"]
})
china = 0
for k in range(len(ans)):
item = ans[k]
if item["x"] in ["Hong Kong", "Taiwan", "Macau"]:
china += item["y"]
del ans[k]
for item in ans:
if item["x"] == "China":
item["y"] += china
return ans

其中world.json是一个用于国家/地区转换的文件,内容如下:

world.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
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
{
"ES": "Spain",
"DE": "Germany",
"IE": "Ireland",
"IT": "Italy",
"AT": "Austria",
"FR": "France",
"BE": "Belgium",
"FI": "Finland",
"DK": "Denmark",
"CZ": "Czech Republic",
"EE": "Estonia",
"HU": "Hungaryngary",
"JE": "Jersey",
"LU": "Luxembourg",
"LV": "Latvia",
"MT": "Malta",
"NL": "Netherlands",
"PT": "Portugal",
"RO": "Romania",
"SI": "Slovenia",
"SK": "Slovakia",
"AE": "United Arab Emirates",
"AF": "Afghanistan",
"AL": "Albania",
"AM": "Armenia",
"AO": "Angola",
"AR": "Argentina",
"AU": "Australia",
"AZ": "Azerbaijan",
"BD": "Bangladesh",
"BF": "Burkina Faso",
"BG": "Bulgaria",
"BH": "Bahrein",
"BI": "Burundi",
"BJ": "Benin",
"BN": "Brunei Darussalam",
"BO": "Bolivia",
"BR": "Brazil",
"BW": "Botswana",
"BY": "Byelorussia",
"CA": "Canada",
"CF": "Central Africa",
"CG": "Congo",
"CH": "Switzerland",
"CL": "Chile",
"CM": "Cameroon",
"CN": "China",
"CO": "Colombia",
"CR": "Costa Rica",
"CS": "Czech Repubic",
"CU": "Cuba",
"CY": "Cyprus",
"DO": "Dominican Republic",
"DZ": "Algeria",
"EC": "Ecuador",
"EG": "Egypt",
"ET": "Ethiopia",
"FJ": "Fiji",
"GA": "Gabon",
"GB": "United Kingdom",
"GD": "Grenada",
"GE": "Georgia",
"GH": "Ghana",
"GN": "Guinea",
"GR": "Greece",
"GT": "Guatemala",
"HK": "Hong Kong",
"HN": "Honduras",
"ID": "Indonesia",
"IL": "Israel",
"IN": "India",
"IQ": "Iraq",
"IR": "Iran",
"IS": "Iceland",
"JM": "Jamaica",
"JO": "Jordan",
"JP": "Japan",
"KG": "Kyrgyzstan",
"KH": "Cambodia",
"KP": "Dem. Rep. Korea",
"KR": "Korea",
"KT": "Ivory Coast",
"KW": "Kuwati",
"KZ": "Kazakhstan",
"LA": "Laos",
"LB": "Lebanon",
"LC": "Saint Lueia",
"LI": "Liechtenstein",
"LK": "Sri Lanka",
"LR": "Liberia",
"LT": "Lithuania",
"LY": "Libyan",
"MA": "Morocco",
"MC": "Monaco",
"MD": "Moldova",
"MG": "Madagascar",
"ML": "Mali",
"MM": "Myanmar",
"MN": "Mongolia",
"MO": "Macau",
"MU": "Mauritius",
"MW": "Malawi",
"MX": "Mexico",
"MY": "Malaysia",
"MZ": "Mozambique",
"NA": "Namibia",
"NE": "Niger",
"NG": "Nigeria",
"NI": "Nicaragua",
"NO": "Norway",
"NP": "Nepal",
"NZ": "New Zealand",
"OM": "Oman",
"PA": "Panama",
"PE": "Peru",
"PG": "Papua New Guinea",
"PH": "Philippines",
"PK": "Pakistan",
"PL": "Poland",
"PY": "Paraguay",
"QA": "Qatar",
"RU": "Russian Federation",
"SA": "Saudi Arabia",
"SC": "Republic of Seychelles",
"SD": "Sudan",
"SE": "Sweden",
"SG": "Singapore",
"SM": "San Marino",
"SN": "Senegal",
"SO": "Somalia",
"SY": "Syria",
"SZ": "Swaziland",
"TD": "Chad",
"TG": "Togo",
"TH": "Thailand",
"TJ": "Tajikistan",
"TM": "Turkmenistan",
"TN": "Tunisia",
"TR": "Turkey",
"TW": "Taiwan",
"TZ": "Tanzania",
"UA": "Ukraine",
"UG": "Uganda",
"US": "United States",
"UY": "Uruguay",
"UZ": "Uzbekistan",
"VC": "Saint Vincent",
"VE": "Venezuela",
"VN": "Viet Nam",
"YE": "Yemen",
"YU": "Yugoslavia",
"ZA": "South Africa",
"ZM": "Zambia",
"ZR": "Zaire",
"ZW": "Zimbabwe"
}

数据显示

构建完API后,我们开始改写之前的census.js,内容如下:

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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
// 访问日历
function generatePieces(maxValue, colorBox) {
var pieces = [];
var quotient = 1;
var temp = {'lt': 1, 'label': '0', 'color': colorBox[0]};
pieces.push(temp);

if (maxValue && maxValue >= 10) {
quotient = Math.floor(maxValue / 10)+1;
for (var i = 1; i <= 10; i++) {
var temp = {};
if (i == 1) temp.gte = 1;
else temp.gte = quotient * (i - 1);
temp.lte = quotient * i;
temp.color = colorBox[i];
pieces.push(temp);
}
}
return JSON.stringify(pieces);
}

function append_div_visitcalendar(parent, text) {
if (parent !== null) {
if (typeof text === 'string') {
var temp = document.createElement('div');
temp.innerHTML = text;
var frag = document.createDocumentFragment();
while (temp.firstChild) {
frag.appendChild(temp.firstChild);
}
parent.appendChild(frag);
} else {
parent.appendChild(text);
}
}
};

function compareFunction(propertyName){
return function (o1, o2) {
//获取比较的值
var v1 = o1[propertyName];
var v2 = o2[propertyName];
return v1 > v2 ? 1 : (v1 == v2 ? 0 : -1);
};
}

function filterTime(time) {
const date = new Date(time)
const Y = date.getFullYear()
const M = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1
const D = date.getDate() < 10 ? '0' + (date.getDate()) : date.getDate()
return `${Y}-${M}-${D}`
}

function calChart() {
let script = document.createElement("script");

let now = new Date();
let date = new Date();
date.setFullYear(now.getFullYear() - 1);
let start_at = date.getTime() - 3600 * 24 * ((date.getDay() + 1) % 7);
let end_at = now.getTime();

// API根据自己的实际更改
fetch('https://api.foolishfox.cn/umami/day_view?start_at=' + start_at + '&end_at=' + end_at).then(data => data.json()).then(data => {
data = data.pageviews;
data.sort(compareFunction("t"));

let calArr = [];
let maxValue = 0, total = 0, weekdatacore = 0, thisweekdatacore = 0;
let colorBox = ['#EBEDF0', '#FFE9BB', '#FFD1A7', '#FFBB95', '#FFA383', '#FF8D70', '#FF745C', '#FF5C4A', '#FF4638', '#FF2E26', '#FF1812'];
for (let i = 0; i < data.length; i++) {
if (i > 0) {
let pre = new Date(data[i - 1].t.replace(/-/g, "/"));
let tmp = new Date(data[i].t.replace(/-/g, "/"));
if (tmp.getTime() - pre.getTime() != 86400 * 1000)
for (let k = 1; k < (tmp.getTime() - pre.getTime()) / (86400 * 1000); k++) {
tmp = new Date(pre.getTime() + 86400 * 1000 * k);
calArr.push([filterTime(tmp), 0]);
}
}

calArr.push([data[i].t, data[i].y]);
maxValue = data[i].y > maxValue ? data[i].y : maxValue;
total += data[i].y;
}
if (calArr[calArr.length - 1][0] != filterTime(now)) calArr.push([filterTime(now), 0]);

for (let i = calArr.length - 1; i >= calArr.length - 7; i--) weekdatacore += calArr[i][1];
for (let i = calArr.length - 1; i >= calArr.length - 30; i--) thisweekdatacore += calArr[i][1];
let calArrJson = JSON.stringify(calArr);
script.innerHTML = `
var calChart = echarts.init(document.getElementById("calendar_container"));
var option = {
title: { text: '访问日历', x: 'center' },
tooltip: {
padding: 10,
backgroundColor: '#555',
borderColor: '#777',
borderWidth: 1,
textStyle: { color: '#fff' },
formatter: function (obj) {
var value = obj.value;
return '<div style="font-size: 14px;">' + value[0] + ': ' + value[1] + '</div>';
}
},
visualMap: {
show: false,
showLabel: true,
min: 0,
max: ${maxValue},
type: 'piecewise',
orient: 'horizontal',
left: 'center',
bottom: 0,
pieces: ${generatePieces(maxValue, colorBox)}
},
calendar: [{
left: 'center',
range: ['${calArr[0][0]}', '${calArr[calArr.length - 1][0]}'],
cellSize: [14, 14],
splitLine: {
show: false
},
itemStyle: {
color: '#ebedf0',
borderColor: '#fff',
borderWidth: 2
},
yearLabel: {
show: false
},
monthLabel: {
nameMap: 'cn',
fontSize: 11
},
dayLabel: {
formatter: '{start} 1st',
nameMap: 'cn',
fontSize: 11
}
}],
series: [{
type: 'heatmap',
coordinateSystem: 'calendar',
calendarIndex: 0,
data: ${calArrJson},
}]
};
calChart.setOption(option);`;
let style = '<style>.number{margin-top: 10px;text-align:center;width:100%;padding:10px;margin:0 auto;}.contrib-column{text-align:center;border-left:1px solid #ddd;border-top:1px solid #ddd;}.contrib-column-first{border-left:0;}.table-column{padding:10px;display:table-cell;flex:1;vertical-align:top;}.contrib-number{font-weight:400;line-height:1.3em;font-size:24px;display:block;}.left.text-muted{float:left;margin-left:9px;color:#767676;}.left.text-muted a{color:#4078c0;text-decoration:none;}.left.text-muted a:hover{text-decoration:underline;}h2.f4.text-normal.mb-3{display:none;}.float-left.text-gray{float:left;}.position-relative{width:100%;}@media screen and (max-width:650px){.contrib-column{display:none}}</style>';
style = '<div style="display:flex;width:100%" class="number"><div class="contrib-column contrib-column-first table-column"><span class="text-muted">过去一年访问</span><span class="contrib-number">' + total + '</span><span class="text-muted">' + calArr[0][0] + '&nbsp;-&nbsp;' + calArr[calArr.length - 1][0] + '</span></div><div class="contrib-column table-column"><span class="text-muted">最近30天访问</span><span class="contrib-number">' + thisweekdatacore + '</span><span class="text-muted">' + calArr[calArr.length - 30][0] + '&nbsp;-&nbsp;' + calArr[calArr.length - 1][0] + '</span></div><div class="contrib-column table-column"><span class="text-muted">最近7天访问</span><span class="contrib-number">' + weekdatacore + '</span><span class="text-muted">' + calArr[calArr.length - 7][0] + '&nbsp;-&nbsp;' + calArr[calArr.length - 1][0] + '</span></div></div>' + style;

document.getElementById("calendar_container").after(script);
append_div_visitcalendar(calendar_container, style);
}).catch(function (error) {
console.log(error);
});
}

// 访问地图
function mapChart () {
let script = document.createElement("script");

let now = new Date();
let date = new Date();
// 这个根据自己开始的时间修改
date.setFullYear(2021, 01, 01);
let start_at = date.getTime();
let end_at = now.getTime();

// API根据自己的实际更改
fetch('https://api.foolishfox.cn/umami/country?start_at=' + start_at + '&end_at=' + end_at).then(data => data.json()).then(data => {
let mapArr = [];
let maxValue = 0;
for (let i = 0; i < data.length; i++) {
maxValue = data[i].y > maxValue ? data[i].y : maxValue ;
mapArr.push({ name: data[i].x, value: data[i].y });
}
let mapArrJson = JSON.stringify(mapArr);
script.innerHTML = `
var mapChart = echarts.init(document.getElementById('map_container'), 'light');
var mapOption = {
title: { text: '访问地点(按人数记)', x: 'center' },
tooltip: { trigger: 'item' },
visualMap: {
min: 0,
max: ${maxValue},
left: 'left',
top: 'bottom',
text: ['高','低'],
color: ['#1E90FF', '#AAFAFA'],
calculable: true
},
series: [{
name: '访问人数',
type: 'map',
mapType: 'world',
showLegendSymbol: false,
label: {
emphasis: { show: false }
},
itemStyle: {
normal: {
areaColor: 'rgba(255, 255, 255, 0.1)',
borderColor: '#121212'
},
emphasis: { areaColor: 'gold' }
},
data: ${mapArrJson}
}]
};
mapChart.setOption(mapOption);`;
document.getElementById('map_container').after(script);
}).catch(function (error) {
console.log(error);
});
}

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");

let now = new Date();
let date = new Date();
// 这个根据自己开始的时间修改
date.setFullYear(2021, 01, 01);
let start_at = date.getTime();
let end_at = now.getTime();

// API根据自己的实际更改
fetch('https://api.foolishfox.cn/umami/month_view?start_at=' + start_at + '&end_at=' + end_at).then(data => data.json()).then(data => {
data = data.pageviews;

let date = new Date();
let monthValueArr = {};
for (let i =2020; i <= date.getFullYear(); i++) monthValueArr[String(i)] = [ , , , , , , , , , , , ];
for (let i = 0; i < data.length; i++) {
let year = get_year(data[i].t);
let month = get_month(data[i].t);
monthValueArr[String(year)][String(month-1)] = data[i].y;
}
script.innerHTML = `
var trendsChart = echarts.init(document.getElementById('trends_container'), 'light');
var trendsOption = {
title: { text: '访问趋势', x: 'center' },
tooltip: { trigger: 'axis' },
legend: { data: ['2021', '2022'], x: 'right' },
xAxis: {
name: '日期', type: 'category', boundaryGap: false,
data: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
},
yAxis: { name: '访问次数', type: 'value' },
series: [
{
name: '2021', type: 'line', smooth: true,
data: [${monthValueArr["2021"]}],
markLine: { data: [{type: 'average', name: '平均值'}] }
},
{
name: '2022', type: 'line', smooth: true,
data: [${monthValueArr["2022"]}],
markLine: { data: [{type: 'average', name: '平均值'}] }
}
]
};
trendsChart.setOption(trendsOption);`;
document.getElementById('trends_container').after(script);
}).catch(function (error) {
console.log(error);
});
}

// 访问来源
function sourcesChart () {
let script = document.createElement("script");
var link = 0, direct = 0, search = 0;
var google = 0, baidu = 0, bing = 0;
var github = 0, travel = 0;

let now = new Date();
let date = new Date();
// 这个根据自己开始的时间修改
date.setFullYear(2021, 01, 01);
let start_at = date.getTime();
let end_at = now.getTime();

// API根据自己的实际更改
fetch('https://api.foolishfox.cn/umami/referrer?start_at=' + start_at + '&end_at=' + end_at).then(data => data.json()).then(data => {
for (let i = 0; i < data.length; i++) {
var ref = data[i].x;
if(ref == "" || ref.includes("foolishfox.cn")) direct += data[i].y;
else if(ref.includes("bing.com")) bing += data[i].y;
else if(ref.includes("baidu.com")) baidu += data[i].y;
else if(ref.includes("google.com")) google += data[i].y;
else if(ref.includes("sogou.com") || ref.includes("sm.cn") || ref.includes("toutiao.com") || ref.includes("so.com"))
search += data[i].y
else if(ref.includes("github.com")) github += data[i].y;
else if(ref.includes("travellings") || ref.includes("foreverblog")) travel += data[i].y;
else link += data[i].y
}

link += github + travel;
search += baidu + google + bing;
script.innerHTML += `
var sourcesChart = echarts.init(document.getElementById('sources_container'), 'light');
var sourcesOption = {
title: { text: '访问来源', x: 'center', },
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
legend: {
data: ['直达', '外链', '搜索', '百度', '谷歌', '必应', 'Github', '开往/十年之约'],
y: 'bottom'
},
series: [
{
name: '来源明细', type: 'pie', radius: ['45%', '60%'],
labelLine: { length: 30 },
label: {
formatter: '{a|{a}}{abg|}\\n{hr|}\\n {b|{b}: }{c} {per|{d}%} ',
backgroundColor: '#F6F8FC', borderColor: '#8C8D8E',
borderWidth: 1, borderRadius: 4,
rich: {
a: { color: '#6E7079', lineHeight: 22, align: 'center' },
hr: { borderColor: '#8C8D8E', width: '100%', borderWidth: 1, height: 0 },
b: { color: '#4C5058', fontSize: 14, fontWeight: 'bold', lineHeight: 33 },
per: { color: '#fff', backgroundColor: '#4C5058', padding: [3, 4], borderRadius: 4 }
}
},
data: [
{value: ${search - google - baidu - bing}, name: '其他', itemStyle: { color : '#008000' }},
{value: ${google}, name: '谷歌', itemStyle: { color : '#009000' }},
{value: ${baidu}, name: '百度', itemStyle: { color : '#00A000' }},
{value: ${bing}, name: '必应', itemStyle: { color : '#00B000' }},
{value: ${direct}, name: '直达', itemStyle: { color : '#FFDB5C' }},
{value: ${github}, name: 'Github', itemStyle: { color : '#10A3C7' }},
{value: ${travel}, name: '开往/十年之约', itemStyle: { color : '#21B4D8' }},
{value: ${link - github - travel}, name: '其他', itemStyle: { color : '#32C5E9' }}
]
},
{
name: '访问来源', type: 'pie', selectedMode: 'single', radius: [0, '30%'],
label: { position: 'inner', fontSize: 14},
labelLine: { show: false },
data: [
{value: ${search}, name: '搜索', itemStyle: { color : '#008000' }},
{value: ${direct}, name: '直达', itemStyle: { color : '#FFDB5C' }},
{value: ${link}, name: '外链', itemStyle: { color : '#32C5E9' }}
]
},
]
};
sourcesChart.setOption(sourcesOption);
window.addEventListener("resize", () => {
calChart.resize();
mapChart.resize();
trendsChart.resize();
sourcesChart.resize();
});`;
}).catch(function (error) {
console.log(error);
});
document.getElementById('sources_container').after(script);
}

if (document.getElementById("calendar_container")) calChart();
if (document.getElementById('map_container')) mapChart();
if (document.getElementById('trends_container')) trendsChart();
if (document.getElementById('sources_container')) sourcesChart();

效果展示

动态展示页面:戳这里

日历图与地图
日历图与地图
访问趋势与来源
访问趋势与来源

V2 版本更新

Umami 更新

本文安装时Umami的版本是1.38.0,目前已经更新到了2.6.2,跨大版本升级需要更新数据库,大致流程如下:

  1. 拉取最新仓库:
1
git pull
  1. 更新到V1的最新版本1.40.0
1
2
3
git checkout cc2be9f4
yarn install
yarn build
  1. 进行数据迁移
1
npx @umami/migrate-v1-v2@latest
  1. 升级到V2
1
2
3
git checkout master
yarn install
yarn build

注意在安装sharp包时可能会由于网络下载相关内容超时导致失败,可以在.npmrc中添加:

1
2
sharp_binary_host=https://npm.taobao.org/mirrors/sharp
sharp_libvips_binary_host=https://npm.taobao.org/mirrors/sharp-libvips

API程序更新

在升级之后,Umami的API也发生了改变,主要包括:

  1. GET /api/website/{id}/pageviews中的id进行了修改,变成了由数字和字母组成的混合字符串,例如74204a9f-8aca-4adf-9f03-d509f0a07116,此外website变为了websites
  2. API中的时间起止从start_atend_at改为了startAtendAt
  3. API中的时区标识从tz改为timezone
  4. 最长只能获取90天内的每日浏览数据,超过90天将会自动归并为每月的访问数据 Fixed in v2.9.0.
  5. 返回数据中时间标识从t改为了x

根据上述更新,我们需要对main.pycensus.js进行修改,示例如下:

  • main.py
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
...
root_path = "https://<your-umami>/api/websites/{}"
...
@app.get("/day_view")
async def root(startAt, endAt):
url = root_path + "/pageviews?startAt={:d}&endAt={:d}&unit=day&timezone=Asia/Shanghai".format(
int(startAt),
int(endAt)
)
res = requests.get(url, headers=header)
res = json.loads(res.content)
return res
...
  • census.js
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
...
function calChart() {
...
// 90 天版本
let now = new Date();
let endAt = now.getTime();
let startAt = endAt - 90 * 86400 * 1000;

// 1 年版本
let now = new Date();
let date = new Date();
date.setFullYear(now.getFullYear() - 1);
let startAt = date.getTime() - 3600 * 24 * ((date.getDay() + 1) % 7);
let endAt = now.getTime();

// API根据自己的实际更改
fetch('https://api.foolishfox.cn/umami/day_view?startAt=' + startAt + '&endAt=' + endAt).then(data => data.json()).then(data => {
data = data.pageviews;
data.sort(compareFunction("x"));
...
let pre = new Date(data[i - 1].x.replace(/-/g, "/"));
let tmp = new Date(data[i].x.replace(/-/g, "/"));
...
calArr.push([data[i].x, data[i].y]);
}
...
}
...

广告反屏蔽

Umami 的默认跟踪代码提供的链接是 https://umami.foolishfox.cn/script.js,会被大多数的广告插件屏蔽(如 AdGuard),统计不到访客信息。通过设置 TRACKER_SCRIPT_NAME 可以修改链接,起到反屏蔽的效果,修改后的跟踪代码链接为https://umami.foolishfox.cn/Bf5ZmaKLu245LG

docker-compose.yml
1
2
3
4
5
6
version: "3.9"
services:
umami:
image: ghcr.io/umami-software/umami:mysql-latest
environment:
TRACKER_SCRIPT_NAME: Bf5ZmaKLu245LG

参考资料

  1. Migrating v1 to v2
  2. 一站式解决Node项目中遇到的 诸如sharp: Command failed.或Building fresh packages…始终执行问题
  3. Environment variables