强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

06 - HTML 报告

06 - HTML 报告

6.1 概述

GoAccess 生成的 HTML 报告是一个完全自包含的单文件,包含所有 CSS、JavaScript 和数据,无需外部依赖即可在任何现代浏览器中打开。这使得报告的分享和部署极为简便。

HTML 报告的核心特性:

  • 📦 自包含:所有资源内嵌在一个 HTML 文件中
  • 📊 交互式:支持排序、分页、搜索、面板切换
  • 📱 响应式:适配桌面和移动设备
  • 🎨 可定制:支持主题、配色、布局调整
  • 📤 可分享:通过文件、链接或嵌入方式分发

6.2 生成基本报告

# 最基本的报告生成
goaccess /var/log/nginx/access.log \
  --log-format=COMBINED \
  -o report.html

# 带标题的报告
goaccess /var/log/nginx/access.log \
  --log-format=COMBINED \
  -o report.html \
  --html-title="网站访问分析报告"

# 指定报告标题
goaccess /var/log/nginx/access.log \
  --log-format=COMBINED \
  -o report.html \
  --html-title="2026年5月报告" \
  --html-report-title="详细统计"

6.3 自定义样式与主题

6.3.1 使用内置主题

GoAccess 提供多种内置主题,通过 --html-prefs 参数设置:

# Bright 主题(亮色)
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"bright"}'

# Dark 主题(暗色)
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"darkGreen"}'

# Mono Green
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"monoGreen"}'

# Mono Blue
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"monoBlue"}'

# Mono Pink
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"monoPink"}'

# Monokai(暗色经典)
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"monokai"}'

# Dracula
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"dracula"}'

# Classic(经典)
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"classic"}'

6.3.2 主题效果对比

主题背景色适用场景
bright白色打印、正式报告
darkGreen深绿长时间阅读、暗色环境
monoGreen黑底绿字极客风格、终端爱好者
monoBlue黑底蓝字专业风格
monoPink黑底粉字个性化
monokai深灰编辑器用户熟悉
dracula深紫暗色主题爱好者
classic浅灰传统风格

6.3.3 完整 HTML 偏好设置

{
  "theme": "bright",
  "perPage": 20,
  "layout": "horizontal",
  "showIndianMap": false,
  "visitors": {
    "sortBy": "hits",
    "sortOrder": "desc"
  },
  "requests": {
    "sortBy": "hits",
    "sortOrder": "desc"
  },
  "status_codes": {
    "sortBy": "hits",
    "sortOrder": "desc"
  }
}
goaccess access.log --log-format=COMBINED -o report.html \
  --html-prefs='{"theme":"darkGreen","perPage":50,"layout":"horizontal"}'

6.3.4 自定义 CSS 样式

由于 HTML 报告是自包含的,你可以在生成后注入自定义 CSS:

#!/bin/bash
# generate_report.sh — 生成带自定义样式的报告

# 生成报告
goaccess /var/log/nginx/access.log \
  --log-format=COMBINED \
  -o /tmp/goaccess_raw.html

# 注入自定义 CSS
cat > /tmp/custom_style.css << 'CSS'
/* 自定义样式覆盖 */
body {
  font-family: 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
}

#dashboard .grid .item {
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

table th {
  background-color: #2c3e50 !important;
  color: white;
}
CSS

# 合并 CSS 到 HTML
sed -i '/<\/style>/i\
/* Custom CSS */' /tmp/goaccess_raw.html
sed -i "/<\/style>/r /tmp/custom_style.css" /tmp/goaccess_raw.html

# 添加中文字体支持
sed -i 's|<head>|<head><link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700\&display=swap" rel="stylesheet">|' /tmp/goaccess_raw.html

mv /tmp/goaccess_raw.html /var/www/html/report.html
echo "报告已生成: /var/www/html/report.html"

6.4 图表配置

GoAccess 的 HTML 报告包含多种交互式图表:

6.4.1 内置图表类型

