← 返回首页

Nginx 日志分析与可视化实战

Nginx 日志分析与可视化实战:从 0 搭建访问统计系统

摘要: 本文详细介绍如何从零搭建 Nginx 日志分析系统,实现 PV/UV/IP 统计、24 小时趋势、来源渠道分析、IP 归属地查询等功能。基于 Python + MySQL + Flask + ECharts,提供完整代码和实测数据。

⚠️ 安全提示: 文中所有 IP 地址、密码等敏感信息均已脱敏处理,请勿直接使用示例配置。


一、前言

为什么需要日志分析?

运营技术博客三个月,累计 4 万 + 次浏览,但我一直不知道:

  • 每天有多少人访问?
  • 用户都来自哪里?
  • 哪些文章最受欢迎?
  • 访问高峰在什么时段?

Google Analytics 虽好,但:

  • 国内加载慢
  • 隐私问题
  • 数据不在自己手里

决定自建日志分析系统,需求:

  1. 实时统计 PV/UV/IP
  2. 24 小时/7 天趋势图
  3. 来源渠道分析 (百度/Google/直接访问)
  4. IP 归属地查询
  5. 轻量级,不占用太多资源

二、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客户端 IP170.106.xxx.xxx
$time_local访问时间13/Apr/2026:14:04:48 +0800
$request请求方法和 URLGET /article/
$statusHTTP 状态码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 小时趋势

时段PVUV
14:002015
15:00714
16:002313
17:002715
18:003022
19:004521
20:003014
21:00109
22:001111

来源渠道

来源占比
直接访问65%
百度搜索20%
Google 搜索10%
其他来源5%

八、总结

已完成功能

✅ PV/UV/IP 实时统计
✅ 24 小时趋势图 (带数据标签)
✅ 7 天趋势图
✅ 来源渠道分析 (饼图)
✅ IP 归属地查询
✅ 登录认证
✅ 定时任务自动分析
✅ 安全加固 (IP 限制/密码保护)

资源占用

指标数值
数据库大小~50MB
分析脚本内存<50MB
Flask 后台内存<30MB
CPU 占用<1%

安全提示汇总

⚠️ 重要: 部署时请注意以下安全事项:

  1. 修改所有默认密码 - 数据库/后台登录
  2. 限制后台访问 IP - 只允许管理 IP 访问
  3. 使用环境变量 - 避免硬编码敏感信息
  4. 保护日志文件 - 设置正确权限
  5. 定期备份数据 - 防止数据丢失
  6. 监控异常访问 - 设置告警通知

作者: 寒呀
发布日期: 2026-04-14
博客: 运维笔记

如果本文对你有帮助,欢迎分享转发!

安全提醒: 生产环境请务必做好安全加固!