前言 通过百度统计、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 requestsimport time, datetimestart_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))) access_token = '121.6e...' site_id = '16******' dataUrl = 'https://openapi.baidu.com/rest/2.0/tongji/report/getData?access_token=' + access_token + '&site_id=' + site_id 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 ="https://www.foolishfox.cn/js/census.js" > </script >
访问日历 访问日历类似于 Github 贡献日历,有两种方法实现。
源自于hexo-githubcalendar 插件 ,由Eurkon 修改,原文见参考资料 3 。需要修改的地方有: 1 2 3 4 5 6 7 ... fetch ('https://api.foolishfox.cn/data/calendar.json?date' +new Date ()).then (data => data.json ()).then (res => {... visit_calendar ('pv_count' , visit_color);...
另外,需要更改容器 id 可以直接搜索替换即可。
通过census.js 进行设置,包括后续的访问地图、访问趋势(访问次数)和访问来源都是基于此文件通过 Echarts 实现的。 1 2 3 4 5 6 7 8 9 10 11 var metrics = 'pv_count' var metricsName = (metrics === 'pv_count' ? '访问次数' : (metrics === 'visitor_count' ? '访客数' : '' ))... function calChart ( ) { let script = document .createElement ("script" ) 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" ) 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 59 60 61 62 63 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" ) 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 ]; 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 ]; } script.innerHTML = ` var trendsChart = echarts.init(document.getElementById('trends_container'), 'light'); var trendsOption = { title: { text: '访问趋势', x: 'center' }, tooltip: { trigger: 'axis' }, legend: { data: ['2020', '2021', '2022'], 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: '平均值'}] } }, { name: '2022', type: 'line', smooth: true, data: [${monthValueArr["2022" ]} ], 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.com和www.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 ... 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 ] }); link = sourcesArr[1 ]['value' ] ; search = sourcesArr[2 ]['value' ]; direct = sourcesArr[0 ]['value' ]; ... 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 ]; 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} , 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 不想使用 python 下载文件保存,想要实时统计 根据参考资料 2 中 4.3 节 —— 自建 Vercel API(可选)进行设置,替换上述文件中的 url 即可 (感谢秋水)
根据参考资料 2 中 5.3 节进行设置,替换上述js文件中的url即可,具体形式可参考参考资料 2 中 4.2 节。
依然出现跨域问题? 请务必保证通过 2.2 节获取的文件保存在博客的网站目录下,并通过 url 可以访问;如果想要向我一样通过其他域名(例如api.foolishfox.cn/data/*.json)访问,需要对服务器进行设置,以 Nginx 为例:
configuration 1 2 3 4 5 6 7 8 9 location ~ ^/data/ { if ($http_origin ~* (http://localhost:4000|https://www.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' ; } }
地图显示错误 务必注意,Echarts 目前已经不提供地图 js / json 文件的下载,所以要通过 npm 获取到旧版本中的China.js文件,因此 Echarts 的版本最好也保持一致
Echarts 绘制的访问日历无法自动适应窗口大小 正在积极解决中…
更新 2021.6.1:访问趋势中第 23 行代码应改为: 1 monthValueArr[String (year)][String (month-1 )] = monthValue[i][0 ] === '--' ? 0 : monthValue[i][0 ];
自动更新token 百度统计的token需要每个月更新一次,手动更新很麻烦,所以可以在代码中添加这一功能。首先更改downFile函数: 1 2 3 4 5 6 7 8 9 def downFile (url, fileName, prefix='./' ): res = requests.get(url) res = json.loads(res.content) if ('error_code' in res.keys()): update(prefix=prefix) downFile(url, fileName, prefix) with open (prefix+fileName, 'w' ) as f: json.dump(res, f)
当发现token错误时,我们执行update函数:
1 2 3 4 5 6 7 8 9 def update (prefix, url=updateUrl ): res = requests.get(url) res = json.loads(res.content) print (res) config['access' ] = res['access_token' ] config['refresh' ] = res['refresh_token' ] with open (prefix + 'token.json' , 'w' ) as f: json.dump(config, f)
config是通过json加载了目录下的token.json文件,格式如下所示,注意添加访问权限(例如Nginx中添加该路径返回403)。
1 2 3 4 5 6 7 8 { "access" : "access token" , "refresh" : "refresh token" , "site_id" : "site id" , "api_key" : "api key" , "serect_key" : "serect key" }
updateUrl是自动更新的url:
1 updateUrl = 'http://openapi.baidu.com/oauth/2.0/token?grant_type=refresh_token&refresh_token=' + config['refresh' ] + '&client_id=' + config['api_key' ] + '&client_secret=' + config['serect_key' ]
完整的py文件可以点击下载 。
参考资料 Hexo 博客访问统计图 Hexo 博客实时访问统计图 Hexo 博客访问日历图 百度统计用户手册 Echarts 文档