图表类型展示内容面板
面积图 (Area Chart)访问量随时间变化趋势通用仪表盘
折线图 (Line Chart)访客数、带宽趋势通用仪表盘
水平柱状图 (Bar Chart)文件排行、IP 排行请求文件、主机面板
饼图 (Pie Chart)状态码分布、浏览器分布状态码、浏览器面板
数据表格 (Table)详细数据列表所有面板

6.4.2 图表交互

HTML 报告中的图表支持以下交互操作:

  • 鼠标悬停:显示详细数据
  • 点击图例:切换显示/隐藏数据系列
  • 分页浏览:大数据集分页显示
  • 排序:点击表头排序
  • 搜索:在表格中搜索关键词

6.5 报告自动化生成

6.5.1 使用 Cron 定时生成

# 编辑 crontab
crontab -e
# 每天凌晨 1 点生成前一天的日报
0 1 * * * /usr/local/bin/goaccess /var/log/nginx/access.log --log-format=COMBINED -o /var/www/html/daily/$(date +\%Y-\%m-\%d).html --process-and-exit 2>&1

# 每周一凌晨 2 点生成周报(合并上周日志)
0 2 * * 1 /usr/local/bin/cat /var/log/nginx/access.log.1 | /usr/local/bin/goaccess --log-format=COMBINED -o /var/www/html/weekly/$(date +\%Y-\%W).html --process-and-exit 2>&1

# 每月 1 日凌晨 3 点生成月报
0 3 1 * * /usr/local/bin/zcat /var/log/nginx/access.log.*.gz | /usr/local/bin/goaccess --log-format=COMBINED -o /var/www/html/monthly/$(date +\%Y-\%m).html --process-and-exit 2>&1

6.5.2 完整的自动化脚本

#!/bin/bash
# auto_report.sh — GoAccess 自动化报告生成脚本

set -euo pipefail

# ============ 配置 ============
LOG_DIR="/var/log/nginx"
REPORT_DIR="/var/www/html/stats"
GOACCESS="/usr/local/bin/goaccess"
LOG_FORMAT="COMBINED"
DATE=$(date +%Y-%m-%d)
YESTERDAY=$(date -d "yesterday" +%Y-%m-%d)

# ============ 创建目录 ============
mkdir -p "${REPORT_DIR}/daily"
mkdir -p "${REPORT_DIR}/weekly"
mkdir -p "${REPORT_DIR}/monthly"

# ============ 生成日报 ============
echo "[${DATE}] 生成日报..."

# 过滤昨天的日志
awk -v date="$(date -d 'yesterday' +'%d/%b/%Y')" '$0 ~ date' \
  "${LOG_DIR}/access.log" | \
  ${GOACCESS} --log-format="${LOG_FORMAT}" \
  -o "${REPORT_DIR}/daily/${YESTERDAY}.html" \
  --html-title="日报 - ${YESTERDAY}" \
  --process-and-exit - 2>/dev/null

echo "[${DATE}] 日报已生成: ${REPORT_DIR}/daily/${YESTERDAY}.html"

# ============ 生成周报(每周一) ============
DOW=$(date +%u)  # 1=Monday, 7=Sunday
if [ "${DOW}" = "1" ]; then
    WEEK=$(date -d "last week" +%Y-W%V)
    echo "[${DATE}] 生成周报 ${WEEK}..."

    cat "${LOG_DIR}/access.log".1 "${LOG_DIR}/access.log" | \
      ${GOACCESS} --log-format="${LOG_FORMAT}" \
      -o "${REPORT_DIR}/weekly/${WEEK}.html" \
      --html-title="周报 - ${WEEK}" \
      --process-and-exit - 2>/dev/null

    echo "[${DATE}] 周报已生成: ${REPORT_DIR}/weekly/${WEEK}.html"
fi

