commit 13a259b0f8e52cf64fdebe854b6315df71d81bdb Author: bizwings Date: Fri Jul 3 16:39:21 2026 +0800 chore: initial commit — existing lottoData codebase Files: - lottery.py (1189 lines) — DoubleColorBallGenerator core engine - fetch_data.py (131 lines) — history data fetcher from 55128.cn - web_executor.py (216 lines) — data fetch Web console (Flask :5000) - app.py (505 lines) — number generation Web service (Flask :8085) - index.html (1171 lines) — frontend SPA - web_console.html (323 lines) — fetch console frontend - deploy/ — systemd service + cron script + logs BIZ-74 architecture review baseline diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e559128 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +.DS_Store +venv/ +.venv/ +LottoSpider/ +*.log diff --git a/app.py b/app.py new file mode 100644 index 0000000..8226c27 --- /dev/null +++ b/app.py @@ -0,0 +1,505 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +双色球 Web UI 服务 +集成号码生成、历史数据查看、生成记录管理 +监听 0.0.0.0,支持 PC 端和移动端响应式访问 +""" + +import os +import sys +import json +import uuid +import shutil +import traceback +from datetime import datetime +from flask import Flask, send_from_directory, jsonify, request, send_file, abort +from functools import wraps + +# 将项目目录加入路径,以便导入 lottery.py +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, BASE_DIR) + +# 导入号码生成器 +from lottery import DoubleColorBallGenerator + +app = Flask(__name__) + +# ============================================================ +# 配置 +# ============================================================ +CONFIG = { + 'host': '0.0.0.0', + 'port': 8085, + 'history_file': os.path.join(BASE_DIR, '双色球历史数据.xlsx'), + 'lottery_output_dir': os.path.join(BASE_DIR, 'lottery'), + 'records_file': os.path.join(BASE_DIR, '.generation_records.json'), + 'api_token': 'lotto2026', + 'auth_enabled': False, + 'max_tickets': 1000, + 'default_tickets': 10, +} + +# ============================================================ +# 生成记录管理 +# ============================================================ +def load_records(): + """加载生成记录""" + if os.path.exists(CONFIG['records_file']): + try: + with open(CONFIG['records_file'], 'r', encoding='utf-8') as f: + return json.load(f) + except: + return [] + return [] + +def save_records(records): + """保存生成记录""" + os.makedirs(os.path.dirname(CONFIG['records_file']), exist_ok=True) + with open(CONFIG['records_file'], 'w', encoding='utf-8') as f: + json.dump(records, f, ensure_ascii=False, indent=2) + +def add_record(strategy, num_tickets, filename): + """添加一条生成记录""" + records = load_records() + records.insert(0, { + 'id': str(uuid.uuid4())[:8], + 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'strategy': '高级策略' if strategy == 'advanced' else '基础策略', + 'num_tickets': num_tickets, + 'filename': filename, + 'filesize': os.path.getsize(os.path.join(BASE_DIR, filename)) if os.path.exists(os.path.join(BASE_DIR, filename)) else 0 + }) + save_records(records) + return records[0] + +# ============================================================ +# 认证装饰器(可选) +# ============================================================ +def require_auth(f): + @wraps(f) + def decorated(*args, **kwargs): + if CONFIG['auth_enabled']: + token = request.headers.get('Authorization', '').replace('Bearer ', '') + if token != CONFIG['api_token']: + return jsonify({'success': False, 'error': '未授权访问'}), 401 + return f(*args, **kwargs) + return decorated + +# ============================================================ +# API:号码生成 +# ============================================================ +@app.route('/api/generate', methods=['POST']) +@require_auth +def api_generate(): + """生成双色球号码""" + try: + data = request.get_json(force=True, silent=True) or {} + num_tickets = int(data.get('num_tickets', CONFIG['default_tickets'])) + strategy = data.get('strategy', 'advanced') + + # 参数校验 + if num_tickets < 1 or num_tickets > CONFIG['max_tickets']: + return jsonify({ + 'success': False, + 'error': f'注数必须在 1-{CONFIG["max_tickets"]} 之间' + }), 400 + + if strategy not in ('advanced', 'basic'): + return jsonify({'success': False, 'error': '策略参数无效,请使用 advanced 或 basic'}), 400 + + # 初始化生成器 + generator = DoubleColorBallGenerator(CONFIG['history_file']) + if not generator.load_history_data(): + return jsonify({'success': False, 'error': '无法加载历史数据'}), 500 + + # 生成号码 + tickets_df = generator.generate_multiple_tickets(num_tickets, strategy) + if tickets_df.empty: + return jsonify({'success': False, 'error': '号码生成失败'}), 500 + + # 保存到 Excel + filename = generator.save_to_excel(tickets_df, num_tickets, strategy) + if not filename: + return jsonify({'success': False, 'error': '文件保存失败'}), 500 + + # 获取相对路径 + rel_path = os.path.relpath(filename, BASE_DIR) + + # 添加生成记录 + record = add_record(strategy, num_tickets, rel_path) + + # 构建返回数据 + tickets_data = [] + for _, row in tickets_df.iterrows(): + tickets_data.append({ + 'index': int(row['序号']), + 'reds': [int(row[f'红球{i}']) for i in range(1, 7)], + 'blue': int(row['蓝球']), + 'sum_value': int(row['和值']), + 'odd_even': row['奇偶比'], + 'size_ratio': row['大小比'], + 'span': int(row['跨度']) + }) + + # 获取统计信息 + stats = get_statistics_data(generator) + + return jsonify({ + 'success': True, + 'data': { + 'tickets': tickets_data[:50], # 前端最多展示 50 注 + 'total': len(tickets_data), + 'filename': rel_path, + 'download_url': f'/api/download/{rel_path}', + 'record': record, + 'statistics': stats + } + }) + + except Exception as e: + return jsonify({'success': False, 'error': f'生成失败: {str(e)}'}), 500 + + +def get_statistics_data(generator=None): + """获取统计数据""" + import pandas as pd + import re + from collections import Counter + + if not os.path.exists(CONFIG['history_file']): + return {} + + # 直接解析 Excel,跳过描述行 + df = pd.read_excel(CONFIG['history_file'], header=None) + data_df = df.iloc[1:].copy() + data_df.columns = ['开奖日期', '期号', '红球', '开机号', '和值特征', '奇偶形态', '大小比', '奇偶形态2', '跨度', '其他'] + + # 解析红球和蓝球 + red_ball_counts = Counter() + blue_ball_counts = Counter() + sum_values = [] + span_values = [] + + for _, row in data_df.iterrows(): + s = str(row['红球']).strip() + if len(s) >= 14: + reds = [int(s[i:i+2]) for i in range(0, 12, 2)] + blue = int(s[12:14]) + if all(1 <= r <= 33 for r in reds) and 1 <= blue <= 16: + red_ball_counts.update(reds) + blue_ball_counts[blue] += 1 + sum_values.append(sum(reds)) + span_values.append(max(reds) - min(reds)) + + stats = {} + + if red_ball_counts: + sorted_reds = sorted(red_ball_counts.items(), key=lambda x: x[1], reverse=True) + stats['hot_reds'] = [x[0] for x in sorted_reds[:15]] + stats['cold_reds'] = [x[0] for x in sorted_reds[-15:]] + + if blue_ball_counts: + sorted_blues = sorted(blue_ball_counts.items(), key=lambda x: x[1], reverse=True) + stats['hot_blues'] = [x[0] for x in sorted_blues[:8]] + + # 奇偶比统计 + odd_even_ratios = Counter() + size_ratios = Counter() + for _, row in data_df.iterrows(): + oe = str(row['奇偶形态']).strip() + sz = str(row['大小比']).strip() + if oe and oe != 'nan': + odd_even_ratios[oe] += 1 + if sz and sz != 'nan': + size_ratios[sz] += 1 + + if odd_even_ratios: + stats['common_odd_even'] = max(odd_even_ratios, key=odd_even_ratios.get) + + if size_ratios: + stats['common_size_ratio'] = max(size_ratios, key=size_ratios.get) + + # 和值 + if sum_values: + import numpy as np + arr = np.array(sum_values) + stats['sum_range'] = { + 'min': int(arr.min()), + 'max': int(arr.max()), + 'mean': float(arr.mean()), + 'std': float(arr.std()) + } + + # 跨度 + if span_values: + import numpy as np + arr = np.array(span_values) + stats['span_range'] = { + 'min': int(arr.min()), + 'max': int(arr.max()), + 'mean': float(arr.mean()), + 'std': float(arr.std()) + } + + stats['history_count'] = len(data_df) + + return stats + + +# ============================================================ +# API:获取统计数据 +# ============================================================ +@app.route('/api/statistics') +@require_auth +def api_statistics(): + """获取统计数据""" + try: + stats = get_statistics_data() + return jsonify({'success': True, 'data': stats}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +# ============================================================ +# API:获取生成记录 +# ============================================================ +@app.route('/api/records') +@require_auth +def api_records(): + """获取生成记录列表""" + try: + page = int(request.args.get('page', 1)) + page_size = int(request.args.get('page_size', 20)) + records = load_records() + + total = len(records) + start = (page - 1) * page_size + end = start + page_size + page_records = records[start:end] + + return jsonify({ + 'success': True, + 'data': { + 'records': page_records, + 'total': total, + 'page': page, + 'page_size': page_size + } + }) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +# ============================================================ +# API:删除生成记录 +# ============================================================ +@app.route('/api/records/', methods=['DELETE']) +@require_auth +def api_delete_record(record_id): + """删除生成记录""" + try: + records = load_records() + target = None + for r in records: + if r['id'] == record_id: + target = r + break + + if not target: + return jsonify({'success': False, 'error': '记录不存在'}), 404 + + # 删除文件 + filepath = os.path.join(BASE_DIR, target['filename']) + if os.path.exists(filepath): + os.remove(filepath) + + # 删除记录 + records = [r for r in records if r['id'] != record_id] + save_records(records) + + return jsonify({'success': True, 'message': '记录已删除'}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +# ============================================================ +# API:文件下载 +# ============================================================ +@app.route('/api/download/') +@require_auth +def api_download(filepath): + """下载文件""" + try: + # 安全检查:防止目录遍历 + safe_path = os.path.normpath(filepath) + if safe_path.startswith('..') or safe_path.startswith('/'): + abort(403) + + full_path = os.path.join(BASE_DIR, safe_path) + if not os.path.exists(full_path): + abort(404) + + return send_file(full_path, as_attachment=True) + except Exception: + abort(404) + + +# ============================================================ +# API:历史数据查看 +# ============================================================ +@app.route('/api/history') +@require_auth +def api_history(): + """获取双色球历史开奖数据""" + try: + page = int(request.args.get('page', 1)) + page_size = int(request.args.get('page_size', 20)) + search = request.args.get('search', '').strip() + + # 读取历史数据 + if not os.path.exists(CONFIG['history_file']): + return jsonify({'success': False, 'error': '历史数据文件不存在'}), 404 + + import pandas as pd + import re + df = pd.read_excel(CONFIG['history_file'], header=None) + + # 第一行是描述行,跳过 + data_df = df.iloc[1:].copy() + data_df.columns = ['开奖日期', '期号', '红球', '开机号', '和值特征', '奇偶形态', '大小比', '奇偶形态2', '跨度', '其他'] + data_df = data_df.reset_index(drop=True) + + # 解析红球(红球列是6个红球+蓝球的拼接字符串,如 '09101316192108') + def parse_red_balls(val): + s = str(val).strip() + if len(s) >= 12: + return [int(s[i:i+2]) for i in range(0, 12, 2)] + return [] + + def parse_blue_ball(val): + s = str(val).strip() + if len(s) >= 14: + return int(s[12:14]) + return None + + data_df['红球列表'] = data_df['红球'].apply(parse_red_balls) + data_df['蓝球'] = data_df['红球'].apply(parse_blue_ball) + + # 搜索过滤 + if search: + mask = data_df.astype(str).apply( + lambda row: row.astype(str).str.contains(search, na=False).any(), axis=1 + ) + data_df = data_df[mask] + + total = len(data_df) + + # 分页 + start = (page - 1) * page_size + end = start + page_size + page_df = data_df.iloc[start:end] + + # 转换为 JSON + records = [] + for _, row in page_df.iterrows(): + reds = row['红球列表'] + record = { + '开奖日期': str(row['开奖日期']), + '期号': str(row['期号']), + '红球': reds if len(reds) == 6 else [], + '蓝球': row['蓝球'], + '开机号': str(row['开机号']), + '和值': str(row['和值特征']), + '奇偶形态': str(row['奇偶形态']), + '大小比': str(row['大小比']), + '跨度': str(row['跨度']), + } + records.append(record) + + return jsonify({ + 'success': True, + 'data': { + 'records': records, + 'total': total, + 'page': page, + 'page_size': page_size, + 'columns': ['开奖日期', '期号', '红球', '蓝球', '开机号', '和值', '大小比', '跨度'] + } + }) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +# ============================================================ +# API:系统状态 +# ============================================================ +@app.route('/api/status') +def api_status(): + """获取系统状态""" + try: + history_exists = os.path.exists(CONFIG['history_file']) + history_size = os.path.getsize(CONFIG['history_file']) if history_exists else 0 + lottery_files = [] + if os.path.exists(CONFIG['lottery_output_dir']): + lottery_files = [f for f in os.listdir(CONFIG['lottery_output_dir']) if f.endswith('.xlsx')] + + records = load_records() + + return jsonify({ + 'success': True, + 'data': { + 'server_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'history_exists': history_exists, + 'history_size': history_size, + 'total_generations': len(records), + 'total_lottery_files': len(lottery_files), + 'config': { + 'port': CONFIG['port'], + 'auth_enabled': CONFIG['auth_enabled'], + 'max_tickets': CONFIG['max_tickets'] + } + } + }) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +# ============================================================ +# 前端页面 +# ============================================================ +@app.route('/') +def index(): + """首页 - 双色球 Web UI""" + return send_from_directory(BASE_DIR, 'index.html') + + +@app.route('/api/config') +def api_config(): + """获取前端配置""" + return jsonify({ + 'success': True, + 'data': { + 'max_tickets': CONFIG['max_tickets'], + 'default_tickets': CONFIG['default_tickets'], + 'auth_enabled': CONFIG['auth_enabled'] + } + }) + + +# ============================================================ +# 启动服务 +# ============================================================ +if __name__ == '__main__': + print('=' * 60) + print('🎯 双色球 Web UI 服务') + print('=' * 60) + print(f'\n📂 项目路径: {BASE_DIR}') + print(f'📁 历史数据: {CONFIG["history_file"]}') + print(f'📁 生成目录: {CONFIG["lottery_output_dir"]}') + print(f'\n🌐 服务地址: http://{CONFIG["host"]}:{CONFIG["port"]}') + print(f' 局域网访问: http://<本机IP>:{CONFIG["port"]}') + print(f'\n✅ 服务就绪!') + print('=' * 60) + + app.run(host=CONFIG['host'], port=CONFIG['port'], debug=False, threaded=True) diff --git a/deploy/DEPLOY.md b/deploy/DEPLOY.md new file mode 100644 index 0000000..3f15f4f --- /dev/null +++ b/deploy/DEPLOY.md @@ -0,0 +1,178 @@ +# 双色球系统部署文档 + +## 部署信息 + +| 项目 | 值 | +|------|-----| +| 项目名称 | 双色球自动化系统 | +| 部署时间 | 2026-06-29 | +| 部署人员 | 严维序 (opengineer) | +| 服务地址 | http://192.168.1.99:5000 | +| 宿主服务器 | Ubuntu-OpenClaw (192.168.1.99) | + +--- + +## 一、项目结构 + +``` +/home/vincent/Studio/lottoData/ +├── venv/ # Python 虚拟环境 +├── web_executor.py # Flask Web 服务 (监听 0.0.0.0:5000) +├── fetch_data.py # 数据抓取脚本 +├── lottery.py # 双色球号码生成器 +├── web_console.html # Web 控制台页面 +├── LottoSpider/ # 爬虫模块 +├── lottery/ # 彩票模块 +├── docs/ # 文档 +├── 双色球历史数据.xlsx # 历史数据文件 +└── deploy/ # 部署文件 + ├── DEPLOY.md # 本文档 + ├── lotto-web.service # systemd 服务文件 + ├── fetch_daily.sh # 每日抓取脚本 + ├── cron.log # Cron 执行日志 + └── fetch_YYYYMMDD.log # 每日抓取详细日志 +``` + +--- + +## 二、依赖清单 + +| 包 | 版本 | 用途 | +|----|------|------| +| Flask | 3.1.3 | Web 服务框架 | +| pandas | 3.0.4 | 数据处理 | +| openpyxl | 3.1.5 | Excel 读写 | +| requests | 2.34.2 | HTTP 请求 | +| beautifulsoup4 | 4.15.0 | HTML 解析 | + +安装命令: +```bash +python3 -m venv venv +./venv/bin/pip install flask pandas openpyxl requests beautifulsoup4 +``` + +--- + +## 三、systemd 服务配置 + +### 服务文件 + +`/etc/systemd/system/lotto-web.service` + +```ini +[Unit] +Description=双色球数据抓取 Web 服务 +After=network.target + +[Service] +Type=simple +User=vincent +WorkingDirectory=/home/vincent/Studio/lottoData +ExecStart=/home/vincent/Studio/lottoData/venv/bin/python3 /home/vincent/Studio/lottoData/web_executor.py +ExecStartPre=/home/vincent/Studio/lottoData/venv/bin/python3 -c "import flask; import pandas; import openpyxl; import requests; import bs4" +Restart=on-failure +RestartSec=5 +KillMode=control-group + +[Install] +WantedBy=multi-user.target +``` + +### 管理命令 + +```bash +# 安装/启用 +sudo cp deploy/lotto-web.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable lotto-web +sudo systemctl start lotto-web + +# 日常管理 +sudo systemctl status lotto-web # 查看状态 +sudo systemctl restart lotto-web # 重启 +sudo systemctl stop lotto-web # 停止 +sudo journalctl -u lotto-web -f # 查看实时日志 +``` + +--- + +## 四、Cron 定时任务 + +### Cron 配置 + +``` +30 2 * * * /home/vincent/Studio/lottoData/deploy/fetch_daily.sh >> /home/vincent/Studio/lottoData/deploy/cron.log 2>&1 +``` + +每天凌晨 2:30 自动抓取双色球历史数据。 + +### 手动执行 + +```bash +/home/vincent/Studio/lottoData/deploy/fetch_daily.sh +# 或 +/home/vincent/Studio/lottoData/venv/bin/python3 /home/vincent/Studio/lottoData/fetch_data.py +``` + +--- + +## 五、Web 接口 + +| 路径 | 方法 | 说明 | +|------|------|------| +| `/` | GET | Web 控制台页面 | +| `/api/status` | GET | 获取执行状态 | +| `/api/execute` | POST | 触发数据抓取 | + +### 示例 + +```bash +# 查看状态 +curl http://192.168.1.99:5000/api/status + +# 触发抓取 +curl -X POST http://192.168.1.99:5000/api/execute +``` + +--- + +## 六、验证清单 + +- [x] 依赖安装完整 (Flask, pandas, openpyxl, requests, beautifulsoup4) +- [x] systemd 服务运行正常 (active, enabled) +- [x] Web 服务可访问 (http://192.168.1.99:5000, HTTP 200) +- [x] API 接口正常 (/api/status, /api/execute) +- [x] Cron 定时任务已配置 (每日 2:30 抓取) +- [x] 手动抓取测试通过 (121 条记录保存成功) +- [x] 开机自启已配置 (systemd enable) + +--- + +## 七、回滚方案 + +```bash +# 停止服务 +sudo systemctl stop lotto-web +sudo systemctl disable lotto-web +sudo rm /etc/systemd/system/lotto-web.service +sudo systemctl daemon-reload + +# 移除 cron +crontab -l | grep -v 'lottoData' | crontab - + +# 不影响数据文件和代码 +``` + +--- + +## 八、监控要点 + +1. **服务存活**:`systemctl status lotto-web` 确认 active +2. **Web 可达**:`curl http://127.0.0.1:5000/api/status` +3. **数据更新**:检查 `/home/vincent/Studio/lottoData/双色球历史数据.xlsx` 修改时间 +4. **Cron 日志**:检查 `/home/vincent/Studio/lottoData/deploy/cron.log` +5. **磁盘空间**:Excel 文件约 250KB,可忽略 + +--- + +> 部署人:严维序 (opengineer) | 2026-06-29 \ No newline at end of file diff --git a/deploy/fetch_daily.sh b/deploy/fetch_daily.sh new file mode 100755 index 0000000..6fbe046 --- /dev/null +++ b/deploy/fetch_daily.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# 双色球历史数据每日自动抓取 +# Cron: 0 2 * * * /home/vincent/Studio/lottoData/deploy/fetch_daily.sh >> /home/vincent/Studio/lottoData/deploy/cron.log 2>&1 + +SCRIPT_DIR="/home/vincent/Studio/lottoData" +VENV_PYTHON="${SCRIPT_DIR}/venv/bin/python3" +FETCH_SCRIPT="${SCRIPT_DIR}/fetch_data.py" +LOG_DIR="${SCRIPT_DIR}/deploy" +LOG_FILE="${LOG_DIR}/fetch_$(date +%Y%m%d).log" + +mkdir -p "${LOG_DIR}" + +echo "=== $(date '+%Y-%m-%d %H:%M:%S') 开始执行双色球数据抓取 ===" +"${VENV_PYTHON}" "${FETCH_SCRIPT}" >> "${LOG_FILE}" 2>&1 +RC=$? +echo "=== $(date '+%Y-%m-%d %H:%M:%S') 执行完成, exit code=${RC} ===" +exit ${RC} \ No newline at end of file diff --git a/deploy/lotto-web.service b/deploy/lotto-web.service new file mode 100644 index 0000000..bff66d0 --- /dev/null +++ b/deploy/lotto-web.service @@ -0,0 +1,16 @@ +[Unit] +Description=双色球数据抓取 Web 服务 +After=network.target + +[Service] +Type=simple +User=vincent +WorkingDirectory=/home/vincent/Studio/lottoData +ExecStart=/home/vincent/Studio/lottoData/venv/bin/python3 /home/vincent/Studio/lottoData/web_executor.py +ExecStartPre=/home/vincent/Studio/lottoData/venv/bin/python3 -c "import flask; import pandas; import openpyxl; import requests; import bs4" +Restart=on-failure +RestartSec=5 +KillMode=control-group + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/docs/PRD-20260619-双色球数据抓取系统.md b/docs/PRD-20260619-双色球数据抓取系统.md new file mode 100644 index 0000000..1501e6a --- /dev/null +++ b/docs/PRD-20260619-双色球数据抓取系统.md @@ -0,0 +1,396 @@ +# 产品需求文档(PRD):双色球数据抓取系统 + +## 版本历史 +| 版本 | 日期 | 变更内容 | 作者 | +|------|------|----------|------| +| v1.0 | 2026-06-19 | 初始版本 | 沈路明 | + +--- + +## 1. 背景与目标 + +### 1.1 业务背景 +- 双色球作为中国最受欢迎的彩票类型,用户需要及时了解历史开奖数据进行分析和参考 +- 目前数据依赖人工查看,效率低且无法进行数据分析 +- 需要建立自动化数据抓取和展示系统,支持日常数据分析和研究 + +### 1.2 解决的问题 +1. **数据获取效率低**:人工访问网站查看开奖结果耗时且容易遗漏 +2. **数据难以分析**:网页数据无法直接用于 Excel 或其他分析工具 +3. **无法实时监控**:无法及时获取最新开奖数据 +4. **访问不便**:需要在局域网内便捷访问数据和管理工具 + +### 1.3 成功指标(可量化) +- **数据准确性**:抓取数据与官方网站一致率 100% +- **更新及时性**:开奖后 30 分钟内完成数据更新 +- **系统可用性**:Web 服务月可用性 ≥ 99% +- **用户体验**:页面加载时间 < 2 秒(局域网环境) + +--- + +## 2. 用户故事 + +| 角色 | 用户故事 | 价值 | +|------|----------|------| +| 数据分析员 | 作为数据分析员,我希望每天自动获取最新的双色球开奖数据,以便进行趋势分析 | 节省手动收集数据的时间,确保数据完整性 | +| 普通用户 | 作为普通用户,我希望通过网页界面手动触发数据抓取并实时查看结果,以便确认系统正常运行 | 透明化系统运行状态,增强信任感 | +| 系统管理员 | 作为系统管理员,我希望配置定时任务和时间,以便控制数据抓取的频率 | 灵活管理系统负载,避免频繁请求 | +| 局域网用户 | 作为局域网用户,我希望能在家中任何设备上访问 Web 界面,以便随时随地查看数据 | 提升访问便利性,支持多设备使用 | + +--- + +## 3. 功能需求 + +### 3.1 数据抓取模块 + +| 功能点 | 描述 | 优先级 | 验收标准 | +|--------|------|--------|----------| +| 数据源接入 | 从 https://www.55128.cn/kjh/fcssq-history-120.htm 抓取近 120 期双色球开奖数据 | P0 | 能正确解析 HTML 并提取所有开奖数据,无遗漏 | +| 增量更新 | 对比本地 Excel 文件,仅添加缺失的期数数据 | P0 | 不产生重复数据,新增数据准确率 100% | +| 数据校验 | 抓取后进行数据完整性校验(期数连续性、号码格式等) | P1 | 发现异常数据时记录错误日志并告警 | +| 错误重试 | 网络异常时自动重试(最多 3 次,间隔 5 秒) | P1 | 临时网络故障下成功抓取率 ≥ 95% | + +### 3.2 数据存储模块 + +| 功能点 | 描述 | 优先级 | 验收标准 | +|--------|------|--------|----------| +| Excel 存储 | 数据以 Excel 格式存储,文件名为「双色球历史数据.xlsx」 | P0 | 文件格式正确,可用 Excel 正常打开 | +| 字段定义 | 包含以下字段(见 4.1) | P0 | 所有字段完整,格式正确 | +| 数据导出 | 支持导出为 CSV 格式(可选) | P2 | 导出数据与 Excel 一致,编码为 UTF-8 | + +### 3.3 Web 界面模块 + +| 功能点 | 描述 | 优先级 | 验收标准 | +|--------|------|--------|----------| +| 手动触发抓取 | 提供"立即抓取"按钮,点击后执行抓取脚本 | P0 | 按钮点击后 3 秒内响应,显示执行状态 | +| 实时日志展示 | 显示抓取过程的实时日志(包括开始时间、抓取期数、完成时间等) | P0 | 日志自动滚动,支持暂停查看,颜色区分正常/警告/错误 | +| 数据概览 | 展示最新一期开奖号码(红球 6 个 + 蓝球 1 个) | P0 | 号码以球状展示,颜色正确(红球红色,蓝球蓝色) | +| 历史数据表 | 展示 Excel 中的数据,支持分页和搜索 | P1 | 支持按期数搜索,分页加载(每页 20 条) | +| 抓取状态监控 | 显示最近一次抓取时间、下次计划抓取时间、抓取状态(成功/失败) | P1 | 状态更新时间 < 5 秒延迟 | +| 定时任务配置 | 提供界面配置定时任务的执行时间 | P0 | 配置后立即生效,无需重启服务 | +| 响应式布局 | 支持桌面端和移动端访问 | P1 | 在主流手机浏览器上正常显示和操作 | + +### 3.4 定时任务模块 + +| 功能点 | 描述 | 优先级 | 验收标准 | +|--------|------|--------|----------| +| Cron 定时任务 | 使用 Linux cron 实现每天自动抓取 | P0 | 任务配置正确,日志中可查到执行记录 | +| 执行时间配置 | 默认为每天 21:30(开奖后 15 分钟) | P0 | 可通过 Web 界面修改执行时间 | +| 任务开关 | 提供启用/禁用定时任务的开关 | P1 | 开关状态立即生效,界面显示当前状态 | + +--- + +## 4. 非功能需求 + +### 4.1 数据字段定义 + +**Excel 文件字段:** + +| 字段名 | 类型 | 说明 | 示例 | +|--------|------|------|------| +| 开奖日期 | Date | 开奖日期 | 2026-06-18 | +| 期数 | String | 双色球期号 | 2026069 | +| 红球 1 | Integer | 第 1 个红球号码(1-33) | 12 | +| 红球 2 | Integer | 第 2 个红球号码(1-33) | 14 | +| 红球 3 | Integer | 第 3 个红球号码(1-33) | 16 | +| 红球 4 | Integer | 第 4 个红球号码(1-33) | 17 | +| 红球 5 | Integer | 第 5 个红球号码(1-33) | 18 | +| 红球 6 | Integer | 第 6 个红球号码(1-33) | 32 | +| 蓝球 | Integer | 蓝球号码(1-16) | 08 | +| 和值 | Integer | 6 个红球号码之和 | 109 | +| 奇偶比 | String | 红球奇偶比例 | 1:5 | +| 大小比 | String | 红球大小比例(1-16 为小,17-33 为大) | 3:3 | +| 开机号 | String | 开机号(如有) | 07,13,19,24,27,31+05 | +| 跨度 | Integer | 红球最大值 - 最小值 | 20 | + +### 4.2 性能要求 + +| 指标 | 要求 | 说明 | +|------|------|------| +| 抓取耗时 | < 30 秒 | 完成 120 期数据抓取和更新的总耗时 | +| 页面加载 | < 2 秒 | Web 界面在局域网环境下的加载时间 | +| 并发支持 | ≥ 5 个并发用户 | 支持至少 5 个用户同时访问 Web 界面 | +| 内存占用 | < 200MB | Python 进程常驻内存占用 | + +### 4.3 兼容性要求 + +| 维度 | 要求 | +|------|------| +| 操作系统 | Ubuntu 20.04+ / macOS 12+ | +| Python 版本 | Python 3.8+ | +| 浏览器 | Chrome 90+、Firefox 88+、Safari 14+、Edge 90+ | +| 移动端 | iOS Safari 14+、Android Chrome 90+ | + +### 4.4 安全要求 + +**局域网访问安全:** + +| 安全项 | 方案 | 说明 | +|--------|------|------| +| 访问控制 | 仅监听 0.0.0.0,通过防火墙限制访问来源 | 只允许内网 IP 段访问(如 192.168.0.0/16) | +| 身份验证 | 可选配置基本 HTTP 认证(用户名 + 密码) | 防止未授权访问,密码通过配置文件管理 | +| 日志记录 | 所有访问和操作记录日志 | 包含时间、IP、操作类型、结果 | +| 命令注入防护 | Web 界面不接受任何命令行输入 | 防止用户通过界面执行任意命令 | + +### 4.5 可靠性要求 + +| 指标 | 要求 | 说明 | +|------|------|------| +| 服务可用性 | ≥ 99% | 月累计不可用时间 < 7.2 小时 | +| 数据持久化 | 100% | 已抓取数据不丢失,异常情况自动备份 | +| 断电恢复 | 自动恢复服务 | 系统重启后服务自动拉起(通过 systemd) | + +--- + +## 5. 系统架构设计 + +### 5.1 技术选型 + +| 模块 | 技术栈 | 选型理由 | +|------|--------|----------| +| 数据抓取 | Python + requests + BeautifulSoup4 | 成熟的网页抓取方案,社区支持好 | +| 数据存储 | openpyxl | 原生支持 Excel 读写,无需安装 Office | +| Web 服务 | Flask | 轻量级,易部署,适合内部工具 | +| 定时任务 | Linux Cron | 系统原生支持,稳定可靠 | +| 前端 | HTML + CSS + JavaScript (jQuery) | 简单直接,无需构建流程 | +| 日志 | Python logging | 标准库支持,灵活配置 | + +### 5.2 目录结构 + +``` +/home/vincent/Studio/lottoData/ +├── lottery.py # 号码生成器(现有) +├── 双色球历史数据.xlsx # 数据存储(现有) +├── scraper.py # 数据抓取脚本(新建) +├── app.py # Flask Web 应用(新建) +├── config.py # 配置文件(新建) +├── templates/ # HTML 模板(新建) +│ └── index.html +├── static/ # 静态资源(新建) +│ ├── css/ +│ └── js/ +├── logs/ # 日志目录(新建) +│ └── lottery.log +└── requirements.txt # Python 依赖(新建) +``` + +### 5.3 定时任务配置 + +**Cron 配置示例:** +```bash +# 每天 21:30 执行数据抓取 +30 21 * * * cd /home/vincent/Studio/lottoData && /usr/bin/python3 scraper.py >> logs/cron.log 2>&1 +``` + +**Systemd 服务配置(Web 服务):** +```ini +# /etc/systemd/system/lottery-web.service +[Unit] +Description=Lottery Data Web Service +After=network.target + +[Service] +Type=simple +User=vincent +WorkingDirectory=/home/vincent/Studio/lottoData +ExecStart=/usr/bin/python3 app.py +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +### 5.4 页面流程图 + +``` +用户访问 Web 界面 + ↓ +加载首页(显示最新开奖数据 + 历史数据) + ↓ +┌──────────────┬──────────────┬──────────────┐ +│ │ │ │ +▼ ▼ ▼ ▼ +手动触发抓取 查看历史数据 配置定时任务 监控抓取状态 + ↓ ↓ ↓ ↓ +显示执行日志 分页/搜索查看 修改执行时间 查看上次抓取结果 + ↓ ↓ ↓ ↓ +更新数据表 导出数据 保存配置 显示下次执行时间 +``` + +--- + +## 6. 接口设计 + +### 6.1 Web 接口 + +| 接口路径 | 方法 | 描述 | 请求参数 | 返回格式 | +|----------|------|------|----------|----------| +| `/` | GET | 首页 - 数据展示 | 无 | HTML | +| `/api/scrape` | POST | 手动触发抓取 | 无 | JSON `{"status": "success", "message": "..."}` | +| `/api/status` | GET | 获取系统状态 | 无 | JSON `{last_scrape_time, next_scheduled_time, status}` | +| `/api/history` | GET | 获取历史数据 | `page`, `limit`, `search` | JSON `{"data": [...], "total": N}` | +| `/api/config` | GET | 获取定时任务配置 | 无 | JSON | +| `/api/config` | POST | 更新定时任务配置 | `enabled`, `cron_expr` | JSON `{"status": "success"}` | + +### 6.2 日志格式 + +``` +[2026-06-19 21:30:01 INFO] 开始抓取数据... +[2026-06-19 21:30:02 INFO] 成功访问数据源页面 +[2026-06-19 21:30:05 INFO] 解析到 120 条数据 +[2026-06-19 21:30:06 INFO] 本地现有数据:118 条 +[2026-06-19 21:30:06 INFO] 新增数据:2 条 (2026069, 2026068) +[2026-06-19 21:30:07 INFO] 数据保存到 Excel 成功 +[2026-06-19 21:30:07 INFO] 抓取完成,耗时 6.2 秒 +``` + +--- + +## 7. 排期建议 + +### 7.1 开发阶段划分 + +| 阶段 | 任务 | 预计耗时 | 依赖 | +|------|------|----------|------| +| 阶段 1 | 数据抓取脚本开发(scraper.py) | 2 小时 | 无 | +| 阶段 2 | Web 服务开发(app.py + templates) | 3 小时 | 阶段 1 完成 | +| 阶段 3 | 定时任务配置和系统部署 | 1 小时 | 阶段 2 完成 | +| 阶段 4 | 测试和优化 | 2 小时 | 阶段 3 完成 | + +**总预计耗时:8 小时** + +### 7.2 关键依赖 + +1. **服务器访问权限**:需要能访问目标网站(55128.cn) +2. **Python 环境**:需要安装 Python 3.8+ 和依赖包 +3. **局域网网络**:确保局域网内设备可以访问服务端口 + +--- + +## 8. 风险与应对 + +### 8.1 需求风险 + +| 风险 | 影响 | 概率 | 应对方案 | +|------|------|------|----------| +| 数据源网站结构变化 | 抓取脚本失效 | 中 | 1. 添加结构变更检测
2. 建立告警机制
3. 定期维护更新 | +| 数据源网站反爬策略 | 被封 IP | 低 | 1. 控制请求频率
2. 添加 User-Agent
3. 必要时使用代理 | + +### 8.2 技术风险 + +| 风险 | 影响 | 概率 | 应对方案 | +|------|------|------|----------| +| Excel 文件被占用 | 写入失败 | 低 | 1. 写入前检查文件锁
2. 使用临时文件 + 原子替换
3. 失败后重试 | +| 网络不稳定 | 抓取失败 | 中 | 1. 实现重试机制
2. 记录失败原因
3. 邮件/消息告警 | +| 端口冲突 | Web 服务启动失败 | 低 | 1. 启动前检查端口占用
2. 支持配置端口
3. 日志明确提示 | + +### 8.3 安全风险 + +| 风险 | 影响 | 概率 | 应对方案 | +|------|------|------|----------| +| 未授权访问 | 数据泄露 | 中 | 1. 防火墙限制 IP 范围
2. 可选 HTTP 认证
3. 访问日志审计 | +| 恶意请求 | 服务崩溃 | 低 | 1. 请求频率限制
2. 异常输入过滤
3. 服务监控 | + +--- + +## 9. 验收标准 + +### 9.1 功能验收 + +- [ ] **数据抓取**:能从 55128.cn 成功抓取近 120 期数据 +- [ ] **增量更新**:不产生重复数据,仅添加缺失期数 +- [ ] **Web 界面**:能手动触发抓取并实时查看日志 +- [ ] **数据展示**:首页展示最新开奖号码和历史数据表 +- [ ] **定时任务**:配置 cron 后每天自动执行 +- [ ] **局域网访问**:其他设备能访问 Web 服务 + +### 9.2 质量验收 + +- [ ] **数据准确性**:与官方网站数据 100% 一致 +- [ ] **性能达标**:页面加载时间 < 2 秒(局域网) +- [ ] **日志完整**:所有关键操作都有日志记录 +- [ ] **错误处理**:异常情况有明确提示和日志 + +### 9.3 文档验收 + +- [ ] **README 文档**:包含安装、配置、使用说明 +- [ ] **依赖清单**:requirements.txt 完整列出 Python 依赖 +- [ ] **部署文档**:Systemd 服务配置说明 + +--- + +## 10. 后续优化建议 + +### 10.1 短期优化(1 个月内) + +1. **数据可视化**:增加号码分布图、冷热号统计等图表 +2. **告警通知**:抓取失败时通过邮件/微信通知 +3. **数据备份**:每天自动备份 Excel 文件到云存储 + +### 10.2 中期优化(3 个月内) + +1. **数据库升级**:使用 SQLite/MySQL 替代 Excel,支持更复杂查询 +2. **API 开放**:提供 REST API 供其他系统调用 +3. **多彩种支持**:扩展支持大乐透、福彩 3D 等其他彩票 + +### 10.3 长期优化(6 个月内) + +1. **数据分析平台**:集成数据分析工具,提供智能预测 +2. **移动端 App**:开发 iOS/Android 应用 +3. **多用户系统**:支持多个用户独立配置和使用 + +--- + +## 附录 + +### A. 依赖包清单 + +```txt +# requirements.txt +requests==2.31.0 +beautifulsoup4==4.12.2 +openpyxl==3.1.2 +flask==3.0.0 +lxml==4.9.3 +``` + +### B. 配置文件示例 + +```python +# config.py +import os + +# 系统配置 +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +DATA_FILE = os.path.join(BASE_DIR, '双色球历史数据.xlsx') +LOG_FILE = os.path.join(BASE_DIR, 'logs', 'lottery.log') + +# 数据源 +DATA_SOURCE_URL = 'https://www.55128.cn/kjh/fcssq-history-120.htm' + +# Web 服务配置 +WEB_HOST = '0.0.0.0' +WEB_PORT = 5000 +DEBUG = False + +# 定时任务配置 +CRON_ENABLED = True +CRON_EXPR = '30 21 * * *' # 每天 21:30 + +# 安全配置(可选) +AUTH_ENABLED = False +AUTH_USERNAME = 'admin' +AUTH_PASSWORD = 'your_password_here' # 建议通过环境变量设置 +``` + +### C. 数据来源说明 + +- **数据源**:彩吧助手 (www.55128.cn) +- **数据更新时间**:每周二、四、日 21:15 开奖 +- **建议抓取时间**:开奖后 15 分钟(21:30) + +--- + +**PRD 评审人:** 承哥 +**PRD 创建日期:** 2026-06-19 +**下次评审日期:** 开发完成后验收评审 \ No newline at end of file diff --git a/docs/PRD-双色球 WebUI-v1.0.md b/docs/PRD-双色球 WebUI-v1.0.md new file mode 100644 index 0000000..d832091 --- /dev/null +++ b/docs/PRD-双色球 WebUI-v1.0.md @@ -0,0 +1,361 @@ +# 产品需求文档(PRD):双色球自动化系统 Web UI + +**文档版本**: v1.0 +**创建日期**: 2026-07-03 +**产品经理**: 沈路明 +**状态**: 待评审 + +--- + +## 1. 背景与目标 + +### 1.1 业务背景 +双色球自动化项目已完成核心功能开发(数据爬取、号码生成、统计分析),但缺乏统一的 Web 界面供用户(刘总及团队成员)便捷访问和使用。现有功能分散在多个 Python 脚本中,需要在网页端整合为一体化服务。 + +### 1.2 解决的问题 +- **访问门槛高**: 用户需要运行 Python 脚本才能生成号码,操作复杂 +- **数据不可视**: 历史数据、生成记录、统计信息无法直观查看 +- **无法局域网共享**: 缺少监听 0.0.0.0 的 Web 服务,无法在 PC/手机端跨设备访问 +- **功能分散**: 数据抓取、号码生成、历史记录查看分散在不同入口 + +### 1.3 成功指标(定量) +| 指标 | 目标值 | 衡量方式 | +|------|--------|----------| +| 功能覆盖率 | 100% 核心功能 | PRD 功能清单验收 | +| 响应式支持 | PC + 移动端 | 主流设备测试通过 | +| 局域网可访问 | 0.0.0.0:8085 | 网络扫描验证 | +| API 响应时间 | <2s (P95) | 压力测试 | +| 用户满意度 | 刘总验收通过 | 评审会议确认 | + +--- + +## 2. 用户故事 + +| ID | 用户角色 | 故事 | 价值 | +|----|----------|------|------| +| US-01 | 刘总(主要用户) | 作为用户,我希望在手机上打开网页就能生成双色球号码,以便随时查看推荐号码 | 便捷性、即时访问 | +| US-02 | 刘总 | 作为用户,我希望查看历史开奖数据,以便分析号码趋势 | 数据驱动决策 | +| US-03 | 刘总 | 作为用户,我希望查看历史生成记录并下载 Excel,以便存档和分享 | 数据持久化 | +| US-04 | 团队成员 | 作为团队成员,我希望在 PC 端大屏查看统计数据,以便进行项目汇报 | 可视化展示 | +| US-05 | 团队成员 | 作为运维人员,我希望服务监听局域网,以便多人同时访问 | 资源共享 | + +--- + +## 3. 功能需求 + +### 3.1 功能清单与优先级 + +| 功能模块 | 功能点 | 描述 | 优先级 | 验收标准 | +|----------|--------|------|--------|----------| +| **号码生成** | GEN-01 | 选择生成注数(1-1000) | P0 | 滑块/输入框可调,范围校验正确 | +| | GEN-02 | 选择策略(基础/高级) | P0 | 下拉选择,高级策略调用热冷号分析 | +| | GEN-03 | 执行生成并展示结果 | P0 | 点击生成后 3s 内返回结果,展示红球 + 蓝球 | +| | GEN-04 | 结果页展示和值、奇偶比、大小比、跨度 | P0 | 每个号码下方显示统计指标 | +| | GEN-05 | 下载 Excel 文件 | P0 | 点击下载可获取完整 Excel | +| | GEN-06 | 生成记录自动保存 | P0 | 每次生成后记录存入 `.generation_records.json` | +| **历史数据** | HIS-01 | 查看历史开奖数据列表 | P0 | 分页展示,每页 20 条,支持翻页 | +| | HIS-02 | 搜索历史数据(按期号/日期) | P1 | 搜索框输入后 500ms 防抖查询 | +| | HIS-03 | 红球/蓝球高亮显示 | P0 | 红球红色背景,蓝球蓝色背景 | +| | HIS-04 | 展示和值、奇偶比、大小比、跨度 | P1 | 列表中包含这些统计字段 | +| **生成记录** | REC-01 | 查看历史生成记录列表 | P0 | 分页展示,显示策略、注数、时间、文件大小 | +| | REC-02 | 下载生成结果文件 | P0 | 点击下载可获取对应 Excel | +| | REC-03 | 删除生成记录 | P1 | 删除后同时删除对应文件,列表刷新 | +| **统计数据** | STA-01 | 展示历史开奖期数 | P0 | 数字准确 | +| | STA-02 | 红球热号 TOP15 | P0 | 按出现频次降序排列 | +| | STA-03 | 红球冷号 TOP15 | P0 | 按出现频次升序排列 | +| | STA-04 | 蓝球热号 TOP8 | P0 | 按出现频次降序排列 | +| | STA-05 | 最常见奇偶比 | P1 | 显示频次最高的奇偶比形态 | +| | STA-06 | 最常见大小比 | P1 | 显示频次最高的 大小比形态 | +| | STA-07 | 和值范围统计 | P1 | 显示最小值、最大值、平均值、标准差 | +| | STA-08 | 跨度范围统计 | P1 | 显示最小值、最大值、平均值、标准差 | +| **系统功能** | SYS-01 | 监听 0.0.0.0:8085 | P0 | `netstat -tlnp` 验证 | +| | SYS-02 | PC/移动端响应式布局 | P0 | 视口宽度 320px-1920px 自适应 | +| | SYS-03 | 页面导航(4 个 Tab) | P0 | 生成、历史数据、生成记录、统计 | +| | SYS-04 | API Token 认证(可选) | P2 | 配置项 `auth_enabled` 控制开关 | +| | SYS-05 | 系统状态接口 | P1 | `/api/status` 返回服务状态 | + +### 3.2 核心业务流程 + +``` +用户访问 → 首页(号码生成) → 选择注数/策略 → 点击生成 + ↓ +后端调用 lottery.py → 分析历史数据 → 生成号码 → 保存 Excel + ↓ +返回结果 → 前端展示(红球 + 蓝球 + 统计指标) → 可下载/查看记录 +``` + +--- + +## 4. 非功能需求 + +### 4.1 性能要求 +| 指标 | 要求 | 说明 | +|------|------|------| +| 页面加载时间 | <3s (P95) | 首次加载,含静态资源 | +| API 响应时间 | <2s (P95) | 不含号码生成(含历史数据查询、统计) | +| 号码生成时间 | <10s (P95) | 100 注以内,高级策略 | +| 并发用户数 | ≥10 | 局域网内同时访问 | + +### 4.2 兼容性要求 +| 平台 | 浏览器 | 版本要求 | +|------|--------|----------| +| PC 端 | Chrome | 90+ | +| PC 端 | Safari | 14+ | +| PC 端 | Edge | 90+ | +| 移动端 | iOS Safari | 14+ | +| 移动端 | Android Chrome | 90+ | +| 移动端 | 微信内置浏览器 | 最新版 | + +### 4.3 安全要求 +- **API 认证**: 可选 Token 认证(`auth_enabled` 配置项) +- **目录遍历防护**: 下载接口校验路径,禁止 `..` 和绝对路径 +- **HTTPS**: 内网环境暂不强制,外网部署需配置 SSL + +### 4.4 可用性要求 +- **服务可用性**: ≥99%(工作时段 9:00-22:00) +- **数据持久化**: 生成记录永久保存,除非用户主动删除 +- **错误处理**: 所有 API 失败返回友好提示,不暴露堆栈信息 + +--- + +## 5. 原型与界面 + +### 5.1 页面结构 +``` +┌─────────────────────────────────────────┐ +│ Header(标题 + 副标题) │ +├─────────────────────────────────────────┤ +│ Nav Tabs: 生成 | 历史数据 | 记录 | 统计 │ +├─────────────────────────────────────────┤ +│ │ +│ Page Content │ +│ (根据 Tab 切换内容) │ +│ │ +└─────────────────────────────────────────┘ +``` + +### 5.2 关键界面描述 + +#### 5.2.1 号码生成页(首页) +- **顶部**: 统计概览(历史期数、常见奇偶比、常见大小比、和值范围、跨度范围、热号预览) +- **中部**: 生成参数配置(滑块选择注数 1-1000,下拉选择策略) +- **操作区**: 「立即生成」大按钮 +- **结果区**: 号码卡片列表(每注显示红球 6 个 + 蓝球 1 个,下方显示和值、奇偶比、大小比、跨度) +- **底部**: 「下载 Excel」按钮 + +#### 5.2.2 历史数据页 +- **顶部**: 搜索框(按期号/日期搜索,500ms 防抖) +- **列表**: 表格展示(期号、开奖日期、红球 6 个、蓝球 1 个、和值、奇偶形态、大小比、跨度) +- **底部**: 分页控件(上一页/页码/下一页) + +#### 5.2.3 生成记录页 +- **列表**: 卡片式展示(策略、注数、生成时间、文件大小) +- **操作**: 每条记录含「下载」和「删除」按钮 +- **底部**: 分页控件 + +#### 5.2.4 统计页 +- **数据卡片**: 历史开奖期数 +- **热号区**: 红球热号 TOP15(红色球)、蓝球热号 TOP8(蓝色球) +- **冷号区**: 红球冷号 TOP15(红色球,透明度降低) +- **统计网格**: 最常见奇偶比、最常见大小比、和值范围(min-max-mean-std)、跨度范围(min-max-mean-std) + +### 5.3 设计规范 +| 元素 | 规范 | +|------|------| +| 主色调 | 红色 #e74c3c(双色球主题) | +| 辅色 | 蓝色 #3498db(蓝球)、紫色 #8e44ad(渐变) | +| 字体 | 系统默认(-apple-system, PingFang SC, Microsoft YaHei) | +| 卡片圆角 | 12px | +| 阴影 | 0 2px 12px rgba(0,0,0,0.08) | +| 移动导航 | 底部固定(高度 56px) | +| PC 导航 | 顶部 Tab(高度 68px,sticky) | + +--- + +## 6. API 接口设计 + +### 6.1 接口清单 + +| 接口 | 方法 | 描述 | 认证 | +|------|------|------|------| +| `/api/generate` | POST | 生成号码 | 可选 | +| `/api/history` | GET | 获取历史开奖数据 | 可选 | +| `/api/records` | GET | 获取生成记录列表 | 可选 | +| `/api/records/:id` | DELETE | 删除生成记录 | 可选 | +| `/api/statistics` | GET | 获取统计数据 | 可选 | +| `/api/download/:filepath` | GET | 下载文件 | 可选 | +| `/api/status` | GET | 系统状态 | 无 | +| `/api/config` | GET | 前端配置 | 无 | + +### 6.2 关键接口示例 + +#### POST /api/generate +**请求**: +```json +{ + "num_tickets": 10, + "strategy": "advanced" +} +``` + +**响应**: +```json +{ + "success": true, + "data": { + "tickets": [ + { + "index": 1, + "reds": [3, 12, 18, 23, 27, 31], + "blue": 9, + "sum_value": 114, + "odd_even": "3:3", + "size_ratio": "4:2", + "span": 28 + } + ], + "total": 10, + "filename": "lottery/双色球_20260703_142530_高级策略_10 注.xlsx", + "download_url": "/api/download/lottery/双色球_20260703_142530_高级策略_10 注.xlsx", + "record": {...}, + "statistics": {...} + } +} +``` + +--- + +## 7. 技术架构 + +### 7.1 技术栈 +| 层级 | 技术 | 版本 | +|------|------|------| +| 后端 | Python + Flask | 3.x + 2.x | +| 前端 | 原生 HTML/CSS/JS | ES6+ | +| 数据存储 | Excel + JSON | openpyxl | +| 号码生成 | NumPy + Pandas | 1.x | +| 部署 | 直接运行 | `python3 app.py` | + +### 7.2 目录结构 +``` +lottoData/ +├── app.py # Flask 主服务(17KB) +├── index.html # 前端 UI(42KB,响应式) +├── lottery.py # 号码生成核心逻辑(51KB) +├── fetch_data.py # 数据爬取脚本 +├── web_executor.py # 数据抓取 Web 服务(独立) +├── web_console.html # 数据抓取控制台 +├── 双色球历史数据.xlsx # 历史数据(由 fetch_data.py 更新) +├── lottery/ # 号码生成结果目录 +├── .generation_records.json # 生成记录索引 +└── .fetch_status.json # 抓取状态(web_executor 用) +``` + +### 7.3 部署方式 +```bash +# 进入项目目录 +cd /home/vincent/Studio/lottoData + +# 启动服务 +python3 app.py + +# 访问地址 +本地:http://localhost:8085 +局域网:http://<本机 IP>:8085 +``` + +### 7.4 端口与监听 +- **默认端口**: 8085 +- **监听地址**: 0.0.0.0(局域网可访问) +- **可配置**: `CONFIG['port']` 和 `CONFIG['host']` + +--- + +## 8. 数据埋点与监控 + +### 8.1 关键事件埋点建议 +| 事件 | 触发时机 | 数据字段 | +|------|----------|----------| +| `page_view` | 页面加载 | page_name, user_agent, timestamp | +| `generate_click` | 点击生成按钮 | num_tickets, strategy | +| `generate_success` | 生成成功 | num_tickets, strategy, duration_ms | +| `generate_failure` | 生成失败 | error_message, strategy | +| `download_click` | 点击下载 | file_name, source (生成结果/记录列表) | +| `record_delete` | 删除记录 | record_id | + +### 8.2 监控指标 +- **服务可用性**: 心跳检测(/api/status) +- **API 错误率**: 按接口统计 5xx 错误占比 +- **生成成功率**: 成功次数 / 总请求次数 +- **平均响应时间**: 各接口 P95延迟 + +--- + +## 9. 排期建议 + +| 阶段 | 工作内容 | 负责人 | 预估工时 | +|------|----------|--------|----------| +| 评审 | PRD 评审 + 架构评审 | 全员 | 2h | +| 开发 | 已有代码,无需开发 | - | 0h | +| 测试 | 功能测试 + 兼容性测试 | 测试 | 4h | +| 部署 | 服务启动 + 防火墙配置 | 运维 | 1h | +| 验收 | 刘总验收 | 刘总 | 待定 | + +**总工时**: 7 小时(主要为测试和验收) + +--- + +## 10. 风险与应对 + +| 风险 | 影响 | 概率 | 应对措施 | +|------|------|------|----------| +| 历史数据文件损坏 | 无法生成号码 | 低 | 定期备份 `.xlsx` 文件 | +| 局域网网络问题 | 无法访问 | 中 | 检查防火墙,确保 8085 端口开放 | +| 并发过高导致服务卡顿 | 体验下降 | 低 | 限制单 IP 请求频率,增加超时控制 | +| Excel 文件过大 | 下载缓慢 | 中 | 单个文件限制 1000 注,超过分批下载 | +| 移动端适配问题 | 显示错乱 | 低 | 真机测试主流设备 | + +--- + +## 11. 版本历史 + +| 版本 | 日期 | 修改内容 | 修改人 | +|------|------|----------|--------| +| v1.0 | 2026-07-03 | 初始版本,基于现有代码逆向整理 PRD | 沈路明 | + +--- + +## 12. 附件 + +### 12.1 已有代码文件清单 +1. `app.py` (17KB) - Flask 后端服务 +2. `index.html` (42KB) - 响应式前端 UI +3. `lottery.py` (51KB) - 号码生成核心逻辑 +4. `fetch_data.py` (3.8KB) - 历史数据抓取 +5. `web_executor.py` (6.4KB) - 数据抓取 Web 服务 +6. `web_console.html` (11KB) - 数据抓取控制台 +7. `双色球历史数据.xlsx` (12KB) - 历史数据文件 + +### 12.2 Git 仓库 +- **仓库地址**: http://192.168.1.99:12299/vincent/Lottery.git +- **当前状态**: 代码已完成,待提交至 Git + +### 12.3 访问地址 +- **本地访问**: http://localhost:8085 +- **局域网访问**: http://192.168.1.99:8085(示例 IP,以实际为准) + +--- + +## 13. 后续优化建议(非本期) + +| 功能 | 描述 | 优先级 | +|------|------|--------| +| 用户登录系统 | 多用户权限管理 | P2 | +| 定时任务调度 | 自动生成 + 推送 | P2 | +| 数据可视化图表 | 走势图、分布图 | P2 | +| 微信推送 | 生成结果推送至微信 | P3 | +| 多彩种支持 | 大乐透、福彩 3D 等 | P3 | + +--- + +**PRD 评审准备就绪,邀请架构师(梁思筑)和开发(徐聪)参与评审。** \ No newline at end of file diff --git a/docs/PRD-操作界面系统.md b/docs/PRD-操作界面系统.md new file mode 100644 index 0000000..8a35e0d --- /dev/null +++ b/docs/PRD-操作界面系统.md @@ -0,0 +1,512 @@ +# 产品需求文档(PRD):双色球操作界面系统 + +## 1. 背景与目标 + +### 业务背景 +基于已有的 lottery.py 号码生成逻辑,需要开发一个 Web 操作界面系统,让用户可以通过 PC 网页端和手机端方便地操控双色球号码生成、查看历史数据、管理生成记录。系统需支持远程访问,生成的数据可随时读取。 + +### 解决的问题 +1. 命令行操作门槛高,普通用户难以使用 +2. 无法随时随地访问和管理数据 +3. 缺少可视化的数据展示和管理界面 +4. 生成的 Excel 文件分散,难以统一管理 + +### 成功指标 +- [ ] PC 和手机端均可正常访问操作 +- [ ] 核心功能(生成号码、查看历史、下载数据)100% 可用 +- [ ] 内网环境下远程访问响应时间 < 2 秒 +- [ ] 数据导出成功率 100% + +--- + +## 2. 用户故事 + +| 角色 | 用户故事 | 价值 | +|------|----------|------| +| 普通用户 | 作为用户,我希望通过手机浏览器生成双色球号码,以便随时随地选号 | 移动便利性 | +| 数据管理员 | 作为管理员,我希望查看历史数据和生成记录,以便分析趋势 | 数据可追溯 | +| 分析师 | 作为分析师,我希望导出 Excel 数据进行深度分析,以便制定策略 | 数据可复用 | +| 运维人员 | 作为运维,我希望系统监听 0.0.0.0 并避免端口冲突,以便内网多设备访问 | 部署便利性 | + +--- + +## 3. 功能需求 + +### 3.1 核心功能模块 + +#### 模块一:号码生成 +| 功能点 | 描述 | 优先级 | 验收标准 | +|--------|------|--------|----------| +| 选择策略 | 用户可选择"高级策略"或"基础策略" | P0 | 默认选中高级策略,切换无延迟 | +| 输入注数 | 用户输入要生成的注数(1-1000) | P0 | 有输入验证,超出范围提示错误 | +| 立即生成 | 点击按钮后生成号码并在页面展示 | P0 | 生成时间 < 3 秒(100 注内) | +| 展示结果 | 以表格形式展示生成的号码,包含和值、奇偶比、大小比、跨度 | P0 | 红球以蓝色圆形展示,蓝球以红色圆形展示 | +| 保存 Excel | 将生成结果保存为 Excel 文件并提供下载 | P0 | 文件格式正确,包含"生成号码"和"统计信息"两个 sheet | + +#### 模块二:历史数据查看 +| 功能点 | 描述 | 优先级 | 验收标准 | +|--------|------|--------|----------| +| 热力图展示 | 展示红球热号/冷号、蓝球热号 | P0 | 用颜色深浅表示频率,支持排序切换 | +| 统计面板 | 显示最常见奇偶比、大小比、和值范围、跨度范围 | P1 | 数据与 lottery.py 计算结果一致 | +| 历史期数列表 | 展示双色球历史开奖数据列表 | P1 | 支持分页,每页 20 条,支持按期号搜索 | + +#### 模块三:生成记录管理 +| 功能点 | 描述 | 优先级 | 验收标准 | +|--------|------|--------|----------| +| 记录列表 | 展示所有历史生成记录(时间、策略、注数、文件) | P0 | 按时间倒序排列,支持分页 | +| 文件下载 | 提供历史生成文件的下载链接 | P0 | 点击即可下载,文件名包含日期和注数 | +| 记录删除 | 允许用户删除单条或多条生成记录 | P2 | 删除前有二次确认,删除后同步删除文件 | + +#### 模块四:数据导出 API +| 功能点 | 描述 | 优先级 | 验收标准 | +|--------|------|--------|----------| +| RESTful API | 提供 GET /api/latest 接口获取最新生成数据 | P1 | 返回 JSON 格式,包含所有字段 | +| API 认证 | 简单 Token 认证机制 | P1 | 请求需携带 Authorization header | +| CORS 支持 | 允许跨域访问 | P1 | 配置 Access-Control-Allow-Origin | + +### 3.2 界面设计要求 + +#### PC 网页端 +- 响应式布局,支持主流浏览器(Chrome、Firefox、Edge、Safari) +- 左侧导航栏:首页(生成)、历史数据、生成记录、系统设置 +- 主内容区自适应宽度 +- 生成号码页面:上方为策略选择和注数输入,中间为结果展示区,下方为操作按钮 + +#### 手机端 +- 采用移动优先设计,触摸操作友好 +- 底部导航栏:首页、数据、记录 +- 生成号码页面:顶部为策略选择(下拉框),中间为注数输入(数字键盘),下方为生成按钮 +- 结果展示支持横向滑动查看完整信息 +- 字体大小适配移动端,关键信息(号码)字号放大 + +### 3.3 非功能需求 + +#### 性能要求 +- 页面加载时间 < 2 秒(内网环境) +- 号码生成响应时间 < 3 秒(100 注内) +- 支持并发用户数:5 个同时操作 + +#### 兼容性要求 +- PC 端:Chrome 80+、Firefox 75+、Edge 80+、Safari 13+ +- 移动端:iOS Safari 13+、Android Chrome 80+ +- 屏幕适配:PC(1280x720 及以上)、手机(375x667 及以上) + +#### 安全要求 +- 内网访问限制:通过防火墙规则限制仅内网 IP 可访问 +- 简单认证:配置文件存储访问 Token,防止未授权访问 +- 文件访问控制:仅允许下载 lottery 目录下的文件,防止目录遍历攻击 + +#### 部署要求 +- 监听地址:0.0.0.0(支持内网任意设备访问) +- 端口:提前与运维确认未使用端口(建议 8080-8099 范围内) +- 服务器:192.168.1.99,路径 ~/Studio/lottoData/ +- 进程保活:使用 systemd 或 supervisor 确保服务异常后自动重启 + +--- + +## 4. 技术架构 + +### 4.1 技术栈选择 +| 层级 | 技术 | 理由 | +|------|------|------| +| 后端框架 | Flask | 轻量级,与现有 lottery.py 兼容性好 | +| 前端框架 | Vue.js 3 + Element Plus | 响应式支持好,组件丰富 | +| 移动端适配 | Vant UI | 移动端组件库,触摸友好 | +| 数据存储 | 本地文件系统 | 保持现有 Excel 存储方式,无需数据库 | +| API 文档 | Swagger/OpenAPI | 便于调试和集成 | + +### 4.2 系统架构图 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 用户设备层 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ PC 浏览器 │ │ 手机浏览器 │ │ API 客户端 │ │ +│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ +│ │ │ │ │ +│ └──────────────────┼──────────────────┘ │ +│ │ HTTP/HTTPS │ +└───────────────────────────┼───────────────────────────────┘ + │ +┌───────────────────────────┼───────────────────────────────┐ +│ 应用服务层 (Flask) │ +│ ┌────────────────────────┴────────────────────────┐ │ +│ │ 路由控制器 │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ 号码生成 │ │ 历史数据 │ │ 记录管理 │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ │ │ +│ └─────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────────────┴────────────────────────┐ │ +│ │ 业务逻辑层 │ │ +│ │ (集成 lottery.py 核心逻辑) │ │ +│ └─────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────────────┴────────────────────────┐ │ +│ │ 数据访问层 │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ Excel 读写│ │ 文件管理 │ │ 缓存管理 │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ │ │ +│ └─────────────────────────────────────────────────┘ │ +└───────────────────────────────────────────────────────────┘ + │ +┌───────────────────────────┼───────────────────────────────┐ +│ 数据存储层 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │历史数据.xlsx │ │ lottery/ │ │ 配置文件 │ │ +│ │ │ │ 生成文件 │ │ config.yaml│ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└───────────────────────────────────────────────────────────┘ +``` + +### 4.3 API 接口设计 + +#### 4.3.1 号码生成 +``` +POST /api/generate +Request: +{ + "num_tickets": 10, + "strategy": "advanced" // 或 "basic" +} +Response: +{ + "success": true, + "data": { + "tickets": [...], + "filename": "lottery/双色球模拟号码 -10 注 -20260619-001.xlsx", + "download_url": "/api/download/lottery/双色球模拟号码 -10 注 -20260619-001.xlsx" + } +} +``` + +#### 4.3.2 获取统计数据 +``` +GET /api/statistics +Response: +{ + "success": true, + "data": { + "hot_reds": [1, 7, 13, 19, 23, 27, 31, 5, 11, 17], + "cold_reds": [2, 8, 14, 20, 26, 32, 4, 10, 16, 22], + "hot_blues": [3, 7, 11, 15, 9], + "common_odd_even": "3:3", + "common_size_ratio": "3:3", + "sum_range": {"min": 73, "max": 148, "mean": 105.5}, + "span_range": {"min": 11, "max": 32, "mean": 23.8} + } +} +``` + +#### 4.3.3 获取生成记录 +``` +GET /api/records?page=1&page_size=20 +Response: +{ + "success": true, + "data": { + "records": [ + { + "id": "uuid", + "created_at": "2026-06-19T10:30:00Z", + "strategy": "advanced", + "num_tickets": 10, + "filename": "lottery/双色球模拟号码 -10 注 -20260619-001.xlsx", + "download_url": "/api/download/..." + } + ], + "total": 150, + "page": 1, + "page_size": 20 + } +} +``` + +#### 4.3.4 下载文件 +``` +GET /api/download/{filepath} +Headers: Authorization: Bearer {token} +Response: 文件流 (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) +``` + +#### 4.3.5 获取最新数据(API 客户端用) +``` +GET /api/latest +Headers: Authorization: Bearer {token} +Response: +{ + "success": true, + "data": { + "latest_record": {...}, + "statistics": {...} + } +} +``` + +--- + +## 5. 原型与界面设计 + +### 5.1 页面流程图 + +``` +首页 + ├── 选择策略(高级/基础) + ├── 输入注数 + ├── 点击"生成号码" + │ └── 加载动画(生成中...) + │ └── 展示结果表格 + │ ├── 查看前 10 注(默认) + │ ├── 展开查看全部 + │ ├── 下载 Excel + │ └── 继续生成 + │ +历史数据 + ├── 热力图(红球/蓝球) + ├── 统计面板 + └── 历史期数列表(分页) + │ +生成记录 + ├── 记录列表(时间倒序) + ├── 搜索(按期号/日期) + └── 单条操作:下载 | 删除 + │ +系统设置 + ├── 端口配置 + ├── Token 管理 + └── 文件存储路径 +``` + +### 5.2 关键界面描述 + +#### 界面 1:首页 - 号码生成(PC 端) +``` +┌────────────────────────────────────────────────────────┐ +│ Logo 双色球号码生成系统 [历史数据] [生成记录] │ +├────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ 生成策略:○ 高级策略 ○ 基础策略 │ │ +│ │ 生成注数:[____] 注 (1-1000) │ │ +│ │ │ │ +│ │ [ 立即生成 ] │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ +│ 生成结果 │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ 序号 │ 红球 1 │ 红球 2 │ ... │ 蓝球 │ 和值 │ 奇偶比│ │ +│ │ 001 │ 05 │ 12 │ ... │ 09 │ 87 │ 3:3 │ │ +│ │ 002 │ 08 │ 15 │ ... │ 14 │ 92 │ 4:2 │ │ +│ │ ... │ ... │ ... │ ... │ ... │ ... │ ... │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ +│ [下载 Excel] [继续生成] │ +│ │ +└────────────────────────────────────────────────────────┘ +``` + +#### 界面 2:首页 - 号码生成(手机端) +``` +┌─────────────────────────┐ +│ 双色球号码生成系统 │ +├─────────────────────────┤ +│ │ +│ 策略:[高级策略 ▼] │ +│ 注数:[____] │ +│ │ +│ [ 立即生成 ] │ +│ │ +│ ┌───────────────────┐ │ +│ │ 第 001 注 │ │ +│ │ 🔴 05 12 19 23 27 │ │ +│ │ 31 🔵 09 │ │ +│ │ 和值 87 奇偶 3:3 │ │ +│ └───────────────────┘ │ +│ │ +│ [下载] [继续] │ +│ │ +├─────────────────────────┤ +│ 首页 │ 数据 │ 记录 │ +└─────────────────────────┘ +``` + +#### 界面 3:历史数据 - 热力图 +``` +┌────────────────────────────────────────────────────────┐ +│ 红球热力图(出现频次) │ +│ │ +│ 01 [████████] 120 次 17 [██████████] 135 次 │ +│ 02 [██████] 95 次 18 [████████] 122 次 │ +│ 03 [████████████] 142 次 19 [██████████] 138 次 │ +│ ... │ +│ │ +│ 蓝球热力图 │ +│ 01 [██████] 88 次 09 [████████] 118 次 │ +│ 02 [████] 75 次 10 [██████] 92 次 │ +│ ... │ +└────────────────────────────────────────────────────────┘ +``` + +--- + +## 6. 排期建议 + +### 开发排期(参考历史估算) +| 阶段 | 任务 | 预估工时 | 负责人 | +|------|------|----------|--------| +| 第一阶段 | Flask 后端框架搭建 + 集成 lottery.py | 4 小时 | 徐聪 | +| 第二阶段 | API 接口开发(生成、查询、下载) | 6 小时 | 徐聪 | +| 第三阶段 | PC 端前端页面开发(Vue3 + Element Plus) | 8 小时 | 苏锦绘 | +| 第四阶段 | 移动端适配(Vant UI) | 6 小时 | 苏锦绘 | +| 第五阶段 | 部署配置 + 端口协调 + 系统测试 | 4 小时 | 严维序 | +| 第六阶段 | 产品验收 + Bug 修复 | 4 小时 | 沈路明 | + +**总预估工时**: 32 小时(约 4 个工作日) + +### 关键依赖 +- 运维需提前确认 192.168.1.99 服务器上的可用端口 +- lottery.py 业务逻辑已审核通过,无需调整 +- 历史数据文件路径:~/Studio/lottoData/双色球历史数据.xlsx + +--- + +## 7. 风险与应对 + +### 需求风险 +| 风险 | 概率 | 影响 | 应对方案 | +|------|------|------|----------| +| 移动端适配工作量大 | 中 | 中 | 优先保证核心功能,复杂交互在 PC 端使用 | +| 用户对 Excel 格式有特殊要求 | 低 | 低 | 保留配置扩展性,支持自定义列 | + +### 技术风险 +| 风险 | 概率 | 影响 | 应对方案 | +|------|------|------|----------| +| 端口冲突 | 中 | 高 | 提前与运维确认端口表,配置文件中可调整端口 | +| 大文件下载超时 | 低 | 中 | 添加进度条,支持断点续传 | +| 并发访问性能瓶颈 | 低 | 中 | 限制单 IP 请求频率,添加请求队列 | + +### 安全风险 +| 风险 | 概率 | 影响 | 应对方案 | +|------|------|------|----------| +| 内网穿透导致外网访问 | 低 | 高 | 防火墙规则限制,仅允许内网网段 | +| Token 泄露 | 中 | 中 | 定期更换 Token,配置文件权限限制 | + +--- + +## 8. 部署与运维 + +### 8.1 服务器配置 +- **服务器**: 192.168.1.99 +- **路径**: ~/Studio/lottoData/ +- **账号**: vincent +- **监听**: 0.0.0.0:{PORT}(PORT 由运维分配) +- **进程管理**: systemd 或 supervisor + +### 8.2 端口分配要求 +请运维工程师提前维护各服务器端口使用表,本项目需分配一个未使用的端口(建议范围 8080-8099)。 + +### 8.3 防火墙规则 +```bash +# 仅允许内网访问 +iptables -A INPUT -p tcp --dport {PORT} -s 192.168.0.0/16 -j ACCEPT +iptables -A INPUT -p tcp --dport {PORT} -j DROP +``` + +### 8.4 服务启动脚本 +```bash +# systemd 配置示例 +[Unit] +Description=Lottery Web Service +After=network.target + +[Service] +Type=simple +User=vincent +WorkingDirectory=/home/vincent/Studio/lottoData +ExecStart=/home/vincent/Studio/lottoData/.venv/bin/python app.py +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +--- + +## 9. 验收标准 + +### 功能验收 +- [ ] PC 端可正常访问并完成号码生成、查看历史、下载文件 +- [ ] 手机端可正常访问并完成核心操作 +- [ ] 数据导出 API 可被远程调用 +- [ ] 生成的 Excel 文件与 lottery.py 直接运行结果一致 + +### 性能验收 +- [ ] 页面加载时间 < 2 秒(内网) +- [ ] 100 注号码生成时间 < 3 秒 +- [ ] 支持 5 个并发用户同时操作 + +### 安全验收 +- [ ] 外网无法访问服务(防火墙限制) +- [ ] 未携带 Token 无法调用 API +- [ ] 无法通过路径遍历访问非 lottery 目录文件 + +--- + +## 10. 版本历史 + +| 版本 | 日期 | 变更内容 | 作者 | +|------|------|----------|------| +| v1.0 | 2026-06-19 | 初始版本,基于 lottery.py 业务逻辑 | 沈路明 | + +--- + +## 11. 附录 + +### 11.1 数据字段定义 +**生成号码表字段**: +- 序号:数字,从 1 开始 +- 红球 1-6:数字 1-33 +- 蓝球:数字 1-16 +- 和值:6 个红球之和 +- 奇偶比:格式 "X:Y",如 "3:3" +- 大小比:格式 "X:Y",如 "3:3" +- 跨度:最大红球 - 最小红球 + +**统计信息表字段**: +- 生成时间:DateTime +- 生成策略:Text ("高级策略" 或 "基础策略") +- 生成注数:Integer +- 红球热号:Text (逗号分隔) +- 红球冷号:Text (逗号分隔) +- 蓝球热号:Text (逗号分隔) +- 最常见奇偶比:Text +- 最常见大小比:Text +- 和值范围:Text ("最小值 - 最大值") +- 跨度范围:Text ("最小值 - 最大值") + +### 11.2 配置文件示例 (config.yaml) +```yaml +server: + host: 0.0.0.0 + port: 8085 # 运维分配的端口 + +security: + api_token: "your-secret-token-here" + allowed_networks: + - "192.168.0.0/16" + +storage: + history_file: "双色球历史数据.xlsx" + output_dir: "lottery" + +display: + default_tickets: 10 + max_tickets: 1000 + tickets_per_page: 20 +``` + +--- + +**PRD 完成时间**: 2026-06-19 +**产品经理**: 沈路明 +**状态**: 待评审 \ No newline at end of file diff --git a/fetch_data.py b/fetch_data.py new file mode 100644 index 0000000..8867d75 --- /dev/null +++ b/fetch_data.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +双色球历史数据抓取脚本 +从 https://www.55128.cn/kjh/fcssq-history-120.htm 抓取数据 +更新「双色球历史数据.xlsx」文件 +""" + +import requests +from bs4 import BeautifulSoup +import pandas as pd +from datetime import datetime +import os +import re + +# 数据源 URL +URL = "https://www.55128.cn/kjh/fcssq-history-120.htm" +# 输出文件路径 +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +OUTPUT_FILE = os.path.join(SCRIPT_DIR, "双色球历史数据.xlsx") + +# 请求头,模拟浏览器 +HEADERS = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", +} + + +def fetch_lottery_data(): + """抓取双色球历史数据""" + print(f"[{datetime.now()}] 开始抓取数据...") + + try: + response = requests.get(URL, headers=HEADERS, timeout=30) + response.raise_for_status() + response.encoding = "utf-8" + + soup = BeautifulSoup(response.text, "html.parser") + + # 查找表格数据 + table = soup.find("table") + if not table: + print("错误:未找到数据表格") + return None + + # 解析表格 + data_rows = [] + rows = table.find_all("tr") + + for row in rows: + cols = row.find_all(["td", "th"]) + if len(cols) >= 8: + try: + row_data = [col.get_text(strip=True) for col in cols] + data_rows.append(row_data) + except Exception as e: + continue + + if not data_rows: + print("错误:未解析到任何数据") + return None + + print(f"成功解析 {len(data_rows)} 条数据") + return data_rows + + except requests.exceptions.RequestException as e: + print(f"网络请求错误:{e}") + return None + except Exception as e: + print(f"解析错误:{e}") + return None + + +def save_to_excel(data_rows): + """保存数据到 Excel 文件""" + if not data_rows: + print("无数据可保存") + return False + + try: + # 创建 DataFrame + num_cols = min(len(row) for row in data_rows) + data_rows = [row[:num_cols] for row in data_rows] + + # 列名定义(最多 11 列) + columns = ["期号", "开奖日期", "红球 1", "红球 2", "红球 3", "红球 4", "红球 5", "红球 6", "蓝球", "特别号", "奖池"] + + # 如果列数不匹配,使用通用列名 + actual_columns = columns[:num_cols] if num_cols <= len(columns) else [f"列{i+1}" for i in range(num_cols)] + + df = pd.DataFrame(data_rows, columns=actual_columns) + + # 保存为 Excel + df.to_excel(OUTPUT_FILE, index=False, engine="openpyxl") + + print(f"[{datetime.now()}] 数据已保存到:{OUTPUT_FILE}") + print(f"共保存 {len(df)} 条记录") + return True + + except Exception as e: + print(f"保存 Excel 错误:{e}") + return False + + +def main(): + """主函数""" + print("=" * 60) + print("双色球历史数据抓取工具") + print("=" * 60) + + # 抓取数据 + data = fetch_lottery_data() + + if data: + # 保存数据 + success = save_to_excel(data) + if success: + print("=" * 60) + print("任务完成!") + print("=" * 60) + return 0 + else: + print("保存失败") + return 1 + else: + print("抓取失败") + return 1 + + +if __name__ == "__main__": + exit(main()) \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..7561421 --- /dev/null +++ b/index.html @@ -0,0 +1,1171 @@ + + + + + + 🎯 双色球号码生成系统 + + + + + +
+ + +
+

