Fluid -24- Leancloud 失效解决方案 —— 自建站点 PV UV 统计

本文最后更新于:2022年8月16日 下午

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 中的显示

实现流程

计数工具

LeanCloud 数据继承

  • 如果有之前的 LeanCloud 国际版数据可以导出为 Json

  • 想办法(大陆IP无法访问)进入 国际版 LeanCloud

  • 导出数据

  • 导出成功

  • 随后可以看到数据库表单基本整理成了json文件下载了下来

  • 对于本文应用来说,核心文件在 Counter.0.jsonl 文件中,该文件主要内容为 json 格式,删去第一行稍加修改即可作为正常 json 文件使用

  • 之后可以按照自己的需求整理成方便可用的计数文件

后台代码

依赖 Umami 的 API,需要搭建好 API 获取环境

核心代码
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
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_urlheader 中的 <url-to-umami><your-token> 为你自己的值

  • active_num 函数获取当前活跃用户数

  • PVUV_num 函数获取站点 PV UV 数

  • post_pv 函数获取 post PV UV 数

  • js_str 函数整合 active_numPVUV_num 的结果返回 js 代码

  • self.conter_dict 为 LeanCloud 计数数据备份 Json 字典

  • 核心代码的行为:

    • 利用 Umami API 获取需要的数据
    • 整合成 js 字符串或直接返回数据
    • js 串功能为修改ID 为 PVstatic, UVstaticACTstatic 的元素内容
后端代码
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
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 示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [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

  • 记得设置开机自动启动

    1
    sudo systemctl enable pvuv.service

前端显示

网站 PV UV
  • 我选择在 Fluid 主题配置文件中加入该部分前端代码

  • 打开 Hexo/_config.fluid.yml 文件

  • 关闭原始 PV、UV 统计

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
      
    # 展示网站的 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 中加入前端代码

    1
    2
    3
    4
    5
    6
    <div style="font-size: 0.85rem;">
    <span> 总访问量 <span style="color: #d7d8d9;" id="PVstatic">0</span> 次&nbsp</span>
    <span> 总访客数 <span style="color: #d7d8d9;" id="UVstatic">0</span> 人&nbsp</span>
    <span> 当前在线 <span style="color: #d7d8d9;" id="ACTstatic">0</span> 人&nbsp</span>
    <script src="https://<url-to-app>/statistics" defer></script>
    </div>
post 文章 PV UV
  • 修改 fluid 主题配置文件 Hexo/_config.fluid.yml,加入新的文章浏览计数来源,我起名叫 vvdpostpvuv

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 浏览量计数
    # 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 分支:

    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
        <% } 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] + '&nbsp&nbsp' %>
    <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';
    }
    }
    }
    };
    </script>
    <% } %>

https 访问

  • Nginx 做 ssl 反向代理,给出 https 访问链接、端口

效果展示

  • 站点 PV UV

  • Post PV UV

参考资料


Fluid -24- Leancloud 失效解决方案 —— 自建站点 PV UV 统计
https://www.zywvvd.com/notes/hexo/theme/fluid/fluid-build-pvuv-statistics/build-pvuv-statistics/
作者
Yiwei Zhang
发布于
2022年8月15日
许可协议