# ============ 生成月报(每月 1 日) ============
DOM=$(date +%d)
if [ "${DOM}" = "01" ]; then
    MONTH=$(date -d "last month" +%Y-%m)
    echo "[${DATE}] 生成月报 ${MONTH}..."

    zcat "${LOG_DIR}/access.log"*.gz | \
      ${GOACCESS} --log-format="${LOG_FORMAT}" \
      -o "${REPORT_DIR}/monthly/${MONTH}.html" \
      --html-title="月报 - ${MONTH}" \
      --process-and-exit - 2>/dev/null

    echo "[${DATE}] 月报已生成: ${REPORT_DIR}/monthly/${MONTH}.html"
fi

# ============ 清理旧报告 ============
# 保留最近 90 天的日报
find "${REPORT_DIR}/daily" -name "*.html" -mtime +90 -delete 2>/dev/null

echo "[${DATE}] 报告生成完成"

6.5.3 使用 systemd Timer 定时执行

# /etc/systemd/system/goaccess-report.service
[Unit]
Description=GoAccess Daily Report Generator
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/auto_report.sh
User=www-data
Group=www-data
# /etc/systemd/system/goaccess-report.timer
[Unit]
Description=Run GoAccess report daily

[Timer]
OnCalendar=*-*-* 01:00:00
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now goaccess-report.timer

# 查看定时器状态
sudo systemctl list-timers goaccess-report.timer

6.6 报告分享策略

6.6.1 通过 Web 服务器分享

# Nginx 配置 — 统计报告站点
server {
    listen 443 ssl;
    server_name stats.example.com;

    ssl_certificate /etc/ssl/certs/stats.pem;
    ssl_certificate_key /etc/ssl/private/stats.key;

    root /var/www/html/stats;
    index index.html;

    # Basic Auth 保护
    auth_basic "Statistics";
    auth_basic_user_file /etc/nginx/.htpasswd;

    # 目录浏览
    autoindex on;
    autoindex_exact_size off;
    autoindex_localtime on;
}
# 创建 htpasswd 文件
sudo htpasswd -c /etc/nginx/.htpasswd admin

6.6.2 通过邮件发送报告

#!/bin/bash
# email_report.sh — 通过邮件发送 GoAccess 报告

REPORT_PATH="/var/www/html/stats/daily/$(date -d yesterday +%Y-%m-%d).html"
RECIPIENT="[email protected]"
SUBJECT="GoAccess 日报 - $(date -d yesterday +%Y-%m-%d)"

if [ -f "${REPORT_PATH}" ]; then
    mail -s "${SUBJECT}" \
         -a "Content-Type: text/html; charset=UTF-8" \
         "${RECIPIENT}" < "${REPORT_PATH}"
    echo "报告已发送至 ${RECIPIENT}"
else
    echo "报告文件不存在: ${REPORT_PATH}"
    exit 1
fi

6.6.3 嵌入到现有页面

<!-- 将 GoAccess 报告嵌入 iframe -->
<iframe
  src="/stats/daily/2026-05-09.html"
  width="100%"
  height="800px"
  frameborder="0"
  style="border: 1px solid #ddd; border-radius: 4px;">
</iframe>

6.6.4 批量导出历史报告

#!/bin/bash
# batch_export.sh — 批量导出历史日志为 HTML 报告

LOG_DIR="/var/log/nginx"
OUTPUT_DIR="/var/www/html/stats/archive"
mkdir -p "${OUTPUT_DIR}"

# 处理所有压缩的日志文件
for logfile in "${LOG_DIR}"/access.log.*.gz; do
    filename=$(basename "${logfile}" .gz)
    date_part=$(echo "${filename}" | grep -oP '\d{8}' || echo "unknown")
    output="${OUTPUT_DIR}/${date_part}.html"

    if [ ! -f "${output}" ]; then
        echo "处理: ${logfile}${output}"
        zcat "${logfile}" | \
          goaccess --log-format=COMBINED \
          -o "${output}" \
          --html-title="历史报告 - ${date_part}" \
          --process-and-exit - 2>/dev/null
    else
        echo "跳过: ${output} 已存在"
    fi