🎯 双色球号码生成系统

+

基于历史数据分析 · 支持 PC 端和移动端

+
+ + + + + +
+
+
+
🎲 生成双色球号码
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ 📋 生成结果 + 10 注 +
+
+ + +
+
+
+
+ + +
+
📊 数据概览
+
+
加载统计数据中...
+
+
+
+ + +
+
+
📊 双色球历史开奖数据
+ +
+
加载历史数据中...
+
+ +
+
+ + +
+
+
📋 生成记录
+
+
加载记录中...
+
+ +
+
+ + +
+
+
📈 号码统计分析
+
+
加载统计中...
+
+
+
+
+ + +
+
+
+ 🎲 生成 +
+
+ 📊 历史 +
+
+ 📋 记录 +
+
+ 📈 统计 +
+
+
+ + + + + diff --git a/lottery.py b/lottery.py new file mode 100644 index 0000000..42662e5 --- /dev/null +++ b/lottery.py @@ -0,0 +1,1189 @@ +import pandas as pd +import numpy as np +import random +from datetime import datetime, timedelta +import os +from collections import Counter +import re +import traceback + + +class DoubleColorBallGenerator: + def __init__(self, history_file="双色球历史数据.xlsx", config=None): + """ + 初始化双色球号码生成器 + + Args: + history_file: 历史数据文件路径 + config: 配置参数 + """ + self.history_file = history_file + self.history_data = None + self.red_stats = None + self.blue_stats = None + self.features_stats = None + + # 默认配置 + self.config = config or { + 'hot_red_count': 15, + 'cold_red_count': 10, + 'hot_blue_count': 8, + 'hot_blue_probability': 0.7, + 'max_adjustment_attempts': 20, + 'hot_red_display_count': 10, + 'cold_red_display_count': 10, + 'hot_blue_display_count': 5, + 'min_tickets': 1, + 'max_tickets': 1000 + } + + def load_history_data(self): + """加载历史数据""" + try: + # 检查文件是否存在 + if not os.path.exists(self.history_file): + print(f"错误: 文件 {self.history_file} 不存在") + return False + + # 读取Excel文件 + print(f"正在读取文件: {self.history_file}") + try: + self.history_data = pd.read_excel(self.history_file) + except Exception as excel_error: + print(f"读取Excel文件失败: {excel_error}") + return False + + # 检查数据是否为空 + if self.history_data.empty: + print("错误: 历史数据文件为空") + return False + + print(f"加载成功,共{len(self.history_data)}条历史记录") + print(f"数据列: {list(self.history_data.columns)}") + + # 检查是否包含必要的列 + if '号码' not in self.history_data.columns: + print("错误: 历史数据文件缺少'号码'列") + return False + + # 解析号码列 + def parse_numbers(row): + """解析单行号码数据""" + try: + # 处理号码字符串 - 直接转换为字符串然后分割 + if pd.isna(row['号码']): + return [], 0 + + numbers_str = str(row['号码']) + + # 使用正则表达式提取所有数字 + number_list = re.findall(r'\d+', numbers_str) + + if len(number_list) >= 7: + try: + red_balls = [int(x) for x in number_list[:6]] + blue_ball = int(number_list[6]) + # 验证号码范围 + if all(1 <= ball <= 33 for ball in red_balls) and 1 <= blue_ball <= 16: + return red_balls, blue_ball + else: + print(f"警告: 号码范围异常: {red_balls} + {blue_ball}") + return [], 0 + except ValueError: + # 如果转换失败,尝试其他解析方式 + # 替换各种空格字符为单个空格 + cleaned_str = re.sub(r'\s+', ' ', numbers_str.strip()) + parts = cleaned_str.split() + if len(parts) >= 7: + try: + red_balls = [int(x) for x in parts[:6]] + blue_ball = int(parts[6]) + # 验证号码范围 + if all(1 <= ball <= 33 for ball in red_balls) and 1 <= blue_ball <= 16: + return red_balls, blue_ball + else: + print(f"警告: 号码范围异常: {red_balls} + {blue_ball}") + return [], 0 + except ValueError: + return [], 0 + return [], 0 + return [], 0 + except Exception as e: + print(f"解析号码时出错: {e}") + return [], 0 + + # 应用解析函数 + parsed = self.history_data.apply(parse_numbers, axis=1) + self.history_data['红球'] = [x[0] for x in parsed] + self.history_data['蓝球'] = [x[1] for x in parsed] + + # 打印解析成功的数量 + valid_count = sum(1 for x in parsed if len(x[0]) == 6 and x[1] > 0) + print(f"成功解析 {valid_count} 条号码数据") + + # 检查是否有足够的有效数据 + if valid_count < 10: + print("警告: 有效历史数据较少,可能影响分析结果") + + # 计算统计数据 + try: + self._calculate_statistics() + except Exception as stats_error: + print(f"计算统计数据失败: {stats_error}") + print(traceback.format_exc()) + return False + + return True + + except Exception as e: + print(f"加载历史数据失败: {e}") + print(traceback.format_exc()) + return False + + def _calculate_statistics(self): + """计算统计数据""" + if self.history_data is None or len(self.history_data) == 0: + print("警告: 没有历史数据可供统计") + return + + # 1. 号码频次统计 + red_ball_counts = Counter() + blue_ball_counts = Counter() + sum_values = [] + span_values = [] + + # 一次性遍历收集所有数据,减少循环次数 + for _, row in self.history_data.iterrows(): + # 处理红球 + red_balls = row.get('红球') + if isinstance(red_balls, list) and len(red_balls) == 6: + # 统计红球频次 + red_ball_counts.update(red_balls) + # 计算和值 + sum_values.append(sum(red_balls)) + # 计算跨度 + span_values.append(max(red_balls) - min(red_balls)) + + # 处理蓝球 + blue_ball = row.get('蓝球') + if blue_ball and blue_ball > 0: + blue_ball_counts[blue_ball] += 1 + + self.red_stats = red_ball_counts + self.blue_stats = blue_ball_counts + + # 2. 特征统计 + self.features_stats = { + 'odd_even_ratio': {}, + 'size_ratio': {}, + 'sum_range': {}, + 'span_range': {} + } + + # 统计奇偶比 - 修复解析 + if '奇偶比' in self.history_data.columns: + odd_even_ratio_counts = Counter() + for ratio in self.history_data['奇偶比']: + if pd.isna(ratio): + continue + ratio_str = str(ratio) + # 提取数字部分 + parts = re.findall(r'\d+', ratio_str) + if len(parts) >= 2: + ratio_key = f"{parts[0]}:{parts[1]}" + odd_even_ratio_counts[ratio_key] += 1 + + if odd_even_ratio_counts: + total_count = sum(odd_even_ratio_counts.values()) + self.features_stats['odd_even_ratio'] = { + k: v/total_count for k, v in odd_even_ratio_counts.items() + } + + # 统计大小比 - 修复解析 + if '大小比' in self.history_data.columns: + size_ratio_counts = Counter() + for ratio in self.history_data['大小比']: + if pd.isna(ratio): + continue + ratio_str = str(ratio) + parts = re.findall(r'\d+', ratio_str) + if len(parts) >= 2: + ratio_key = f"{parts[0]}:{parts[1]}" + size_ratio_counts[ratio_key] += 1 + + if size_ratio_counts: + total_count = sum(size_ratio_counts.values()) + self.features_stats['size_ratio'] = { + k: v/total_count for k, v in size_ratio_counts.items() + } + + # 计算和值统计 + if sum_values: + sum_array = np.array(sum_values) + self.features_stats['sum_range']['min'] = int(sum_array.min()) + self.features_stats['sum_range']['max'] = int(sum_array.max()) + self.features_stats['sum_range']['mean'] = float(sum_array.mean()) + self.features_stats['sum_range']['std'] = float(sum_array.std()) + + # 计算跨度统计 + if span_values: + span_array = np.array(span_values) + self.features_stats['span_range']['min'] = int(span_array.min()) + self.features_stats['span_range']['max'] = int(span_array.max()) + self.features_stats['span_range']['mean'] = float(span_array.mean()) + self.features_stats['span_range']['std'] = float(span_array.std()) + + def get_hot_red_balls(self, n=10): + """获取热号红球""" + if not self.red_stats: + print("警告: 红球统计数据为空") + # 返回随机红球作为默认,避免固定范围 + return random.sample(range(1, 34), min(n, 33)) + + # 按出现频率排序 + sorted_reds = sorted(self.red_stats.items(), + key=lambda x: x[1], reverse=True) + result = [x[0] for x in sorted_reds[:n]] + + # 如果结果不够n个,用其他球补全 + if len(result) < n: + all_balls = list(range(1, 34)) + missing = [x for x in all_balls if x not in result] + result.extend(random.sample( + missing, min(n - len(result), len(missing)))) + + return result + + def get_cold_red_balls(self, n=10): + """获取冷号红球""" + if not self.red_stats: + print("警告: 红球统计数据为空") + # 返回随机红球作为默认,避免固定范围 + return random.sample(range(1, 34), min(n, 33)) + + # 按出现频率排序(升序) + sorted_reds = sorted(self.red_stats.items(), key=lambda x: x[1]) + result = [x[0] for x in sorted_reds[:n]] + + # 如果结果不够n个,用其他球补全 + if len(result) < n: + all_balls = list(range(1, 34)) + missing = [x for x in all_balls if x not in result] + result.extend(random.sample( + missing, min(n - len(result), len(missing)))) + + return result + + def get_hot_blue_balls(self, n=5): + """获取热号蓝球""" + if not self.blue_stats: + print("警告: 蓝球统计数据为空") + # 返回随机蓝球作为默认,避免固定范围 + return random.sample(range(1, 17), min(n, 16)) + + sorted_blues = sorted(self.blue_stats.items(), + key=lambda x: x[1], reverse=True) + result = [x[0] for x in sorted_blues[:n]] + + # 如果结果不够n个,用其他球补全 + if len(result) < n: + all_balls = list(range(1, 17)) + missing = [x for x in all_balls if x not in result] + result.extend(random.sample( + missing, min(n - len(result), len(missing)))) + + return result + + def parse_ratio(self, ratio_str): + """解析奇偶比/大小比字符串""" + if pd.isna(ratio_str): + return 3, 3 + + ratio_str = str(ratio_str) + # 提取数字 + parts = re.findall(r'\d+', ratio_str) + if len(parts) >= 2: + odd = int(parts[0]) + even = int(parts[1]) + return odd, even + return 3, 3 # 默认3:3 + + def _adjust_balls_by_criteria(self, red_balls, current_value, target_value, get_balls_to_remove, get_candidates, recalculate_current): + """通用的号码调整方法 + + Args: + red_balls: 当前红球集合 + current_value: 当前值 + target_value: 目标值 + get_balls_to_remove: 获取要移除的球的函数 + get_candidates: 获取候选球的函数 + recalculate_current: 重新计算当前值的函数 + + Returns: + 调整后的红球集合 + """ + attempts = 0 + max_attempts = self.config['max_adjustment_attempts'] + + while abs(current_value - target_value) > 1 and attempts < max_attempts: + balls_to_remove = get_balls_to_remove(red_balls) + candidates = get_candidates(red_balls) + + if balls_to_remove and candidates: + # 移除一个球并添加一个候选球 + ball_to_remove = random.choice(balls_to_remove) + ball_to_add = random.choice(candidates) + + red_balls.remove(ball_to_remove) + red_balls.append(ball_to_add) + + # 重新计算当前值 + current_value = recalculate_current(red_balls) + + attempts += 1 + + return red_balls + + def _select_hot_cold_balls(self): + """选择热号和冷号组合""" + red_balls = set() + + # 获取热号和冷号 + hot_reds = self.get_hot_red_balls(self.config['hot_red_count']) + cold_reds = self.get_cold_red_balls(self.config['cold_red_count']) + + # 增加随机性:热号数量在2-4之间随机 + hot_count = random.randint(2, 4) + cold_count = 6 - hot_count + + # 从热号中随机选择(去除已选的) + available_hot = [x for x in hot_reds if x not in red_balls] + if available_hot and hot_count > 0: + # 增加随机性:不总是选择前几个热号 + if len(available_hot) > hot_count: + # 随机打乱热号顺序后选择 + random.shuffle(available_hot) + selected = random.sample( + available_hot, min(hot_count, len(available_hot))) + red_balls.update(selected) + + # 从冷号中随机选择 + available_cold = [x for x in cold_reds if x not in red_balls] + if available_cold and cold_count > 0: + selected = random.sample(available_cold, min( + cold_count, len(available_cold))) + red_balls.update(selected) + + # 如果还不够6个,用随机数补全 + while len(red_balls) < 6: + ball = random.randint(1, 33) + red_balls.add(ball) + + return list(red_balls) + + def _adjust_odd_even_ratio(self, red_balls): + """调整奇偶比""" + if self.features_stats.get('odd_even_ratio'): + common_ratios = list(self.features_stats['odd_even_ratio'].keys()) + if common_ratios: + # 增加随机性:80%概率选择最常见的奇偶比,20%随机选择 + if random.random() < 0.8: + target_ratio = max(self.features_stats['odd_even_ratio'], + key=self.features_stats['odd_even_ratio'].get) + else: + target_ratio = random.choice(common_ratios) + target_odd, target_even = self.parse_ratio(target_ratio) + + # 调整当前组合的奇偶比 + current_odd = sum(1 for x in red_balls if x % 2 == 1) + + def get_balls_to_remove_odd_excess(balls): + return [x for x in balls if x % 2 == 1] + + def get_candidates_odd_excess(balls): + return [x for x in range(1, 34) if x % 2 == 0 and x not in balls] + + def get_balls_to_remove_even_excess(balls): + return [x for x in balls if x % 2 == 0] + + def get_candidates_even_excess(balls): + return [x for x in range(1, 34) if x % 2 == 1 and x not in balls] + + def recalculate_odd(balls): + return sum(1 for x in balls if x % 2 == 1) + + if current_odd > target_odd: + # 减少奇数,增加偶数 + red_balls = self._adjust_balls_by_criteria( + red_balls, current_odd, target_odd, + get_balls_to_remove_odd_excess, + get_candidates_odd_excess, + recalculate_odd + ) + elif current_odd < target_odd: + # 增加奇数,减少偶数 + red_balls = self._adjust_balls_by_criteria( + red_balls, current_odd, target_odd, + get_balls_to_remove_even_excess, + get_candidates_even_excess, + recalculate_odd + ) + return red_balls + + def _adjust_size_ratio(self, red_balls): + """调整大小比""" + if self.features_stats.get('size_ratio'): + common_size_ratios = list(self.features_stats['size_ratio'].keys()) + if common_size_ratios: + # 增加随机性:80%概率选择最常见的大小比,20%随机选择 + if random.random() < 0.8: + target_size_ratio = max(self.features_stats['size_ratio'], + key=self.features_stats['size_ratio'].get) + else: + target_size_ratio = random.choice(common_size_ratios) + target_small, target_large = self.parse_ratio( + target_size_ratio) + + current_small = sum(1 for x in red_balls if x <= 16) + + def get_balls_to_remove_small_excess(balls): + return [x for x in balls if x <= 16] + + def get_candidates_small_excess(balls): + return [x for x in range(17, 34) if x not in balls] + + def get_balls_to_remove_large_excess(balls): + return [x for x in balls if x > 16] + + def get_candidates_large_excess(balls): + return [x for x in range(1, 17) if x not in balls] + + def recalculate_small(balls): + return sum(1 for x in balls if x <= 16) + + if current_small > target_small: + # 减少小数,增加大数 + red_balls = self._adjust_balls_by_criteria( + red_balls, current_small, target_small, + get_balls_to_remove_small_excess, + get_candidates_small_excess, + recalculate_small + ) + elif current_small < target_small: + # 增加小数,减少大数 + red_balls = self._adjust_balls_by_criteria( + red_balls, current_small, target_small, + get_balls_to_remove_large_excess, + get_candidates_large_excess, + recalculate_small + ) + return red_balls + + def _adjust_sum_range(self, red_balls): + """调整和值范围""" + if self.features_stats.get('sum_range') and 'mean' in self.features_stats['sum_range']: + current_sum = sum(red_balls) + target_mean = self.features_stats['sum_range']['mean'] + target_std = self.features_stats['sum_range']['std'] + + # 增加随机性:90%概率调整到正常范围,10%保持原样 + if random.random() < 0.9: + # 如果和值偏离平均值太多,进行调整 + lower_bound = target_mean - target_std + upper_bound = target_mean + target_std + + attempts = 0 + while (current_sum < lower_bound or current_sum > upper_bound) and attempts < 20: + if current_sum < lower_bound: + # 和值太小,用大数替换小数 + small_balls = [x for x in red_balls if x <= 10] + large_candidates = [x for x in range( + 25, 34) if x not in red_balls] + if small_balls and large_candidates: + red_balls.remove(random.choice(small_balls)) + red_balls.append(random.choice(large_candidates)) + elif current_sum > upper_bound: + # 和值太大,用小数替换大数 + large_balls = [x for x in red_balls if x >= 25] + small_candidates = [x for x in range( + 1, 12) if x not in red_balls] + if large_balls and small_candidates: + red_balls.remove(random.choice(large_balls)) + red_balls.append(random.choice(small_candidates)) + + current_sum = sum(red_balls) + attempts += 1 + return red_balls + + def _adjust_span_range(self, red_balls): + """调整跨度范围""" + if self.features_stats.get('span_range') and 'mean' in self.features_stats['span_range']: + current_span = max(red_balls) - min(red_balls) + span_mean = self.features_stats['span_range']['mean'] + span_std = self.features_stats['span_range']['std'] + + # 增加随机性:90%概率调整到正常范围,10%保持原样 + if random.random() < 0.9: + # 跨度在平均值±标准差范围内 + span_lower = span_mean - span_std + span_upper = span_mean + span_std + + attempts = 0 + while (current_span < span_lower or current_span > span_upper) and attempts < 20: + if current_span < span_lower: + # 跨度太小,扩大范围 + # 尝试替换最小或最大的球 + if random.choice([True, False]): + # 替换最小球为更小的数 + min_ball = min(red_balls) + candidates = [x for x in range( + 1, min_ball) if x not in red_balls] + if candidates: + red_balls.remove(min_ball) + red_balls.append(random.choice(candidates)) + else: + # 替换最大球为更大的数 + max_ball = max(red_balls) + candidates = [x for x in range( + max_ball + 1, 34) if x not in red_balls] + if candidates: + red_balls.remove(max_ball) + red_balls.append(random.choice(candidates)) + elif current_span > span_upper: + # 跨度太大,缩小范围 + # 随机替换一个球,使其更靠近中心 + center = sum(red_balls) / 6 + ball_to_replace = random.choice(red_balls) + + # 选择离中心更近的候选球 + candidates = [x for x in range( + 1, 34) if x not in red_balls] + if candidates: + # 找到离中心最近的候选球 + closest = min( + candidates, key=lambda x: abs(x - center)) + red_balls.remove(ball_to_replace) + red_balls.append(closest) + + current_span = max(red_balls) - min(red_balls) + attempts += 1 + return red_balls + + def _select_blue_ball(self): + """选择蓝球""" + hot_blues = self.get_hot_blue_balls(self.config['hot_blue_count']) + if hot_blues and random.random() < self.config['hot_blue_probability']: # 基于配置的概率选择热号蓝球 + blue_ball = random.choice(hot_blues) + else: + blue_ball = random.randint(1, 16) + return blue_ball + + def generate_single_ticket_advanced(self): + """生成单注号码(高级策略)""" + # 选择热号和冷号组合 + red_balls = self._select_hot_cold_balls() + + # 调整奇偶比 + red_balls = self._adjust_odd_even_ratio(red_balls) + + # 调整大小比 + red_balls = self._adjust_size_ratio(red_balls) + + # 调整和值范围 + red_balls = self._adjust_sum_range(red_balls) + + # 调整跨度范围 + red_balls = self._adjust_span_range(red_balls) + + # 对红球排序 + red_balls.sort() + + # 选择蓝球 + blue_ball = self._select_blue_ball() + + return red_balls, blue_ball + + def generate_single_ticket_basic(self): + """生成单注号码(基础策略)""" + # 完全随机生成 + red_balls = sorted(random.sample(range(1, 34), 6)) + blue_ball = random.randint(1, 16) + return red_balls, blue_ball + + def generate_multiple_tickets(self, num_tickets, strategy="advanced"): + """生成多注号码 + + Args: + num_tickets: 注数 + strategy: 生成策略,可选 "advanced"(高级) 或 "basic"(基础) + """ + tickets = [] + generated_numbers = set() # 用于存储已生成的号码组合,避免重复 + + failed_attempts = 0 + max_attempts = num_tickets * 10 # 最多尝试次数 + max_attempts_per_ticket = 100 # 每注最多尝试次数 + + # 预计算一些值,避免重复计算 + use_advanced = strategy == "advanced" + + for i in range(num_tickets): + attempts = 0 + success = False + + while not success and attempts < max_attempts_per_ticket: + try: + if use_advanced: + reds, blue = self.generate_single_ticket_advanced() + else: + reds, blue = self.generate_single_ticket_basic() + + # 验证生成的号码 + if len(reds) == 6: + # 检查号码范围 + if all(1 <= x <= 33 for x in reds) and 1 <= blue <= 16: + # 检查是否有重复号码 + if len(set(reds)) == 6: + # 生成唯一键,用于检查重复 + ticket_key = (tuple(sorted(reds)), blue) + if ticket_key not in generated_numbers: + # 计算一次和值,避免重复计算 + sum_reds = sum(reds) + # 计算奇偶比 + odd_count = sum(1 for x in reds if x % 2 == 1) + even_count = 6 - odd_count + # 计算大小比 + large_count = sum(1 for x in reds if x > 16) + small_count = 6 - large_count + # 计算跨度 + span = max(reds) - min(reds) + + tickets.append({ + '序号': i + 1, + '红球1': reds[0], + '红球2': reds[1], + '红球3': reds[2], + '红球4': reds[3], + '红球5': reds[4], + '红球6': reds[5], + '蓝球': blue, + '和值': sum_reds, + '奇偶比': f"{odd_count}:{even_count}", + '大小比': f"{large_count}:{small_count}", + '跨度': span + }) + generated_numbers.add(ticket_key) + success = True + + attempts += 1 + failed_attempts += 1 + + if failed_attempts > max_attempts: + print(f"警告: 生成失败次数过多,可能已生成{len(tickets)}注") + if len(tickets) > 0: + return pd.DataFrame(tickets) + else: + # 返回基础随机生成的号码 + print("切换到基础随机策略") + return self.generate_multiple_tickets(num_tickets, "basic") + + except Exception as e: + attempts += 1 + failed_attempts += 1 + if attempts % 50 == 0: # 减少错误打印频率 + print(f"生成第{i+1}注时出错,尝试{attempts}次: {e}") + + # 确保返回的DataFrame结构正确 + if tickets: + return pd.DataFrame(tickets) + else: + # 如果没有生成成功,返回空的DataFrame + return pd.DataFrame(columns=['序号', '红球1', '红球2', '红球3', '红球4', '红球5', '红球6', '蓝球', '和值', '奇偶比', '大小比', '跨度']) + + def save_to_excel(self, tickets_df, num_tickets, strategy="advanced"): + """保存生成的号码到Excel文件 + + Args: + tickets_df: 号码DataFrame + num_tickets: 注数 + strategy: 生成策略 + """ + try: + # 检查DataFrame是否为空 + if tickets_df.empty: + print("错误: 没有号码数据可保存") + return None + + # 确保保存目录存在 + save_dir = "./lottery" + if not os.path.exists(save_dir): + try: + os.makedirs(save_dir) + print(f"创建保存目录: {save_dir}") + except Exception as dir_error: + print(f"创建保存目录失败: {dir_error}") + return None + + # 检查目录是否可写 + if not os.access(save_dir, os.W_OK): + print(f"错误: 目录 {save_dir} 没有写入权限") + return None + + # 生成文件名 + today = datetime.now().strftime("%Y%m%d") + base_filename = f"双色球模拟号码-{num_tickets}注-{today}" + + # 检查文件是否存在,避免覆盖 + counter = 1 + filename = f"{save_dir}/{base_filename}-{counter:03d}.xlsx" + while os.path.exists(filename): + counter += 1 + filename = f"{save_dir}/{base_filename}-{counter:03d}.xlsx" + + print(f"正在保存到文件: {filename}") + + # 保存到Excel + try: + with pd.ExcelWriter(filename, engine='openpyxl') as writer: + # 写入生成号码 + tickets_df.to_excel(writer, sheet_name='生成号码', index=False) + + # 添加统计信息sheet + stats_data = { + '统计项': ['生成时间', '生成策略', '生成注数'], + '统计值': [ + datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "高级策略" if strategy == "advanced" else "基础策略", + num_tickets + ] + } + + # 添加红球热号统计 + if self.red_stats: + try: + hot_reds = self.get_hot_red_balls(self.config['hot_red_display_count']) + stats_data['统计项'].append(f'红球热号(前{self.config["hot_red_display_count"]})') + stats_data['统计值'].append(', '.join(map(str, hot_reds))) + except Exception as e: + print(f"添加红球热号统计失败: {e}") + + # 添加红球冷号统计 + if self.red_stats: + try: + cold_reds = self.get_cold_red_balls(self.config['cold_red_display_count']) + stats_data['统计项'].append(f'红球冷号(前{self.config["cold_red_display_count"]})') + stats_data['统计值'].append(', '.join(map(str, cold_reds))) + except Exception as e: + print(f"添加红球冷号统计失败: {e}") + + # 添加蓝球热号统计 + if self.blue_stats: + try: + hot_blues = self.get_hot_blue_balls(self.config['hot_blue_display_count']) + stats_data['统计项'].append(f'蓝球热号(前{self.config["hot_blue_display_count"]})') + stats_data['统计值'].append(', '.join(map(str, hot_blues))) + except Exception as e: + print(f"添加蓝球热号统计失败: {e}") + + # 添加奇偶比统计 + if self.features_stats.get('odd_even_ratio'): + try: + common_odd_even = max(self.features_stats['odd_even_ratio'], + key=self.features_stats['odd_even_ratio'].get) + stats_data['统计项'].append('最常见奇偶比') + stats_data['统计值'].append(f"{common_odd_even}") + except Exception as e: + print(f"添加奇偶比统计失败: {e}") + + # 添加大小比统计 + if self.features_stats.get('size_ratio'): + try: + common_size = max(self.features_stats['size_ratio'], + key=self.features_stats['size_ratio'].get) + stats_data['统计项'].append('最常见大小比') + stats_data['统计值'].append(f"{common_size}") + except Exception as e: + print(f"添加大小比统计失败: {e}") + + # 添加和值范围 + if self.features_stats.get('sum_range'): + try: + sum_range = self.features_stats['sum_range'] + if 'min' in sum_range and 'max' in sum_range: + stats_data['统计项'].append('和值范围') + stats_data['统计值'].append( + f"{sum_range['min']}-{sum_range['max']}") + except Exception as e: + print(f"添加和值范围统计失败: {e}") + + # 添加跨度范围 + if self.features_stats.get('span_range'): + try: + span_range = self.features_stats['span_range'] + if 'min' in span_range and 'max' in span_range: + stats_data['统计项'].append('跨度范围') + stats_data['统计值'].append( + f"{span_range['min']}-{span_range['max']}") + except Exception as e: + print(f"添加跨度范围统计失败: {e}") + + # 写入统计信息 + try: + stats_df = pd.DataFrame(stats_data) + stats_df.to_excel(writer, sheet_name='统计信息', index=False) + except Exception as e: + print(f"写入统计信息失败: {e}") + + # 自动调整列宽 + try: + for sheet_name in writer.sheets: + worksheet = writer.sheets[sheet_name] + for column in worksheet.columns: + max_length = 0 + column_letter = column[0].column_letter + for cell in column: + try: + if cell.value and len(str(cell.value)) > max_length: + max_length = len(str(cell.value)) + except: + pass + adjusted_width = min(max_length + 2, 50) + worksheet.column_dimensions[column_letter].width = adjusted_width + except Exception as e: + print(f"调整列宽失败: {e}") + + except Exception as excel_error: + print(f"Excel写入失败: {excel_error}") + # 尝试删除可能的部分文件 + if os.path.exists(filename): + try: + os.remove(filename) + except: + pass + return None + + # 检查文件是否成功创建 + if not os.path.exists(filename): + print("错误: 文件创建失败") + return None + + # 检查文件大小 + if os.path.getsize(filename) < 100: + print("警告: 文件大小异常,可能保存不完整") + + print(f"✓ 号码已成功保存到文件: {filename}") + print(f"✓ 文件路径: {os.path.abspath(filename)}") + return filename + + except Exception as e: + print(f"保存文件失败: {e}") + print(traceback.format_exc()) + return None + + def display_statistics(self): + """显示统计信息""" + if not self.features_stats: + print("警告: 统计信息为空") + return + + print("\n" + "="*60) + print("双色球历史数据统计信息") + print("="*60) + + # 显示记录数量 + if self.history_data is not None: + print(f"历史记录总数: {len(self.history_data)}条") + + # 红球热号 + hot_reds = self.get_hot_red_balls(self.config['hot_red_display_count']) + print(f"红球热号(前{self.config['hot_red_display_count']}): {', '.join(map(str, hot_reds))}") + + # 红球冷号 + cold_reds = self.get_cold_red_balls(self.config['cold_red_display_count']) + print(f"红球冷号(前{self.config['cold_red_display_count']}): {', '.join(map(str, cold_reds))}") + + # 蓝球热号 + hot_blues = self.get_hot_blue_balls(self.config['hot_blue_display_count']) + print(f"蓝球热号(前{self.config['hot_blue_display_count']}): {', '.join(map(str, hot_blues))}") + + # 常见奇偶比 + if self.features_stats.get('odd_even_ratio'): + common_odd_even = max(self.features_stats['odd_even_ratio'], + key=self.features_stats['odd_even_ratio'].get) + prob = self.features_stats['odd_even_ratio'][common_odd_even] + print(f"最常见奇偶比: {common_odd_even} (概率: {prob:.2%})") + + # 常见大小比 + if self.features_stats.get('size_ratio'): + common_size = max(self.features_stats['size_ratio'], + key=self.features_stats['size_ratio'].get) + prob = self.features_stats['size_ratio'][common_size] + print(f"最常见大小比: {common_size} (概率: {prob:.2%})") + + # 和值统计 + if self.features_stats.get('sum_range'): + sum_range = self.features_stats['sum_range'] + if 'min' in sum_range and 'max' in sum_range: + print(f"和值范围: {sum_range['min']} - {sum_range['max']}") + if 'mean' in sum_range: + print(f"和值平均值: {sum_range['mean']:.1f}") + if 'std' in sum_range: + print(f"和值标准差: {sum_range['std']:.1f}") + + # 跨度统计 + if self.features_stats.get('span_range'): + span_range = self.features_stats['span_range'] + if 'min' in span_range and 'max' in span_range: + print(f"跨度范围: {span_range['min']} - {span_range['max']}") + if 'mean' in span_range: + print(f"跨度平均值: {span_range['mean']:.1f}") + if 'std' in span_range: + print(f"跨度标准差: {span_range['std']:.1f}") + + print("="*60) + + def run_tests(self): + """运行测试用例,验证代码正确性""" + print("\n" + "="*60) + print("开始运行测试用例") + print("="*60) + + test_results = [] + + # 测试1: 验证号码范围 + def test_number_ranges(): + print("\n测试1: 验证号码范围") + try: + # 生成多注号码并验证范围 + for _ in range(100): + # 测试高级策略 + reds, blue = self.generate_single_ticket_advanced() + assert len(reds) == 6, f"红球数量错误: {len(reds)}" + assert all(1 <= x <= 33 for x in reds), f"红球范围错误: {reds}" + assert 1 <= blue <= 16, f"蓝球范围错误: {blue}" + assert len(set(reds)) == 6, f"红球重复: {reds}" + + # 测试基础策略 + reds_basic, blue_basic = self.generate_single_ticket_basic() + assert len(reds_basic) == 6, f"基础策略红球数量错误: {len(reds_basic)}" + assert all(1 <= x <= 33 for x in reds_basic), f"基础策略红球范围错误: {reds_basic}" + assert 1 <= blue_basic <= 16, f"基础策略蓝球范围错误: {blue_basic}" + assert len(set(reds_basic)) == 6, f"基础策略红球重复: {reds_basic}" + + print("✓ 号码范围测试通过") + return True + except Exception as e: + print(f"✗ 号码范围测试失败: {e}") + return False + + # 测试2: 验证多注生成 + def test_multiple_tickets(): + print("\n测试2: 验证多注生成") + try: + # 测试生成不同数量的号码 + for num in [1, 5, 10, 50]: + df = self.generate_multiple_tickets(num, "advanced") + assert len(df) == num, f"高级策略生成数量错误: 期望{num}, 实际{len(df)}" + + df_basic = self.generate_multiple_tickets(num, "basic") + assert len(df_basic) == num, f"基础策略生成数量错误: 期望{num}, 实际{len(df_basic)}" + + print("✓ 多注生成测试通过") + return True + except Exception as e: + print(f"✗ 多注生成测试失败: {e}") + return False + + # 测试3: 验证统计信息 + def test_statistics(): + print("\n测试3: 验证统计信息") + try: + # 验证统计数据结构 + if self.features_stats: + assert 'odd_even_ratio' in self.features_stats, "缺少奇偶比统计" + assert 'size_ratio' in self.features_stats, "缺少大小比统计" + assert 'sum_range' in self.features_stats, "缺少和值范围统计" + assert 'span_range' in self.features_stats, "缺少跨度范围统计" + + # 验证热号冷号获取 + hot_reds = self.get_hot_red_balls(10) + assert len(hot_reds) == 10, f"红球热号数量错误: {len(hot_reds)}" + + cold_reds = self.get_cold_red_balls(10) + assert len(cold_reds) == 10, f"红球冷号数量错误: {len(cold_reds)}" + + hot_blues = self.get_hot_blue_balls(5) + assert len(hot_blues) == 5, f"蓝球热号数量错误: {len(hot_blues)}" + + print("✓ 统计信息测试通过") + return True + except Exception as e: + print(f"✗ 统计信息测试失败: {e}") + return False + + # 测试4: 验证配置参数 + def test_configuration(): + print("\n测试4: 验证配置参数") + try: + # 验证配置参数存在 + required_configs = [ + 'hot_red_count', 'cold_red_count', 'hot_blue_count', + 'hot_blue_probability', 'max_adjustment_attempts', + 'hot_red_display_count', 'cold_red_display_count', + 'hot_blue_display_count', 'min_tickets', 'max_tickets' + ] + + for config in required_configs: + assert config in self.config, f"缺少配置参数: {config}" + + print("✓ 配置参数测试通过") + return True + except Exception as e: + print(f"✗ 配置参数测试失败: {e}") + return False + + # 运行所有测试 + test_results.append(test_number_ranges()) + test_results.append(test_multiple_tickets()) + test_results.append(test_statistics()) + test_results.append(test_configuration()) + + # 显示测试结果 + print("\n" + "="*60) + print("测试结果汇总") + print("="*60) + + passed = sum(test_results) + total = len(test_results) + + print(f"通过测试: {passed}/{total}") + + if passed == total: + print("✓ 所有测试通过!") + else: + print("✗ 部分测试失败,请检查代码") + + print("="*60) + + return passed == total + + +def main(): + """主程序""" + print("="*60) + print("双色球模拟号码生成器") + print("="*60) + + # 初始化生成器 + generator = DoubleColorBallGenerator("双色球历史数据.xlsx") + + # 加载历史数据 + print("\n正在加载历史数据...") + if not generator.load_history_data(): + print("无法加载历史数据,将使用默认随机生成策略") + # 创建一个简单的历史数据用于后续统计 + generator.history_data = pd.DataFrame() + generator.red_stats = Counter() + generator.blue_stats = Counter() + generator.features_stats = {} + + # 显示统计信息 + generator.display_statistics() + + # 用户输入 + while True: + try: + print("\n" + "-"*60) + print("请选择操作:") + print("1. 生成号码") + print("2. 运行测试") + print("0. 退出") + choice = input("请选择 (1/2/0): ").strip() + + if choice == "0": + print("感谢使用,再见!") + break + elif choice == "2": + # 运行测试 + generator.run_tests() + continue + elif choice != "1": + print("请选择有效的操作") + continue + + # 生成号码 + num_tickets = int(input("请输入要生成的注数 (1-1000,输入0退出): ")) + + if num_tickets == 0: + print("感谢使用,再见!") + break + + if num_tickets < generator.config['min_tickets'] or num_tickets > generator.config['max_tickets']: + print(f"注数必须在{generator.config['min_tickets']}-{generator.config['max_tickets']}之间") + continue + + # 选择策略 + print("\n请选择生成策略:") + print("1. 高级策略 (基于历史数据分析)") + print("2. 基础策略 (随机生成)") + strategy_choice = input("请选择 (1或2, 默认为1): ").strip() + + if strategy_choice == "2": + strategy = "basic" + print("使用基础随机策略") + else: + strategy = "advanced" + print("使用高级分析策略") + + # 生成号码 + print(f"\n正在生成 {num_tickets} 注号码...") + tickets_df = generator.generate_multiple_tickets( + num_tickets, strategy) + + if len(tickets_df) == 0: + print("生成号码失败,请重试") + continue + + # 显示前几注 + display_count = min(10, num_tickets) + print(f"\n生成的号码 (显示前{display_count}注):") + print("-"*80) + for i, row in tickets_df.head(display_count).iterrows(): + reds = [row['红球1'], row['红球2'], row['红球3'], + row['红球4'], row['红球5'], row['红球6']] + print( + f"第{row['序号']:03d}注: 红球 {', '.join(f'{x:02d}' for x in reds)} | 蓝球 {row['蓝球']:02d}") + print( + f" 和值: {row['和值']}, 奇偶比: {row['奇偶比']}, 大小比: {row['大小比']}, 跨度: {row['跨度']}") + print("-"*80) + + if num_tickets > display_count: + print(f"... 还有 {num_tickets - display_count} 注未显示") + + # 保存到文件 + save_choice = input( + "\n是否保存到Excel文件? (y/n, 默认为y): ").strip().lower() + if save_choice != 'n': + filename = generator.save_to_excel( + tickets_df, num_tickets, strategy) + if filename: + print(f"\n✓ 文件保存完成,总共 {len(tickets_df)} 注") + + # 继续生成 + continue_choice = input("\n是否继续生成? (y/n, 默认为y): ").strip().lower() + if continue_choice == 'n': + print("感谢使用,再见!") + break + + except ValueError: + print("请输入有效的数字") + except KeyboardInterrupt: + print("\n用户中断操作,再见!") + break + except Exception as e: + print(f"发生错误: {e}") + print(traceback.format_exc()) + + +if __name__ == "__main__": + main() diff --git a/lottery/双色球模拟号码-1000注-20260122-001.xlsx b/lottery/双色球模拟号码-1000注-20260122-001.xlsx new file mode 100644 index 0000000..8395574 Binary files /dev/null and b/lottery/双色球模拟号码-1000注-20260122-001.xlsx differ diff --git a/lottery/双色球模拟号码-2注-20260208-001.xlsx b/lottery/双色球模拟号码-2注-20260208-001.xlsx new file mode 100644 index 0000000..99ac7a9 Binary files /dev/null and b/lottery/双色球模拟号码-2注-20260208-001.xlsx differ diff --git a/lottery/双色球模拟号码-2注-20260521-001.xlsx b/lottery/双色球模拟号码-2注-20260521-001.xlsx new file mode 100644 index 0000000..ea2f3a6 Binary files /dev/null and b/lottery/双色球模拟号码-2注-20260521-001.xlsx differ diff --git a/lottery/双色球模拟号码-3注-20260208-001.xlsx b/lottery/双色球模拟号码-3注-20260208-001.xlsx new file mode 100644 index 0000000..c0a570d Binary files /dev/null and b/lottery/双色球模拟号码-3注-20260208-001.xlsx differ diff --git a/lottery/双色球模拟号码-3注-20260521-001.xlsx b/lottery/双色球模拟号码-3注-20260521-001.xlsx new file mode 100644 index 0000000..c4daaad Binary files /dev/null and b/lottery/双色球模拟号码-3注-20260521-001.xlsx differ diff --git a/lottery/双色球模拟号码-5注-20260130-001.xlsx b/lottery/双色球模拟号码-5注-20260130-001.xlsx new file mode 100644 index 0000000..3574585 Binary files /dev/null and b/lottery/双色球模拟号码-5注-20260130-001.xlsx differ diff --git a/lottery/双色球模拟号码-5注-20260130-002.xlsx b/lottery/双色球模拟号码-5注-20260130-002.xlsx new file mode 100644 index 0000000..51892fa Binary files /dev/null and b/lottery/双色球模拟号码-5注-20260130-002.xlsx differ diff --git a/lottery/双色球模拟号码-5注-20260205-001.xlsx b/lottery/双色球模拟号码-5注-20260205-001.xlsx new file mode 100644 index 0000000..d157376 Binary files /dev/null and b/lottery/双色球模拟号码-5注-20260205-001.xlsx differ diff --git a/lottery/双色球模拟号码-5注-20260205-002.xlsx b/lottery/双色球模拟号码-5注-20260205-002.xlsx new file mode 100644 index 0000000..c9e9347 Binary files /dev/null and b/lottery/双色球模拟号码-5注-20260205-002.xlsx differ diff --git a/lottery/双色球模拟号码-5注-20260208-001.xlsx b/lottery/双色球模拟号码-5注-20260208-001.xlsx new file mode 100644 index 0000000..4b6efb8 Binary files /dev/null and b/lottery/双色球模拟号码-5注-20260208-001.xlsx differ diff --git a/lottery/双色球模拟号码-5注-20260210-001.xlsx b/lottery/双色球模拟号码-5注-20260210-001.xlsx new file mode 100644 index 0000000..d051451 Binary files /dev/null and b/lottery/双色球模拟号码-5注-20260210-001.xlsx differ diff --git a/lottery/双色球模拟号码-5注-20260503-001.xlsx b/lottery/双色球模拟号码-5注-20260503-001.xlsx new file mode 100644 index 0000000..65b720d Binary files /dev/null and b/lottery/双色球模拟号码-5注-20260503-001.xlsx differ diff --git a/lottery/双色球模拟号码-5注-20260503-002.xlsx b/lottery/双色球模拟号码-5注-20260503-002.xlsx new file mode 100644 index 0000000..b7f62fb Binary files /dev/null and b/lottery/双色球模拟号码-5注-20260503-002.xlsx differ diff --git a/web_console.html b/web_console.html new file mode 100644 index 0000000..d3ec63a --- /dev/null +++ b/web_console.html @@ -0,0 +1,324 @@ + + + + + + 双色球数据抓取 - 执行控制台 + + + +
+
+

