Fluid -24- Leancloud 失效解决方案 —— 自建站点 PV UV 统计
2022年8月,LeanCloud 国际版不再为来自中国大陆的 IP 提供服务,基于 LeanCloud 的站点统计因此失效,本文基于 Umami 的统计信息自建 PV UV 统计后台,解决上述问题。 前端、后端小白,对Python比较熟悉,后端代码用 Python 实现的,仅仅完成了功能,过程也比较繁琐,思路过程供大家参考
背景
- 基于某些原因,LeanCloud 国际版不再为来自中国大陆的 IP 提供服务
- 在 Hexo Fluid 主题中使用 LeanCloud 的主要有 站点/文章 PV、UV 统计和评论系统
- 截止当前(2022年8月15日)Walline 的 LeanCloud 数据库可以正常访问,即仍在正常运转,可能是有后台的代理服务器
- 站点 PV、UV 凉了,于是自建
功能需求
- 全站页面浏览量 (PV) 统计
- 全站用户访问量 (UV) 统计
- 当前在线用户数统计
- 文章页面浏览量统计
- 文章用户访问量统计
原理思路
计数工具
- 讲道理只要有看门的 callback 将用户信息发送到后台进行统计并想办法显示统计数据即可
- github 上有很多工程可以使用
- 我在之前搭建了基于 Google 统计的工具 Umami
- 正好 Umami 有方便的 API 接口 可以调用
于是决定基于这款工具开发 PV UV 统计
LeanCloud 数据继承
- 如果直接放弃 LeanCloud 那么之前的访问数据就清零了
- 要是觉得可惜的话可以将 LeanCloud 数据下载下来,在 Umami 计数结果中加上 LeanCloud 的以往数据即可 当然了,优秀的同学也可以去改 Umami 的数据库
后台代码
- 需要后端服务器监听端口,负责为显示 PVUV 数据请求提供服务
- 核心就是对 Umami API 的封装
- 需要解决跨域访问的问题
前端显示
- 建立几个 span 块,JavaScript 代码动态修改并填入数据内容
https 访问
- https 站点 要求内部链接都是 https
- 于是需要 Nginx 反向代理成 https
插入 Fluid
- 在Fluid 合适的部分插入上述前端代码,完成在 Hexo 中的显示
实现流程
计数工具
- 搭建个人 Umami 统计平台: 站统计工具 Umami 安装部署教程
LeanCloud 数据继承
- 如果有之前的 LeanCloud 国际版数据可以导出为 Json
- 想办法(大陆IP无法访问)进入 国际版 LeanCloud
- 导出数据
- 导出成功
- 随后可以看到数据库表单基本整理成了json文件下载了下来
-
对于本文应用来说,核心文件在
Counter.0.jsonl
文件中,该文件主要内容为 json 格式,删去第一行稍加修改即可作为正常 json 文件使用 - 之后可以按照自己的需求整理成方便可用的计数文件
后台代码
依赖 Umami 的 API ,需要搭建好 API 获取环境
核心代码
import requests
import json
import mtutils as mt
from pathlib import Path
class Statistic:
root_url = "http://<url-to-umami>/api/website/1/"
header={
"Accept": "application/json",
"Access-Control-Allow-Origin": '*',
"Authorization": "Bearer <your-token>"
def __init__(self):
conter_path = Path(__file__).with_name('conter.json')
self.conter_dict = mt.json_load(conter_path)
@staticmethod
def make_json_str(pv, uv, act):
res_str = "(function vvdstatistics(){"+\
"var PVstatic='" +\
str(pv) +\
"';var dom=document.querySelector('#PVstatic');Array.isArray(dom)?dom[0].innerText=PVstatic:dom.innerText=PVstatic;"+\
"var UVstatic='" +\
str(uv) +\
"';var dom=document.querySelector('#UVstatic');Array.isArray(dom)?dom[0].innerText=UVstatic:dom.innerText=UVstatic;"+\
"var ACTstatic='" +\
str(act) +\
"';var dom=document.querySelector('#ACTstatic');Array.isArray(dom)?dom[0].innerText=ACTstatic:dom.innerText=ACTstatic;"+\
"})()"
return res_str
def active_num(self):
url = self.root_url + 'active'
res = requests.get(url=url, data=json.dumps({}), headers=self.header)
res_dict = json.loads(res.text)
act_str = max(1, res_dict[0]['x'])
return act_str
def PVUV_num(self):
url = self.root_url + 'stats?start_at=1350679719687&end_at=1990039038644'
res = requests.get(url=url, data=json.dumps({}), headers=self.header)
res_dict = json.loads(res.text)
pv = res_dict['pageviews']['value'] + self.conter_dict['site-pv']
uv = res_dict['uniques']['value'] + self.conter_dict['site-uv']
return pv, uv
def js_str(self):
pv, uv = self.PVUV_num()
act = self.active_num()
return self.make_json_str(pv, uv, act)
def post_pv(self, sub_url):
url = self.root_url + 'stats?start_at=1350679719687&end_at=1990039038644' + '&url=' + sub_url
res = requests.get(url=url, data=json.dumps({}), headers=self.header)
res_dict = json.loads(res.text)
pv = res_dict['pageviews']['value'] + self.conter_dict.get(sub_url, 0)
uv = res_dict['uniques']['value']
return pv, uv
-
使用时需要修改
root_url
和header
中的<url-to-umami>
和<your-token>
为你自己的值 - active_num 函数获取当前活跃用户数
- PVUV_num 函数获取站点 PV UV 数
- post_pv 函数获取 post PV UV 数
- js_str 函数整合 active_num 和 PVUV_num 的结果返回 js 代码
- self.conter_dict 为 LeanCloud 计数数据备份 Json 字典
- 核心代码的行为:
- 利用 Umami API 获取需要的数据
- 整合成 js 字符串或直接返回数据
- js 串功能为修改ID 为 `PVstatic`, `UVstatic` 和 `ACTstatic` 的元素内容
后端代码
from flask import Flask, request
from flask_cors import CORS
import mtutils as mt
from lib import statis_obj
port = '<your-port>'
log_file_path = '/usr/local/static/log.log'
app = Flask(__name__)
app.logger = mt.log_init(log_file_path)
CORS(app, supports_credentials=True)
@app.route("/statistics", methods=['GET','POST'])
def statistics():
res = statis_obj.js_str()
return res
@app.route("/poststats", methods=['GET','POST'])
def poststats():
url = request.data.decode('utf8')[1:-1]
pv, uv = statis_obj.post_pv(url)
return {'pv': pv, 'uv': uv}
if __name__ == '__main__':
app.logger("**************** Static Sever Start *******************")
app.run('0.0.0.0', port=port)
pass
-
使用时将
<your-port>
换为你自己需要监听的端口 - 开放两个路由:
- **statistics** 站点 PV UV 和 活跃用户数,返回内容为一段 js 代码 访问示例
- **poststats** 文章 PV UV 访问示例
搭建服务
代码调整好后需要让他在服务器自动运行
需要用到 systemctl 工具
此处 service 示例代码
[Unit]
Description = Service to count pv,uv for www.zywvvd.com
After = network.target
[Service]
ExecStart = /home/lighthouse/anaconda3/bin/python main.py
WorkingDirectory = /usr/local/static/
StandardOutput = inherit
StandardError = inherit
Restart = always
User = lighthouse
[Install]
WantedBy=multi-user.target
记得设置开机自动启动
sudo systemctl enable pvuv.service
前端显示
网站 PV UV
我选择在 Fluid 主题配置文件中加入该部分前端代码
打开
Hexo/_config.fluid.yml
文件
关闭原始 PV、UV 统计
# 展示网站的 PV、UV 统计数
# Display website PV and UV statistics
statistics:
enable: false
# 统计数据来源,使用 leancloud 需要设置 `web_analytics: leancloud` 中的参数;使用 busuanzi 不需要额外设置,但是有时不稳定,另外本地运行时 busuanzi 显示统计数据很大属于正常现象,部署后会正常
# Data source. If use leancloud, you need to set the parameter in `web_analytics: leancloud`
# Options: busuanzi | leancloud
source: "leancloud"
pv_format: "总访问量 {} 次"
uv_format: "总访客数 {} 人"
由于 LeanCloud 仅在大陆无法访问,国外网友访问时还是会正常显示一行 PV,UV 统计,为了避免重复把原来的关掉
在 footer.content 中加入前端代码
<div style="font-size: 0.85rem;">
<span> 总访问量 <span style="color: #d7d8d9;" id="PVstatic">0</span> 次 </span>
<span> 总访客数 <span style="color: #d7d8d9;" id="UVstatic">0</span> 人 </span>
<span> 当前在线 <span style="color: #d7d8d9;" id="ACTstatic">0</span> 人 </span>
<script src="https://<url-to-app>/statistics" defer></script>
</div>
post 文章 PV UV
修改 fluid 主题配置文件
Hexo/_config.fluid.yml
,加入新的文章浏览计数来源,我起名叫
vvdpostpvuv
# 浏览量计数
# Number of visits
views:
enable: true
# 统计数据来源
# Data Source
# Options: busuanzi | leancloud | 自建基于 Umami 统计的 文章PV vvdpostpvuv
source: "vvdpostpvuv"
format: "{} 次"
打开文件
Hexo/themes/fluid/layout/_partials/post/meta-top.ejs
, 加入新的 else 分支:
<% } else if (theme.post.meta.views.source === 'busuanzi') { %>
<span id="busuanzi_container_page_pv" style="display: none">
<i class="iconfont icon-eye" aria-hidden="true"></i>
<%- views_texts[0] %><span id="busuanzi_value_page_pv"></span><%- views_texts[1] %>
</span>
<% import_js(theme.static_prefix.busuanzi, 'busuanzi.pure.mini.js', 'defer') %>
<% } else if (theme.post.meta.views.source === 'vvdpostpvuv') { %>
<span id="vvdpost_container_page_pvuv" style="display: none">
<i class="iconfont icon-eye" aria-hidden="true"></i>
<%- views_texts[0] %><span id="vvdpost_value_page_pv">0</span><%- views_texts[1] + '  ' %>
<i class="iconfont icon-users" aria-hidden="true"></i>
<%- views_texts[0] %><span id="vvdpost_value_page_uv">0</span><%- ' 人' %>
</span>
<script>
console.log(window.location.pathname)
var httpRequest = new XMLHttpRequest();
httpRequest.open('POST', 'https://101.43.39.125:6101/poststats', true);
httpRequest.setRequestHeader("Content-type","application/json");
httpRequest.send(JSON.stringify(window.location.pathname));
httpRequest.onreadystatechange = function () {//请求后的回调接口,可将请求成功后要执行的程序写在其中
if (httpRequest.readyState == 4 && httpRequest.status == 200) {//验证请求是否发送成功
var json = httpRequest.responseText;//获取到服务端返回的数据
var obj = JSON.parse(json);
console.log(obj.pv);
var pvCtn = document.querySelector('#vvdpost_container_page_pvuv');
console.log(pvCtn);
if (pvCtn) {
var pv_ele = document.querySelector('#vvdpost_value_page_pv');
console.log(pv_ele);
var uv_ele = document.querySelector('#vvdpost_value_page_uv');
console.log(uv_ele);
if (uv_ele && uv_ele) {
pv_ele.innerText = obj.pv;
uv_ele.innerText = obj.uv;
pvCtn.style.display = 'inline';