done

echo "批量导出完成"

6.7 生成 JSON/CSV 报告

6.7.1 JSON 报告

# 生成 JSON 格式报告
goaccess /var/log/nginx/access.log \
  --log-format=COMBINED \
  -o report.json

# 输出到标准输出(便于管道处理)
goaccess /var/log/nginx/access.log \
  --log-format=COMBINED \
  -o - \
  --no-global-config | jq '.general'

6.7.2 CSV 报告

# 生成 CSV 报告
goaccess /var/log/nginx/access.log \
  --log-format=COMBINED \
  -o report.csv

# CSV 报告包含所有面板的数据,用注释分隔各面板

6.7.3 多格式同时输出

# 同时生成 HTML 和 JSON(使用两次调用)
goaccess access.log --log-format=COMBINED -o report.html
goaccess access.log --log-format=COMBINED -o report.json

# 使用持久化避免重复解析
goaccess access.log --log-format=COMBINED -o /tmp/db --persist
goaccess /tmp/db --restore -o report.html
goaccess /tmp/db --restore -o report.json
goaccess /tmp/db --restore -o report.csv

6.8 报告索引页

当有多个报告文件时,可以生成一个索引页方便浏览:

#!/bin/bash
# generate_index.sh — 生成报告索引页

REPORT_DIR="/var/www/html/stats"
INDEX="${REPORT_DIR}/index.html"

