Nginx 日志分析与可视化实战:从 0 搭建访问统计系统
摘要: 本文详细介绍如何从零搭建 Nginx 日志分析系统,实现 PV/UV/IP 统计、24 小时趋势、来源渠道分析、IP 归属地查询等功能。基于 Python + MySQL + Flask + ECharts,提供完整代码和实测数据。
⚠️ 安全提示: 文中所有 IP 地址、密码等敏感信息均已脱敏处理,请勿直接使用示例配置。
一、前言
为什么需要日志分析?
运营技术博客三个月,累计 4 万 + 次浏览,但我一直不知道:
- 每天有多少人访问?
- 用户都来自哪里?
- 哪些文章最受欢迎?
- 访问高峰在什么时段?
Google Analytics 虽好,但:
- 国内加载慢
- 隐私问题
- 数据不在自己手里
决定自建日志分析系统,需求:
- 实时统计 PV/UV/IP
- 24 小时/7 天趋势图
- 来源渠道分析 (百度/Google/直接访问)
- IP 归属地查询
- 轻量级,不占用太多资源
二、Nginx 日志格式解析
默认日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /www/wwwlogs/your_domain.log main;
⚠️ 注意: 日志文件路径根据你的实际域名修改,不要暴露服务器 IP!
日志示例
170.106.xxx.xxx - - [13/Apr/2026:14:04:48 +0800] "GET /2026/04/12/article/ HTTP/1.1" 200 12345 "-" "Mozilla/5.0"
123.160.xxx.xxx - - [13/Apr/2026:14:04:51 +0800] "GET / HTTP/1.1" 200 8901 "-" "curl/7.64.1"
字段说明
| 字段 | 说明 | 示例 |
|---|---|---|
$remote_addr | 客户端 IP | 170.106.xxx.xxx |
$time_local | 访问时间 | 13/Apr/2026:14:04:48 +0800 |
$request | 请求方法和 URL | GET /article/ |
$status | HTTP 状态码 | 200, 404, 500 |
$http_referer | 来源页面 | https://www.baidu.com/ |
$http_user_agent | 用户代理 | Mozilla/5.0... |
三、日志分析方案设计
技术栈选择
| 组件 | 选型 | 理由 |
|---|---|---|
| 数据库 | MySQL 8.0 | 成熟稳定,查询效率高 |
| 分析脚本 | Python 3 | 正则处理方便,定时任务简单 |
| 后台框架 | Flask | 轻量级,开发快 |
| 图表库 | ECharts 5 | 百度开源,文档完善,国内访问快 |
| 定时任务 | cron | 系统自带,可靠 |
数据库设计
visits 表 (原始访问记录)
CREATE TABLE visits (
id INT PRIMARY KEY AUTO_INCREMENT,
ip VARCHAR(45),
url VARCHAR(499),
status INT DEFAULT 200,
referer VARCHAR(499),
user_agent VARCHAR(499),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ip (ip),
INDEX idx_created_at (created_at)
);
daily_stats 表 (每日统计)
CREATE TABLE daily_stats (
date DATE PRIMARY KEY,
pv INT DEFAULT 0,
uv INT DEFAULT 0,
ip_count INT DEFAULT 0
);
hourly_stats 表 (每小时统计)
CREATE TABLE hourly_stats (
date_hour DATETIME PRIMARY KEY,
pv INT DEFAULT 0,
uv INT DEFAULT 0
);
四、环境搭建
1. 安装 MySQL
# CentOS 7
yum install mysql-server -y
systemctl start mysqld
systemctl enable mysqld
# 初始化数据库
mysql -u root -p
CREATE DATABASE blog_stats CHARACTER SET utf8mb4;
CREATE USER 'blog_stats'@'localhost' IDENTIFIED BY 'YourStrongPassword2026!';
GRANT ALL PRIVILEGES ON blog_stats.* TO 'blog_stats'@'localhost';
FLUSH PRIVILEGES;
⚠️ 安全提示:
- 密码使用强密码 (大小写 + 数字 + 特殊字符)
- 不要使用文中示例密码!
- 建议从环境变量读取配置
2. 安装 Python 依赖
pip3 install mysql-connector-python flask --break-system-packages
3. 创建项目目录
mkdir -p /opt/blog-stats/{admin,scripts}
chmod 755 /opt/blog-stats
五、日志分析脚本实现
核心脚本:analyze-logs-local.py
#!/usr/bin/env python3
# Nginx 日志分析脚本
import re, mysql.connector
from datetime import datetime
from collections import defaultdict
import os
# 从环境变量读取配置,避免硬编码
DB = {
'host': os.getenv('DB_HOST', 'localhost'),
'user': os.getenv('DB_USER', 'blog_stats'),
'password': os.getenv('DB_PASSWORD'),
'database': 'blog_stats'
}
LOG = os.getenv('NGINX_LOG_PATH', '/www/wwwlogs/your_domain.log')
MARKER = '/opt/blog-stats/.last_pos'
# Nginx 日志正则解析
PAT = re.compile(
r'(?P<ip>[\d.]+) .* \[(?P<t>[^\]]+)\] "\w+ (?P<url>\S+) .* '
r'(?P<status>\d+) .* "(?P<ref>[^"]*)" "(?P<ua>[^"]*)"'
)
def get_pos():
"""获取上次读取位置"""
try:
return int(open(MARKER).read().strip())
except:
return 0
def save_pos(p):
"""保存当前读取位置"""
open(MARKER, 'w').write(str(p))
def main():
conn = mysql.connector.connect(**DB)
cur = conn.cursor()
# 读取日志文件
f = open(LOG)
f.seek(get_pos())
visits = []
hourly = defaultdict(lambda: {'pv': 0, 'ips': set()})
daily = defaultdict(lambda: {'pv': 0, 'ips': set()})
for line in f:
m = PAT.match(line)
if not m:
continue
d = m.groupdict()
# 解析时间
try:
t = datetime.strptime(d['t'].split()[0], '%d/%b/%Y:%H:%M:%S')
except:
t = datetime.now()
# 字段截断,防止超长
url = d['url'][:499] if len(d['url']) > 499 else d['url']
ref = (d['ref'][:499] if d['ref'] != '-' else None)
ua = d['ua'][:499] if len(d['ua']) > 499 else d['ua']
status = int(d['status'])
# 收集访问记录
visits.append((d['ip'], url, status, ref, ua, t))
# 小时统计
hk = t.replace(minute=0, second=0)
hourly[hk]['pv'] += 1
hourly[hk]['ips'].add(d['ip'])
# 日统计
dk = t.date()
daily[dk]['pv'] += 1
daily[dk]['ips'].add(d['ip'])
# 批量插入访问记录
if visits:
cur.executemany(
'INSERT INTO visits(ip,url,status,referer,user_agent,created_at) '
'VALUES(%s,%s,%s,%s,%s,%s)',
visits
)
# 更新小时统计
for k, s in hourly.items():
cur.execute(
'INSERT INTO hourly_stats(date_hour,pv,uv) VALUES(%s,%s,%s) '
'ON DUPLICATE KEY UPDATE pv=pv+VALUES(pv),uv=VALUES(uv)',
(k, s['pv'], len(s['ips']))
)
# 更新日统计
for k, s in daily.items():
cur.execute(
'INSERT INTO daily_stats(date,pv,uv,ip_count) VALUES(%s,%s,%s,%s) '
'ON DUPLICATE KEY UPDATE pv=VALUES(pv),uv=VALUES(uv),ip_count=VALUES(ip_count)',
(k, s['pv'], len(s['ips']), len(s['ips']))
)
conn.commit()
save_pos(f.tell())
print(f'✅ {len(visits)} records')
cur.close()
conn.close()
if __name__ == '__main__':
main()
六、安全加固建议
1. 后台访问限制
# Flask 后台 IP 限制
from functools import wraps
from flask import request, abort
ALLOWED_IPS = ['你的管理 IP'] # 只允许特定 IP 访问
def ip_whitelist(f):
@wraps(f)
def decorated(*args, **kwargs):
if request.remote_addr not in ALLOWED_IPS:
abort(403)
return f(*args, **kwargs)
return decorated
2. 数据库密码保护
错误做法 ❌:
DB = {'password': '明文密码'}
正确做法 ✅:
# 方式 1: 环境变量
DB = {'password': os.getenv('DB_PASSWORD')}
# 方式 2: 配置文件 (chmod 600)
import configparser
config = configparser.ConfigParser()
config.read('/etc/blog-stats/config.ini')
DB = {'password': config['database']['password']}
3. Nginx 后台访问控制
# 限制统计后台只能内网访问
location / {
allow 127.0.0.1;
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
proxy_pass http://127.0.0.1:8081;
}
4. 日志文件保护
# 设置日志文件权限
chmod 640 /www/wwwlogs/your_domain.log
chown nginx:nginx /www/wwwlogs/your_domain.log
# 定期切割日志
logrotate /etc/logrotate.d/nginx
七、实测数据 (脱敏)
系统运行效果
部署时间: 2026-04-13
日志总量: 4 万 + 条访问记录
今日数据 (2026-04-13):
- PV: 585
- UV: 223
- IP 数:223
24 小时趋势
| 时段 | PV | UV |
|---|---|---|
| 14:00 | 20 | 15 |
| 15:00 | 71 | 4 |
| 16:00 | 23 | 13 |
| 17:00 | 27 | 15 |
| 18:00 | 30 | 22 |
| 19:00 | 45 | 21 |
| 20:00 | 30 | 14 |
| 21:00 | 10 | 9 |
| 22:00 | 11 | 11 |
来源渠道
| 来源 | 占比 |
|---|---|
| 直接访问 | 65% |
| 百度搜索 | 20% |
| Google 搜索 | 10% |
| 其他来源 | 5% |
八、总结
已完成功能
✅ PV/UV/IP 实时统计
✅ 24 小时趋势图 (带数据标签)
✅ 7 天趋势图
✅ 来源渠道分析 (饼图)
✅ IP 归属地查询
✅ 登录认证
✅ 定时任务自动分析
✅ 安全加固 (IP 限制/密码保护)
资源占用
| 指标 | 数值 |
|---|---|
| 数据库大小 | ~50MB |
| 分析脚本内存 | <50MB |
| Flask 后台内存 | <30MB |
| CPU 占用 | <1% |
安全提示汇总
⚠️ 重要: 部署时请注意以下安全事项:
- 修改所有默认密码 - 数据库/后台登录
- 限制后台访问 IP - 只允许管理 IP 访问
- 使用环境变量 - 避免硬编码敏感信息
- 保护日志文件 - 设置正确权限
- 定期备份数据 - 防止数据丢失
- 监控异常访问 - 设置告警通知
作者: 寒呀
发布日期: 2026-04-14
博客: 运维笔记
如果本文对你有帮助,欢迎分享转发!
安全提醒: 生产环境请务必做好安全加固!