🎯 双色球数据抓取控制台

+

定时任务执行器 - 实时监控数据抓取状态

+
+ +
+
+
+ 等待执行 +
+
+ 数据源: www.55128.cn +
+
+ 上次更新: 从未 +
+
+ +
+

📋 使用说明

+
    +
  • ✅ 点击「立即执行」按钮抓取最新双色球历史数据
  • +
  • ✅ 数据将保存到:/Users/vincent/Studio/lottoData/双色球历史数据.xlsx
  • +
  • ✅ 定时任务:每天自动执行一次(通过系统 cron)
  • +
  • ✅ 实时监控:执行日志在此页面实时显示
  • +
+
+ +
+ +
+ +
+

📄 执行日志

+
// 等待执行任务... +// 点击「立即执行抓取」按钮开始任务
+
+ +
+ 脚本路径:/Users/vincent/Studio/lottoData/fetch_data.py | + 输出文件:双色球历史数据.xlsx +
+
+ + + + \ No newline at end of file diff --git a/web_executor.py b/web_executor.py new file mode 100644 index 0000000..329ed60 --- /dev/null +++ b/web_executor.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +双色球数据抓取 Web 服务 +提供 Web 界面执行抓取任务和查看实时结果 +监听 0.0.0.0,支持局域网访问 +""" + +from flask import Flask, send_from_directory, jsonify +import subprocess +import os +import json +from datetime import datetime +import threading + +app = Flask(__name__) + +# 脚本路径和输出文件 +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +SCRIPT_PATH = os.path.join(SCRIPT_DIR, "fetch_data.py") +OUTPUT_FILE = os.path.join(SCRIPT_DIR, "双色球历史数据.xlsx") +STATUS_FILE = os.path.join(SCRIPT_DIR, ".fetch_status.json") + +# 全局状态 +execution_status = { + "is_running": False, + "last_update": None, + "last_record_count": 0, + "last_error": None +} + +# 状态锁 +status_lock = threading.Lock() + + +def load_status(): + """从文件加载状态""" + global execution_status + if os.path.exists(STATUS_FILE): + try: + with open(STATUS_FILE, 'r', encoding='utf-8') as f: + execution_status = json.load(f) + except: + pass + + +def save_status(): + """保存状态到文件""" + with status_lock: + with open(STATUS_FILE, 'w', encoding='utf-8') as f: + json.dump(execution_status, f, ensure_ascii=False, indent=2) + + +@app.route('/') +def index(): + """首页 - Web 控制台""" + return send_from_directory(SCRIPT_DIR, 'web_console.html') + + +@app.route('/api/status') +def api_status(): + """获取当前执行状态""" + with status_lock: + return jsonify({ + "isRunning": execution_status.get("is_running", False), + "lastUpdate": execution_status.get("last_update"), + "recordCount": execution_status.get("last_record_count", 0), + "lastError": execution_status.get("last_error") + }) + + +@app.route('/api/execute', methods=['POST']) +def api_execute(): + """执行抓取脚本""" + global execution_status + + with status_lock: + if execution_status.get("is_running", False): + return jsonify({ + "success": False, + "error": "任务正在执行中,请稍后再试" + }), 409 + + # 启动执行线程 + def run_script(): + global execution_status + + with status_lock: + execution_status["is_running"] = True + execution_status["last_error"] = None + save_status() + + try: + print(f"[{datetime.now()}] 开始执行抓取脚本...") + + # 执行 Python 脚本 + result = subprocess.run( + ["python3", SCRIPT_PATH], + capture_output=True, + text=True, + timeout=300 + ) + + if result.returncode == 0: + # 解析输出,获取记录数 + record_count = 0 + for line in result.stdout.split('\n'): + if '共保存' in line and '条记录' in line: + try: + record_count = int(line.split('共保存')[1].split('条记录')[0].strip()) + except: + pass + elif '成功解析' in line and '条数据' in line: + try: + record_count = int(line.split('成功解析')[1].split('条数据')[0].strip()) + except: + pass + + with status_lock: + execution_status["last_update"] = datetime.now().isoformat() + execution_status["last_record_count"] = record_count + execution_status["is_running"] = False + save_status() + + print(f"✅ 执行成功,共抓取 {record_count} 条数据") + + else: + error_msg = result.stderr or f"脚本执行失败,返回码:{result.returncode}" + with status_lock: + execution_status["last_error"] = error_msg + execution_status["is_running"] = False + save_status() + print(f"❌ {error_msg}") + + except subprocess.TimeoutExpired: + error_msg = "脚本执行超时(超过 5 分钟)" + with status_lock: + execution_status["last_error"] = error_msg + execution_status["is_running"] = False + save_status() + print(f"❌ {error_msg}") + + except Exception as e: + error_msg = f"执行异常:{str(e)}" + with status_lock: + execution_status["last_error"] = error_msg + execution_status["is_running"] = False + save_status() + print(f"❌ {error_msg}") + + # 在后台线程执行 + thread = threading.Thread(target=run_script, daemon=True) + thread.start() + + return jsonify({ + "success": True, + "message": "任务已启动,正在执行中..." + }) + + +def check_dependencies(): + """检查依赖""" + missing = [] + + try: + import flask + except ImportError: + missing.append("flask") + + try: + import requests + except ImportError: + missing.append("requests") + + try: + import bs4 + except ImportError: + missing.append("beautifulsoup4") + + try: + import pandas + except ImportError: + missing.append("pandas") + + try: + import openpyxl + except ImportError: + missing.append("openpyxl") + + if missing: + print(f"❌ 缺少依赖包:{', '.join(missing)}") + print(f" 请运行:pip3 install {' '.join(missing)}") + return False + + print("✅ 所有依赖已安装") + return True + + +if __name__ == "__main__": + print("=" * 60) + print("双色球数据抓取 Web 服务") + print("=" * 60) + + if not check_dependencies(): + exit(1) + + load_status() + + print(f"\n📂 脚本路径:{SCRIPT_PATH}") + print(f"📁 输出文件:{OUTPUT_FILE}") + print(f"\n🌐 服务启动中...") + print(f" 监听地址:http://0.0.0.0:5000") + print(f" 访问方式:局域网内任意设备访问 http://<本机 IP>:5000") + print(f"\n✅ 服务就绪!") + print("=" * 60) + + app.run(host='0.0.0.0', port=5000, debug=False, threaded=True) \ No newline at end of file diff --git a/双色球历史数据.xlsx b/双色球历史数据.xlsx new file mode 100644 index 0000000..f99ab05 Binary files /dev/null and b/双色球历史数据.xlsx differ