cat > "${INDEX}" << 'HTML'
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>网站访问统计报告</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Noto Sans SC', sans-serif; background: #f5f5f5; padding: 20px; }
        .container { max-width: 800px; margin: 0 auto; }
        h1 { color: #2c3e50; margin-bottom: 30px; text-align: center; }
        .section { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .section h2 { color: #34495e; margin-bottom: 15px; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
        .report-list { list-style: none; }
        .report-list li { padding: 8px 0; border-bottom: 1px solid #eee; }
        .report-list li:last-child { border-bottom: none; }
        .report-list a { color: #3498db; text-decoration: none; font-size: 15px; }
        .report-list a:hover { text-decoration: underline; }
        .report-list .date { color: #7f8c8d; font-size: 13px; margin-left: 10px; }
    </style>
</head>
<body>
    <div class="container">
        <h1>📊 网站访问统计报告</h1>

        <div class="section">
            <h2>📋 月报</h2>
            <ul class="report-list" id="monthly">
                <!-- 由脚本动态生成 -->
            </ul>
        </div>

        <div class="section">
            <h2>📅 周报</h2>
            <ul class="report-list" id="weekly">
                <!-- 由脚本动态生成 -->
            </ul>
        </div>

        <div class="section">
            <h2>📆 日报</h2>
            <ul class="report-list" id="daily">
                <!-- 由脚本动态生成 -->
            </ul>
        </div>
    </div>
</body>
</html>
HTML

# 生成日报链接
echo '<script>' >> "${INDEX}"
echo 'const daily = [' >> "${INDEX}"
ls -r "${REPORT_DIR}/daily/"*.html 2>/dev/null | while read f; do
    name=$(basename "${f}" .html)
    echo "  { name: '${name}', path: 'daily/${name}.html' }," >> "${INDEX}"
done
echo '];' >> "${INDEX}"

echo 'const weekly = [' >> "${INDEX}"
ls -r "${REPORT_DIR}/weekly/"*.html 2>/dev/null | while read f; do
    name=$(basename "${f}" .html)
    echo "  { name: '${name}', path: 'weekly/${name}.html' }," >> "${INDEX}"
done
echo '];' >> "${INDEX}"

echo 'const monthly = [' >> "${INDEX}"
ls -r "${REPORT_DIR}/monthly/"*.html 2>/dev/null | while read f; do
    name=$(basename "${f}" .html)
    echo "  { name: '${name}', path: 'monthly/${name}.html' }," >> "${INDEX}"
done
echo '];' >> "${INDEX}"

cat >> "${INDEX}" << 'JS'
function render(list, containerId) {
    const ul = document.getElementById(containerId);
    if (!list.length) {
        ul.innerHTML = '<li style="color:#999">暂无数据</li>';
        return;
    }
    ul.innerHTML = list.map(item =>
        `<li><a href="${item.path}">${item.name}</a></li>`
    ).join('');
}
render(daily, 'daily');
render(weekly, 'weekly');
render(monthly, 'monthly');
</script>
</html>
JS

echo "索引页已生成: ${INDEX}"

6.9 报告数据导出与二次分析

6.9.1 导出为 JSON 后用 Python 分析

# 导出 JSON
goaccess /var/log/nginx/access.log --log-format=COMBINED -o report.json

# 用 Python 分析
python3 << 'PYEOF'
import json

with open('report.json', 'r') as f:
    data = json.load(f)

# 打印概览
g = data['general']
print(f"总请求数: {g['total_requests']}")
print(f"独立访客: {g['unique_visitors']}")
print(f"带宽消耗: {g['bandwidth'] / 1024 / 1024:.1f} MB")

# Top 10 请求文件
print("\nTop 10 请求文件:")
for item in data['requests']['data'][:10]:
    print(f"  {item['data']}: {item['hits']['count']} 次")

# 状态码分布
print("\n状态码分布:")
for item in data['status_codes']['data']:
    print(f"  {item['data']}: {item['hits']['count']} ({item['hits']['percent']}%)")
PYEOF

6.9.2 导入到数据库

# 将 JSON 数据导入 SQLite
goaccess /var/log/nginx/access.log --log-format=COMBINED -o report.json

python3 << 'PYEOF'
import json
import sqlite3

with open('report.json', 'r') as f:
    data = json.load(f)

conn = sqlite3.connect('goaccess.db')
c = conn.cursor()

# 创建表
c.execute('''CREATE TABLE IF NOT EXISTS visitors (
    ip TEXT, hits INTEGER, visitors INTEGER,
    hit_pct REAL, visitor_pct REAL
)''')

c.execute('''CREATE TABLE IF NOT EXISTS requests (
    file TEXT, hits INTEGER, visitors INTEGER,
    hit_pct REAL, visitor_pct REAL, bandwidth TEXT
)''')

c.execute('''CREATE TABLE IF NOT EXISTS status_codes (
    code TEXT, hits INTEGER, percent REAL
)''')

# 插入访客数据
for item in data['visitors']['data']:
    c.execute('INSERT INTO visitors VALUES (?,?,?,?,?)',
              (item['data'], item['hits']['count'],
               item['visitors']['count'],
               item['hits']['percent'],
               item['visitors']['percent']))

# 插入请求数据
for item in data['requests']['data']:
    c.execute('INSERT INTO requests VALUES (?,?,?,?,?,?)',
              (item['data'], item['hits']['count'],
               item['visitors']['count'],
               item['hits']['percent'],
               item['visitors']['percent'],
               item.get('bandwidth', {}).get('count', '0')))

# 插入状态码
for item in data['status_codes']['data']:
    c.execute('INSERT INTO status_codes VALUES (?,?,?)',
              (item['data'], item['hits']['count'],
               item['hits']['percent']))

conn.commit()
conn.close()
print("数据已导入 SQLite: goaccess.db")
PYEOF

6.10 小结

功能命令/方法
基本报告-o report.html
自定义主题--html-prefs='{"theme":"bright"}'
自定义标题--html-title="标题"
定时生成Cron / systemd timer
安全分享Nginx + Basic Auth
批量导出遍历日志文件脚本
二次分析JSON 导出 + Python/jq

下一章

下一章将详细介绍 GoAccess 的过滤与排除功能,包括日期范围过滤、状态码过滤、IP 排除和请求模式排除。

07 - 过滤与排除


扩展阅读