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
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
venv/
|
||||
.venv/
|
||||
LottoSpider/
|
||||
*.log
|
||||
@@ -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/<record_id>', 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/<path:filepath>')
|
||||
@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)
|
||||
@@ -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
|
||||
Executable
+17
@@ -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}
|
||||
@@ -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
|
||||
@@ -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. 添加结构变更检测<br>2. 建立告警机制<br>3. 定期维护更新 |
|
||||
| 数据源网站反爬策略 | 被封 IP | 低 | 1. 控制请求频率<br>2. 添加 User-Agent<br>3. 必要时使用代理 |
|
||||
|
||||
### 8.2 技术风险
|
||||
|
||||
| 风险 | 影响 | 概率 | 应对方案 |
|
||||
|------|------|------|----------|
|
||||
| Excel 文件被占用 | 写入失败 | 低 | 1. 写入前检查文件锁<br>2. 使用临时文件 + 原子替换<br>3. 失败后重试 |
|
||||
| 网络不稳定 | 抓取失败 | 中 | 1. 实现重试机制<br>2. 记录失败原因<br>3. 邮件/消息告警 |
|
||||
| 端口冲突 | Web 服务启动失败 | 低 | 1. 启动前检查端口占用<br>2. 支持配置端口<br>3. 日志明确提示 |
|
||||
|
||||
### 8.3 安全风险
|
||||
|
||||
| 风险 | 影响 | 概率 | 应对方案 |
|
||||
|------|------|------|----------|
|
||||
| 未授权访问 | 数据泄露 | 中 | 1. 防火墙限制 IP 范围<br>2. 可选 HTTP 认证<br>3. 访问日志审计 |
|
||||
| 恶意请求 | 服务崩溃 | 低 | 1. 请求频率限制<br>2. 异常输入过滤<br>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
|
||||
**下次评审日期:** 开发完成后验收评审
|
||||
@@ -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 评审准备就绪,邀请架构师(梁思筑)和开发(徐聪)参与评审。**
|
||||
@@ -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
|
||||
**产品经理**: 沈路明
|
||||
**状态**: 待评审
|
||||
+132
@@ -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())
|
||||
+1171
File diff suppressed because it is too large
Load Diff
+1189
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,324 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>双色球数据抓取 - 执行控制台</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
background: #f8f9fa;
|
||||
padding: 15px 30px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #6c757d;
|
||||
}
|
||||
|
||||
.status-indicator.ready { background: #28a745; }
|
||||
.status-indicator.running {
|
||||
background: #ffc107;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
.status-indicator.completed { background: #28a745; }
|
||||
.status-indicator.error { background: #dc3545; }
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.controls {
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 15px 40px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.console {
|
||||
padding: 0 30px 30px;
|
||||
}
|
||||
|
||||
.console-output {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
font-family: "Monaco", "Menlo", "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.console-output .success { color: #6a9955; }
|
||||
.console-output .error { color: #f44747; }
|
||||
.console-output .info { color: #569cd6; }
|
||||
|
||||
.last-update {
|
||||
padding: 15px 30px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e9ecef;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
padding: 20px 30px;
|
||||
background: #fff3cd;
|
||||
border-top: 1px solid #ffc107;
|
||||
}
|
||||
|
||||
.info-section h3 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.info-section ul {
|
||||
list-style: none;
|
||||
font-size: 14px;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.info-section li { padding: 5px 0; }
|
||||
.info-section code {
|
||||
background: rgba(0,0,0,0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🎯 双色球数据抓取控制台</h1>
|
||||
<p>定时任务执行器 - 实时监控数据抓取状态</p>
|
||||
</div>
|
||||
|
||||
<div class="status-bar">
|
||||
<div class="status-item">
|
||||
<div class="status-indicator ready" id="statusIndicator"></div>
|
||||
<span id="statusText">等待执行</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<strong>数据源:</strong> www.55128.cn
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<strong>上次更新:</strong> <span id="lastUpdate">从未</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h3>📋 使用说明</h3>
|
||||
<ul>
|
||||
<li>✅ 点击「立即执行」按钮抓取最新双色球历史数据</li>
|
||||
<li>✅ 数据将保存到:<code>/Users/vincent/Studio/lottoData/双色球历史数据.xlsx</code></li>
|
||||
<li>✅ 定时任务:每天自动执行一次(通过系统 cron)</li>
|
||||
<li>✅ 实时监控:执行日志在此页面实时显示</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn" id="executeBtn" onclick="executeScript()">
|
||||
🚀 立即执行抓取
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="console">
|
||||
<h3 style="margin-bottom: 10px; color: #495057; font-size: 16px;">📄 执行日志</h3>
|
||||
<div class="console-output" id="consoleOutput">// 等待执行任务...
|
||||
// 点击「立即执行抓取」按钮开始任务</div>
|
||||
</div>
|
||||
|
||||
<div class="last-update">
|
||||
脚本路径:<code>/Users/vincent/Studio/lottoData/fetch_data.py</code> |
|
||||
输出文件:<span id="outputFile">双色球历史数据.xlsx</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let isExecuting = false;
|
||||
|
||||
function log(message, type = 'info') {
|
||||
const output = document.getElementById('consoleOutput');
|
||||
const timestamp = new Date().toLocaleTimeString('zh-CN');
|
||||
const className = type === 'success' ? 'success' : type === 'error' ? 'error' : 'info';
|
||||
output.innerHTML += `\n<span class="${className}">[${timestamp}] ${message}</span>`;
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
|
||||
function updateStatus(status, text) {
|
||||
const indicator = document.getElementById('statusIndicator');
|
||||
indicator.className = 'status-indicator ' + status;
|
||||
document.getElementById('statusText').textContent = text;
|
||||
}
|
||||
|
||||
async function executeScript() {
|
||||
if (isExecuting) {
|
||||
log('任务正在执行中...', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
isExecuting = true;
|
||||
document.getElementById('executeBtn').disabled = true;
|
||||
document.getElementById('consoleOutput').innerHTML = '';
|
||||
log('开始执行抓取任务...', 'info');
|
||||
updateStatus('running', '执行中');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/execute', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
log('正在抓取数据,请稍候...', 'info');
|
||||
|
||||
// 轮询状态直到完成
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const statusResp = await fetch('/api/status');
|
||||
const status = await statusResp.json();
|
||||
|
||||
if (!status.isRunning) {
|
||||
clearInterval(pollInterval);
|
||||
|
||||
if (status.lastError) {
|
||||
log(`❌ 任务失败:${status.lastError}`, 'error');
|
||||
updateStatus('error', '执行失败');
|
||||
} else {
|
||||
log(`✅ 任务执行成功!`, 'success');
|
||||
log(`📊 共抓取 ${status.recordCount} 条数据`, 'success');
|
||||
updateStatus('completed', '执行完成');
|
||||
document.getElementById('lastUpdate').textContent = new Date(status.lastUpdate).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
isExecuting = false;
|
||||
document.getElementById('executeBtn').disabled = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('轮询失败:', e);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
} else {
|
||||
log(`❌ 任务失败:${data.error}`, 'error');
|
||||
updateStatus('error', '执行失败');
|
||||
isExecuting = false;
|
||||
document.getElementById('executeBtn').disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
log(`❌ 请求错误:${error.message}`, 'error');
|
||||
updateStatus('error', '执行失败');
|
||||
isExecuting = false;
|
||||
document.getElementById('executeBtn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/status');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.lastUpdate) {
|
||||
document.getElementById('lastUpdate').textContent = new Date(data.lastUpdate).toLocaleString('zh-CN');
|
||||
if (data.recordCount > 0) {
|
||||
log(`上次执行:共抓取 ${data.recordCount} 条数据`, 'info');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载状态失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
loadStatus();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+217
@@ -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)
|
||||
Binary file not shown.
Reference in New Issue
Block a user