Featured image of post 用Python简单实现博客的访问统计

用Python简单实现博客的访问统计

起因

看别人博客都有访客人数统计,搞得我也想来整一个。最初是看到别人使用的是不蒜子统计平台,虽然使用起来简单粗暴,但我实在不放心第三方的平台,指不定哪天突然就跑路了,决定先找个能独立部署的开源工具来搭建。又去谷歌找了一圈,看到有个开源统计系统Umami还不错,数据展示直观、统计功能又强大,但是在我看到它需要Node.js和MySQL时还是放弃了,毕竟我只需要的是访客人数统计,并不需要其他太多的功能,增加系统复杂性只会让我维护变得更困难。最后决定,使用Python来自己编写实现这个功能。

开始编写

后端部分

这里使用了Sqlite3做数据库,分别创建两个功能接口:一个通过前端传来带路径参数的Get请求,来记录访客的IP、路径和时间,另一个则是通过路径参数查询并返回给前端访客次数。由此构成了统计的基本功能。
废话不多说 (其实就是懒) ,这里直接上代码。

 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
import sqlite3, time, logging
from flask import Flask, request, render_template
from gevent import pywsgi

logging.basicConfig(
	level=logging.DEBUG,
	filename='log.txt',
	filemode='a',
	format='[%(asctime)s] %(levelname)s: %(message)s',
	datefmt='%Y-%m-%d %H:%M:%S'
)

logging.info("系统启动中...")
logging.info("程序版本:V1.0")
logging.info("编写日期:2023-06-26")
logging.info("编写者:LaoZhu")

# 数据库部分
try:
	conn = sqlite3.connect('count.db')
	c = conn.cursor()
	c.execute(''' CREATE TABLE IF NOT EXISTS COUNT( 
		IP TEXT NOT NULL,
		PATH TEXT NOT NULL,
		DATE TEXT NOT NULL
		);'''
	)
except:
	logging.error("数据库打开失败")
	exit(0)


# WEB接口部分
app = Flask(__name__)

@app.route('/')
def html():
	return "老猪的博客访问统计系统"

@app.route('/addCount', methods=["GET"])
def count():
	#获取IP
	ip = request.remote_addr
	if(request.headers.get('X-Real-IP') != None):
		ip = request.headers.get('X-Real-IP')
	#获取路径
	path = request.args.get("pathname")
	if(path == ''):
		path = None
	#获取时间
	date = time.strftime("%Y-%m-%d %H:%M:%S",time.localtime())
	#上传数据
	try:
		c.execute(
			"INSERT INTO COUNT (IP, PATH, DATE) VALUES (?, ?, ?)",
			[ip, path, date]
		)
		conn.commit()
		logging.info(f"用户({ip})访问了 {path}")
		return "数据上传成功"
	except:
		logging.warning(f"用户({ip})记录失败")
		return "数据上传失败"

@app.route('/getCount', methods=["GET"])
def getCount():
	#获取IP
	ip = request.remote_addr
	if(request.headers.get('X-Real-IP') != None):
		ip = request.headers.get('X-Real-IP')
	#获取路径
	path = request.args.get("pathname")
	if(path == ''):
		path = None
	#查找数据
	try:
		c.execute(
			"SELECT COUNT(*) FROM (SELECT * FROM COUNT WHERE PATH = ? GROUP BY IP, STRFTIME('%Y-%m-%d', DATE))",
			[path]
		)
		#按次数计数:SELECT COUNT(*) FROM COUNT WHERE PATH = ?
		#按小时计数:SELECT COUNT(*) FROM (SELECT * FROM COUNT WHERE PATH = ? GROUP BY IP, STRFTIME('%Y-%m-%d %H', DATE))
		#按每日计数:SELECT COUNT(*) FROM (SELECT * FROM COUNT WHERE PATH = ? GROUP BY IP, STRFTIME('%Y-%m-%d', DATE))
		count = str(c.fetchone()[0])
		logging.info(f"用户({ip})查询了 {path}")
		return count
	except:
		logging.warning(f"用户({ip})查询失败")
		return ""

logging.info('系统启动完成')
server = pywsgi.WSGIServer(('0.0.0.0', 5000), app, log = None)
server.serve_forever()

前端部分

这里也不多说了 (就是懒) ,实际也就是创建了增加计数和获取计数的两个函数,在前端调用即可。

 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
function addCount() {
    var origin = window.location.origin;
    var pathname = window.location.pathname;
    var url = origin + '/addCount?pathname=' + pathname;

    var httpRequest = new XMLHttpRequest();
    httpRequest.open('GET', url, true);
    httpRequest.send();
}


function getCount(pathname) {
    var origin = window.location.origin;
    var url = origin + '/getCount?pathname=' + pathname;

    var httpRequest = new XMLHttpRequest();
    httpRequest.open('GET', url, true);
    httpRequest.send();

    httpRequest.onload = function() {
        if(httpRequest.status == 200){
            var count = Number(httpRequest.responseText);
            var element = document.getElementById("count-" + pathname);
            
            if(count != NaN && element != NaN) {
                element.innerText = count;
            }
        }	
    }	
}

当然还要在Nginx上添加反向代理配置。

1
2
3
4
5
6
7
8
9
	location /addCount {
		proxy_set_header X-Real-IP $remote_addr;
		proxy_pass http://127.0.0.1:5000;
	}
        
	location /getCount {
		proxy_set_header X-Real-IP $remote_addr;
		proxy_pass http://127.0.0.1:5000;
	}

主题内容修改

修改Stack主题样式文件[details.html]以显示博客访问人数,在<footer class=“article-time”>标签附近,添加如下内容:

 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
//文件位置: ./themes/hugo-theme-stack/layouts/partials/article/components/details.html
	...
	{{ $showViews := .Params.views | default (.Site.Params.article.views) }}

	{{ if $showFooter }}
	<footer class="article-time">
		...
		{{ if $showViews }}
			<div>
				{{ partial "helper/icon" "eye" }}
				<script src="/count.js"></script>
				<script>
					getCount( "{{ .RelPermalink }}" );
				</script>
				<time class="rticle-time--views">
					{{ T "article.views.main"}}
					<span id="count-{{ .RelPermalink }}">0</span>
					{{ T "article.views.unit"}}
				</time> 
			</div>
		{{ end }}

	</footer>
	{{ end }}
	...

为保证每次访问博客,有且仅有一次增加计数,所以我把增加计数的函数放在了主题头部的自定义页:

1
2
3
4
5
//文件位置: ./themes/hugo-theme-stack/layouts/partials/head/custom.html
<script src="/count.js"></script>
<script>
	addCount();
</script>

最后别忘了启用该功能和添加显示文本:

1
2
3
#文件位置: ./config/_default/params.toml
[article]
views = true
1
2
3
4
5
6
7
8
#文件位置: ./themes/hugo-theme-stack/i18n/zh-cn.yaml
article:
    ...
    views:
        main:
            other: "浏览次数: "
        unit:
            other: " 次"

最终效果

看着真不错

折腾完毕,博客折腾就暂时告一段落了。
PS. 写了这么久的Python,我都差不多把C++/QT给忘光了,不得不说,Python是真的好用。