fix: 修复代码审查反馈全部问题
审查意见修复清单:
P1 列映射语义修复 (lottery.py):
- _normalize_history_format() 不再将红球2-6映射到开机号/和值特征/奇偶比等
格式A不含这些特征字段,缺失列留空,前端做降级显示
- 删除已用于构建号码列的原始分列,避免数据重复
P2 架构优化:
- 提取 Excel 兼容逻辑到公共模块 history_loader.py
lottery.py 和 app.py 共同引用,消除三处重复代码
- web_executor.py 标记为已废弃,功能已整合到 app.py
部署修复:
- 删除 deploy/lotto-web.service (旧服务),仅保留 lotto-app.service
- 更新 deploy/DEPLOY.md: 端口5000→8085, 接口清单更新, 添加迁移说明
安全加固:
- API Token 改为环境变量读取: os.environ.get('LOTTO_API_TOKEN')
- 错误信息不再暴露内部异常,改为通用错误消息+日志记录
- 目录遍历防护改用 os.path.realpath 检查最终路径
其他:
- .gitignore 补充排除 双色球历史数据.xlsx
- app.py 引用公共模块,简化 get_statistics_data 和 load_history_dataframe
测试验证: 全部 API 测试通过,120条历史数据正确解析
Issue: BIZ-75
This commit is contained in:
@@ -10,6 +10,7 @@ LottoSpider/
|
|||||||
.fetch_status.json
|
.fetch_status.json
|
||||||
.generation_records.json
|
.generation_records.json
|
||||||
lottery/
|
lottery/
|
||||||
|
双色球历史数据.xlsx
|
||||||
|
|
||||||
# 备份文件
|
# 备份文件
|
||||||
*.bak
|
*.bak
|
||||||
|
|||||||
@@ -17,8 +17,11 @@ from datetime import datetime
|
|||||||
from flask import Flask, send_from_directory, jsonify, request, send_file, abort
|
from flask import Flask, send_from_directory, jsonify, request, send_file, abort
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
# 将项目目录加入路径,以便导入 lottery.py
|
# 将项目目录加入路径,以便导入 lottery.py 和 history_loader.py
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
# 导入公共历史数据加载模块
|
||||||
|
from history_loader import load_history_dataframe as _load_history, parse_number_string, compute_statistics
|
||||||
sys.path.insert(0, BASE_DIR)
|
sys.path.insert(0, BASE_DIR)
|
||||||
|
|
||||||
# 导入号码生成器
|
# 导入号码生成器
|
||||||
@@ -35,7 +38,7 @@ CONFIG = {
|
|||||||
'history_file': os.path.join(BASE_DIR, '双色球历史数据.xlsx'),
|
'history_file': os.path.join(BASE_DIR, '双色球历史数据.xlsx'),
|
||||||
'lottery_output_dir': os.path.join(BASE_DIR, 'lottery'),
|
'lottery_output_dir': os.path.join(BASE_DIR, 'lottery'),
|
||||||
'records_file': os.path.join(BASE_DIR, '.generation_records.json'),
|
'records_file': os.path.join(BASE_DIR, '.generation_records.json'),
|
||||||
'api_token': 'lotto2026',
|
'api_token': os.environ.get('LOTTO_API_TOKEN', 'lotto2026'),
|
||||||
'auth_enabled': False,
|
'auth_enabled': False,
|
||||||
'max_tickets': 1000,
|
'max_tickets': 1000,
|
||||||
'default_tickets': 10,
|
'default_tickets': 10,
|
||||||
@@ -105,74 +108,11 @@ def add_record(strategy, num_tickets, filename):
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# Excel 历史数据读取辅助
|
# Excel 历史数据读取辅助
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 标准列名 (与 lottery.py 兼容)
|
# 历史数据加载 — 使用公共模块 history_loader.py
|
||||||
HISTORY_COLUMNS = ['开奖时间', '期数', '号码', '开机号', '和值特征', '奇偶比', '大小比', '奇偶形态', '跨度', '其他']
|
# ============================================================
|
||||||
|
|
||||||
def load_history_dataframe():
|
def load_history_dataframe():
|
||||||
"""智能加载历史数据 Excel,兼容多种格式。
|
"""加载历史数据 Excel,委托公共模块处理多格式兼容。"""
|
||||||
|
return _load_history(CONFIG['history_file'])
|
||||||
格式A(fetch_data.py 当前输出):
|
|
||||||
Row0=新列名(期号|开奖日期|红球1...|蓝球|特别号)
|
|
||||||
Row1=旧列名(开奖时间|期数|号码|开机号|...)
|
|
||||||
Row2+=实际数据
|
|
||||||
格式B(标准格式):
|
|
||||||
Row0=列名(开奖时间|期数|号码|开机号|...)
|
|
||||||
Row1+=数据
|
|
||||||
"""
|
|
||||||
import pandas as pd
|
|
||||||
raw_df = pd.read_excel(CONFIG['history_file'], header=None)
|
|
||||||
|
|
||||||
row0_vals = raw_df.iloc[0].astype(str).tolist() if len(raw_df) > 0 else []
|
|
||||||
row1_vals = raw_df.iloc[1].astype(str).tolist() if len(raw_df) > 1 else []
|
|
||||||
|
|
||||||
has_legacy_in_row0 = any(col in row0_vals for col in ['开奖时间', '期数', '号码'])
|
|
||||||
has_legacy_in_row1 = any(col in row1_vals for col in ['开奖时间', '期数', '号码'])
|
|
||||||
has_new_cols_in_row0 = any(col in row0_vals for col in ['期号', '开奖日期', '红球 1'])
|
|
||||||
|
|
||||||
if has_new_cols_in_row0 and has_legacy_in_row1:
|
|
||||||
# 格式A:跳过 Row0(新列名) 和 Row1(旧列名),用旧列名,数据从 Row2 开始
|
|
||||||
data_df = raw_df.iloc[2:].copy()
|
|
||||||
num_cols = min(len(data_df.columns), len(HISTORY_COLUMNS))
|
|
||||||
data_df.columns = HISTORY_COLUMNS[:num_cols] + [f'col_{i}' for i in range(num_cols, len(data_df.columns))]
|
|
||||||
elif has_legacy_in_row0:
|
|
||||||
# 格式B:Row0 就是标准列名
|
|
||||||
data_df = raw_df.iloc[1:].copy()
|
|
||||||
num_cols = min(len(data_df.columns), len(HISTORY_COLUMNS))
|
|
||||||
data_df.columns = HISTORY_COLUMNS[:num_cols] + [f'col_{i}' for i in range(num_cols, len(data_df.columns))]
|
|
||||||
else:
|
|
||||||
# 尝试默认读取
|
|
||||||
df = pd.read_excel(CONFIG['history_file'])
|
|
||||||
if '号码' not in df.columns and any(c in df.columns for c in ['红球 1', '红球1']):
|
|
||||||
# 分列格式,需要构建号码列
|
|
||||||
data_df = df.copy()
|
|
||||||
red_cols = [f'红球 {i}' for i in range(1, 7)]
|
|
||||||
if not all(c in data_df.columns for c in red_cols):
|
|
||||||
red_cols = [f'红球{i}' for i in range(1, 7)]
|
|
||||||
if all(c in data_df.columns for c in red_cols) and '蓝球' in data_df.columns:
|
|
||||||
def build_num(row):
|
|
||||||
parts = []
|
|
||||||
for c in red_cols:
|
|
||||||
val = row.get(c)
|
|
||||||
if pd.isna(val):
|
|
||||||
return None
|
|
||||||
s = str(int(val)) if isinstance(val, (int, float)) else str(val).strip()
|
|
||||||
parts.append(s.zfill(2))
|
|
||||||
blue_val = row.get('蓝球')
|
|
||||||
if pd.isna(blue_val):
|
|
||||||
return None
|
|
||||||
blue_s = str(int(blue_val)) if isinstance(blue_val, (int, float)) else str(blue_val).strip()
|
|
||||||
return ''.join(parts) + blue_s.zfill(2)
|
|
||||||
data_df['号码'] = data_df.apply(build_num, axis=1)
|
|
||||||
else:
|
|
||||||
data_df = df
|
|
||||||
|
|
||||||
num_cols = min(len(data_df.columns), len(HISTORY_COLUMNS))
|
|
||||||
# 如果列名不匹配标准,重命名
|
|
||||||
if not any(c in data_df.columns for c in HISTORY_COLUMNS[:3]):
|
|
||||||
data_df.columns = HISTORY_COLUMNS[:num_cols] + [f'col_{i}' for i in range(num_cols, len(data_df.columns))]
|
|
||||||
|
|
||||||
data_df = data_df.reset_index(drop=True)
|
|
||||||
return data_df
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 认证装饰器(可选)
|
# 认证装饰器(可选)
|
||||||
@@ -259,89 +199,13 @@ def api_generate():
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': f'生成失败: {str(e)}'}), 500
|
traceback.print_exc()
|
||||||
|
return jsonify({'success': False, 'error': '号码生成失败,请检查历史数据文件是否完整'}), 500
|
||||||
|
|
||||||
|
|
||||||
def get_statistics_data(generator=None):
|
def get_statistics_data(generator=None):
|
||||||
"""获取统计数据"""
|
"""获取统计数据 — 委托公共模块计算"""
|
||||||
import pandas as pd
|
return compute_statistics(CONFIG['history_file'])
|
||||||
import re
|
|
||||||
from collections import Counter
|
|
||||||
|
|
||||||
if not os.path.exists(CONFIG['history_file']):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# 使用智能加载函数
|
|
||||||
data_df = load_history_dataframe()
|
|
||||||
|
|
||||||
# 解析红球和蓝球
|
|
||||||
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
|
return stats
|
||||||
|
|
||||||
@@ -357,11 +221,8 @@ def api_statistics():
|
|||||||
stats = get_statistics_data()
|
stats = get_statistics_data()
|
||||||
return jsonify({'success': True, 'data': stats})
|
return jsonify({'success': True, 'data': stats})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
traceback.print_exc()
|
||||||
|
return jsonify({'success': False, 'error': '获取统计数据失败'}), 500
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# API:获取生成记录
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@app.route('/api/records')
|
@app.route('/api/records')
|
||||||
@require_auth
|
@require_auth
|
||||||
@@ -387,7 +248,8 @@ def api_records():
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
traceback.print_exc()
|
||||||
|
return jsonify({'success': False, 'error': '获取生成记录失败'}), 500
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -431,11 +293,8 @@ def api_delete_record(record_id):
|
|||||||
|
|
||||||
return jsonify({'success': True, 'message': '记录已删除'})
|
return jsonify({'success': True, 'message': '记录已删除'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
traceback.print_exc()
|
||||||
|
return jsonify({'success': False, 'error': '删除记录失败'}), 500
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# API:文件下载
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@app.route('/api/download/<path:filepath>')
|
@app.route('/api/download/<path:filepath>')
|
||||||
@require_auth
|
@require_auth
|
||||||
@@ -444,10 +303,11 @@ def api_download(filepath):
|
|||||||
try:
|
try:
|
||||||
# 安全检查:防止目录遍历
|
# 安全检查:防止目录遍历
|
||||||
safe_path = os.path.normpath(filepath)
|
safe_path = os.path.normpath(filepath)
|
||||||
if safe_path.startswith('..') or safe_path.startswith('/'):
|
full_path = os.path.realpath(os.path.join(BASE_DIR, safe_path))
|
||||||
|
# 使用 realpath 检查最终路径是否仍在 BASE_DIR 内
|
||||||
|
if not full_path.startswith(os.path.realpath(BASE_DIR)):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
full_path = os.path.join(BASE_DIR, safe_path)
|
|
||||||
if not os.path.exists(full_path):
|
if not os.path.exists(full_path):
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
@@ -536,7 +396,8 @@ def api_history():
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
traceback.print_exc()
|
||||||
|
return jsonify({'success': False, 'error': '获取历史数据失败'}), 500
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -570,11 +431,8 @@ def api_status():
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
traceback.print_exc()
|
||||||
|
return jsonify({'success': False, 'error': '获取系统状态失败'}), 500
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# 前端页面
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
|
|||||||
+110
-62
@@ -5,9 +5,11 @@
|
|||||||
| 项目 | 值 |
|
| 项目 | 值 |
|
||||||
|------|-----|
|
|------|-----|
|
||||||
| 项目名称 | 双色球自动化系统 |
|
| 项目名称 | 双色球自动化系统 |
|
||||||
| 部署时间 | 2026-06-29 |
|
| 部署时间 | 2026-07-04 |
|
||||||
|
| 开发人员 | 徐聪 (costcodev) |
|
||||||
| 部署人员 | 严维序 (opengineer) |
|
| 部署人员 | 严维序 (opengineer) |
|
||||||
| 服务地址 | http://192.168.1.99:5000 |
|
| 服务地址 | http://192.168.1.99:8085 |
|
||||||
|
| 代码仓库 | http://192.168.1.99:12299/vincent/Lottery.git |
|
||||||
| 宿主服务器 | Ubuntu-OpenClaw (192.168.1.99) |
|
| 宿主服务器 | Ubuntu-OpenClaw (192.168.1.99) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -17,38 +19,48 @@
|
|||||||
```
|
```
|
||||||
/home/vincent/Studio/lottoData/
|
/home/vincent/Studio/lottoData/
|
||||||
├── venv/ # Python 虚拟环境
|
├── venv/ # Python 虚拟环境
|
||||||
├── web_executor.py # Flask Web 服务 (监听 0.0.0.0:5000)
|
├── app.py # Flask 统一 Web 服务 (监听 0.0.0.0:8085)
|
||||||
├── fetch_data.py # 数据抓取脚本
|
├── index.html # 前端 UI (响应式,4 Tab 页面)
|
||||||
├── lottery.py # 双色球号码生成器
|
├── lottery.py # 双色球号码生成器核心逻辑
|
||||||
├── web_console.html # Web 控制台页面
|
├── fetch_data.py # 历史数据抓取脚本
|
||||||
├── LottoSpider/ # 爬虫模块
|
├── web_console.html # 数据抓取控制台前端页面
|
||||||
├── lottery/ # 彩票模块
|
├── web_executor.py # [已废弃] 旧版独立抓取服务,功能已整合到 app.py
|
||||||
├── docs/ # 文档
|
├── requirements.txt # Python 依赖清单
|
||||||
├── 双色球历史数据.xlsx # 历史数据文件
|
├── 双色球历史数据.xlsx # 历史数据文件 (不纳入 git)
|
||||||
|
├── lottery/ # 号码生成结果输出目录 (不纳入 git)
|
||||||
|
├── .generation_records.json # 生成记录索引 (不纳入 git)
|
||||||
|
├── .fetch_status.json # 抓取状态文件 (不纳入 git)
|
||||||
|
├── docs/ # 文档目录
|
||||||
|
│ ├── PRD-双色球 WebUI-v1.0.md
|
||||||
|
│ └── 开发文档-双色球WebUI-v1.0.md
|
||||||
└── deploy/ # 部署文件
|
└── deploy/ # 部署文件
|
||||||
├── DEPLOY.md # 本文档
|
├── DEPLOY.md # 本文档
|
||||||
├── lotto-web.service # systemd 服务文件
|
├── lotto-app.service # systemd 服务文件 (统一入口)
|
||||||
├── fetch_daily.sh # 每日抓取脚本
|
├── fetch_daily.sh # 每日定时抓取脚本
|
||||||
|
├── backup.sh # 备份脚本 (30天保留)
|
||||||
├── cron.log # Cron 执行日志
|
├── cron.log # Cron 执行日志
|
||||||
└── fetch_YYYYMMDD.log # 每日抓取详细日志
|
└── fetch_YYYYMMDD.log # 每日抓取详细日志
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**说明**: `app.py` 是统一入口,整合了号码生成、历史数据、生成记录、统计数据抓取等全部功能。`web_executor.py` 已废弃,不需独立部署。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 二、依赖清单
|
## 二、依赖清单
|
||||||
|
|
||||||
| 包 | 版本 | 用途 |
|
| 包 | 用途 |
|
||||||
|----|------|------|
|
|----|------|
|
||||||
| Flask | 3.1.3 | Web 服务框架 |
|
| Flask | Web 服务框架 |
|
||||||
| pandas | 3.0.4 | 数据处理 |
|
| pandas | 数据处理 |
|
||||||
| openpyxl | 3.1.5 | Excel 读写 |
|
| openpyxl | Excel 读写 |
|
||||||
| requests | 2.34.2 | HTTP 请求 |
|
| numpy | 数值计算 |
|
||||||
| beautifulsoup4 | 4.15.0 | HTML 解析 |
|
| requests | HTTP 请求 (数据抓取) |
|
||||||
|
| beautifulsoup4 | HTML 解析 (数据抓取) |
|
||||||
|
|
||||||
安装命令:
|
安装命令:
|
||||||
```bash
|
```bash
|
||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
./venv/bin/pip install flask pandas openpyxl requests beautifulsoup4
|
./venv/bin/pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -57,22 +69,23 @@ python3 -m venv venv
|
|||||||
|
|
||||||
### 服务文件
|
### 服务文件
|
||||||
|
|
||||||
`/etc/systemd/system/lotto-web.service`
|
`/etc/systemd/system/lotto-app.service`
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=双色球数据抓取 Web 服务
|
Description=双色球号码生成 Web 服务 (app.py :8085)
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=vincent
|
User=vincent
|
||||||
WorkingDirectory=/home/vincent/Studio/lottoData
|
WorkingDirectory=/home/vincent/Studio/lottoData
|
||||||
ExecStart=/home/vincent/Studio/lottoData/venv/bin/python3 /home/vincent/Studio/lottoData/web_executor.py
|
ExecStart=/home/vincent/Studio/lottoData/venv/bin/python3 /home/vincent/Studio/lottoData/app.py
|
||||||
ExecStartPre=/home/vincent/Studio/lottoData/venv/bin/python3 -c "import flask; import pandas; import openpyxl; import requests; import bs4"
|
ExecStartPre=/home/vincent/Studio/lottoData/venv/bin/python3 -c "import flask; import pandas; import openpyxl; import numpy"
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
KillMode=control-group
|
KillMode=control-group
|
||||||
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
@@ -82,16 +95,25 @@ WantedBy=multi-user.target
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 安装/启用
|
# 安装/启用
|
||||||
sudo cp deploy/lotto-web.service /etc/systemd/system/
|
sudo cp deploy/lotto-app.service /etc/systemd/system/
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable lotto-web
|
sudo systemctl enable lotto-app
|
||||||
sudo systemctl start lotto-web
|
sudo systemctl start lotto-app
|
||||||
|
|
||||||
# 日常管理
|
# 日常管理
|
||||||
sudo systemctl status lotto-web # 查看状态
|
sudo systemctl status lotto-app # 查看状态
|
||||||
sudo systemctl restart lotto-web # 重启
|
sudo systemctl restart lotto-app # 重启
|
||||||
sudo systemctl stop lotto-web # 停止
|
sudo systemctl stop lotto-app # 停止
|
||||||
sudo journalctl -u lotto-web -f # 查看实时日志
|
sudo journalctl -u lotto-app -f # 查看实时日志
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产部署建议
|
||||||
|
|
||||||
|
建议使用 gunicorn 替代 Flask 内置服务器:
|
||||||
|
```bash
|
||||||
|
./venv/bin/pip install gunicorn
|
||||||
|
# 修改 ExecStart 为:
|
||||||
|
# /home/vincent/Studio/lottoData/venv/bin/gunicorn -w 4 -b 0.0.0.0:8085 app:app
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -102,77 +124,103 @@ sudo journalctl -u lotto-web -f # 查看实时日志
|
|||||||
|
|
||||||
```
|
```
|
||||||
30 2 * * * /home/vincent/Studio/lottoData/deploy/fetch_daily.sh >> /home/vincent/Studio/lottoData/deploy/cron.log 2>&1
|
30 2 * * * /home/vincent/Studio/lottoData/deploy/fetch_daily.sh >> /home/vincent/Studio/lottoData/deploy/cron.log 2>&1
|
||||||
|
0 3 * * * /home/vincent/Studio/lottoData/deploy/backup.sh >> /home/vincent/Studio/lottoData/deploy/cron.log 2>&1
|
||||||
```
|
```
|
||||||
|
|
||||||
每天凌晨 2:30 自动抓取双色球历史数据。
|
- 每天 02:30 自动抓取双色球历史数据
|
||||||
|
- 每天 03:00 自动备份数据(保留 30 天)
|
||||||
|
|
||||||
### 手动执行
|
### 手动执行
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/home/vincent/Studio/lottoData/deploy/fetch_daily.sh
|
/home/vincent/Studio/lottoData/deploy/fetch_daily.sh
|
||||||
# 或
|
# 或通过 Web 控制台触发: http://192.168.1.99:8085/fetch
|
||||||
/home/vincent/Studio/lottoData/venv/bin/python3 /home/vincent/Studio/lottoData/fetch_data.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 五、Web 接口
|
## 五、Web 接口清单
|
||||||
|
|
||||||
| 路径 | 方法 | 说明 |
|
| 路径 | 方法 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `/` | GET | Web 控制台页面 |
|
| `/` | GET | 双色球 Web UI 首页(号码生成) |
|
||||||
| `/api/status` | GET | 获取执行状态 |
|
| `/fetch` | GET | 数据抓取控制台 |
|
||||||
| `/api/execute` | POST | 触发数据抓取 |
|
| `/api/generate` | POST | 生成号码(参数: num_tickets, strategy) |
|
||||||
|
| `/api/history` | GET | 获取历史开奖数据(参数: page, page_size, search) |
|
||||||
|
| `/api/records` | GET | 获取生成记录列表(参数: page, page_size) |
|
||||||
|
| `/api/records/:id` | DELETE | 删除生成记录 |
|
||||||
|
| `/api/statistics` | GET | 获取统计分析数据 |
|
||||||
|
| `/api/download/:filepath` | GET | 下载文件 |
|
||||||
|
| `/api/status` | GET | 系统状态 |
|
||||||
|
| `/api/config` | GET | 前端配置 |
|
||||||
|
| `/api/fetch/status` | GET | 抓取执行状态 |
|
||||||
|
| `/api/fetch/execute` | POST | 触发数据抓取 |
|
||||||
|
|
||||||
### 示例
|
### 示例
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 查看状态
|
# 查看状态
|
||||||
curl http://192.168.1.99:5000/api/status
|
curl http://192.168.1.99:8085/api/status
|
||||||
|
|
||||||
|
# 生成号码
|
||||||
|
curl -X POST http://192.168.1.99:8085/api/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"num_tickets": 10, "strategy": "advanced"}'
|
||||||
|
|
||||||
# 触发抓取
|
# 触发抓取
|
||||||
curl -X POST http://192.168.1.99:5000/api/execute
|
curl -X POST http://192.168.1.99:8085/api/fetch/execute
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 六、验证清单
|
## 六、迁移说明(从旧版升级)
|
||||||
|
|
||||||
- [x] 依赖安装完整 (Flask, pandas, openpyxl, requests, beautifulsoup4)
|
如果之前部署了旧版 `lotto-web.service`(端口 5000):
|
||||||
- [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
|
```bash
|
||||||
# 停止服务
|
# 1. 停止旧服务
|
||||||
sudo systemctl stop lotto-web
|
sudo systemctl stop lotto-web
|
||||||
sudo systemctl disable lotto-web
|
sudo systemctl disable lotto-web
|
||||||
sudo rm /etc/systemd/system/lotto-web.service
|
sudo rm /etc/systemd/system/lotto-web.service
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
# 移除 cron
|
# 2. 部署新服务
|
||||||
crontab -l | grep -v 'lottoData' | crontab -
|
sudo cp deploy/lotto-app.service /etc/systemd/system/
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now lotto-app
|
||||||
|
|
||||||
# 不影响数据文件和代码
|
# 3. 更新 cron(指向新的 fetch_daily.sh)
|
||||||
|
crontab -l | sed 's|web_executor|app|g' | crontab -
|
||||||
|
# 或手动编辑: crontab -e
|
||||||
|
|
||||||
|
# 4. 验证
|
||||||
|
curl http://127.0.0.1:8085/api/status
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 七、验证清单
|
||||||
|
|
||||||
|
- [ ] 依赖安装完整 (Flask, pandas, openpyxl, numpy, requests, beautifulsoup4)
|
||||||
|
- [ ] systemd 服务运行正常 (active, enabled)
|
||||||
|
- [ ] Web 服务可访问 (http://192.168.1.99:8085, HTTP 200)
|
||||||
|
- [ ] API 接口正常 (/api/status, /api/generate, /api/history 等)
|
||||||
|
- [ ] 前端页面正常 (4 Tab: 号码生成、历史数据、生成记录、统计分析)
|
||||||
|
- [ ] 移动端响应式布局正常
|
||||||
|
- [ ] Cron 定时任务已配置 (每日 2:30 抓取, 3:00 备份)
|
||||||
|
- [ ] 旧版 lotto-web.service 已停止并移除
|
||||||
|
- [ ] 开机自启已配置 (systemd enable)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 八、监控要点
|
## 八、监控要点
|
||||||
|
|
||||||
1. **服务存活**:`systemctl status lotto-web` 确认 active
|
1. **服务存活**:`systemctl status lotto-app` 确认 active
|
||||||
2. **Web 可达**:`curl http://127.0.0.1:5000/api/status`
|
2. **Web 可达**:`curl http://127.0.0.1:8085/api/status`
|
||||||
3. **数据更新**:检查 `/home/vincent/Studio/lottoData/双色球历史数据.xlsx` 修改时间
|
3. **数据更新**:检查 `双色球历史数据.xlsx` 修改时间
|
||||||
4. **Cron 日志**:检查 `/home/vincent/Studio/lottoData/deploy/cron.log`
|
4. **Cron 日志**:检查 `deploy/cron.log`
|
||||||
5. **磁盘空间**:Excel 文件约 250KB,可忽略
|
5. **磁盘空间**:Excel 文件约 13KB,定期检查 `lottery/` 目录增长
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> 部署人:严维序 (opengineer) | 2026-06-29
|
> 部署人:严维序 (opengineer) | 2026-07-04
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
[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,206 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
history_loader.py — 双色球历史数据 Excel 公共加载模块
|
||||||
|
|
||||||
|
统一 Excel 格式检测和列名标准化逻辑,供 lottery.py 和 app.py 共同引用。
|
||||||
|
支持三种格式:
|
||||||
|
格式A: Row0=新列名(期号|开奖日期|红球1~6|蓝球|特别号), Row1=旧列名(开奖时间|期数|号码|...), Row2+=数据
|
||||||
|
格式B: Row0=标准列名(开奖时间|期数|号码|开机号|...), Row1+=数据
|
||||||
|
格式C: 直接含"号码"列的标准 DataFrame
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import os
|
||||||
|
from collections import Counter
|
||||||
|
import re
|
||||||
|
|
||||||
|
# 标准列名(lottery.py 和 app.py 期望的列)
|
||||||
|
LEGACY_COLUMNS = ['开奖时间', '期数', '号码', '开机号', '和值特征', '奇偶比', '大小比', '奇偶形态', '跨度', '其他']
|
||||||
|
|
||||||
|
|
||||||
|
def load_history_dataframe(history_file):
|
||||||
|
"""智能加载历史数据 Excel,兼容多种格式。
|
||||||
|
|
||||||
|
返回统一的 DataFrame,使用 LEGACY_COLUMNS 列名。
|
||||||
|
"""
|
||||||
|
if not os.path.exists(history_file):
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
raw_df = pd.read_excel(history_file, header=None)
|
||||||
|
|
||||||
|
if raw_df.empty:
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
row0_vals = raw_df.iloc[0].astype(str).tolist() if len(raw_df) > 0 else []
|
||||||
|
row1_vals = raw_df.iloc[1].astype(str).tolist() if len(raw_df) > 1 else []
|
||||||
|
|
||||||
|
has_legacy_in_row0 = any(col in row0_vals for col in ['开奖时间', '期数', '号码'])
|
||||||
|
has_legacy_in_row1 = any(col in row1_vals for col in ['开奖时间', '期数', '号码'])
|
||||||
|
has_new_cols_in_row0 = any(col in row0_vals for col in ['期号', '开奖日期', '红球 1'])
|
||||||
|
|
||||||
|
if has_new_cols_in_row0 and has_legacy_in_row1:
|
||||||
|
# 格式A:跳过 Row0(新列名) 和 Row1(旧列名),用旧列名,数据从 Row2 开始
|
||||||
|
data_df = raw_df.iloc[2:].copy()
|
||||||
|
num_cols = len(data_df.columns)
|
||||||
|
data_df.columns = LEGACY_COLUMNS[:min(num_cols, len(LEGACY_COLUMNS))] + \
|
||||||
|
[f'col_{i}' for i in range(min(num_cols, len(LEGACY_COLUMNS)), num_cols)]
|
||||||
|
elif has_legacy_in_row0:
|
||||||
|
# 格式B:Row0 就是标准列名
|
||||||
|
data_df = raw_df.iloc[1:].copy()
|
||||||
|
num_cols = len(data_df.columns)
|
||||||
|
data_df.columns = LEGACY_COLUMNS[:min(num_cols, len(LEGACY_COLUMNS))] + \
|
||||||
|
[f'col_{i}' for i in range(min(num_cols, len(LEGACY_COLUMNS)), num_cols)]
|
||||||
|
else:
|
||||||
|
# 格式C:尝试默认读取
|
||||||
|
data_df = pd.read_excel(history_file)
|
||||||
|
if '号码' not in data_df.columns:
|
||||||
|
# 可能是分列格式,尝试构建号码列
|
||||||
|
red_cols = [f'红球 {i}' for i in range(1, 7)]
|
||||||
|
if not all(c in data_df.columns for c in red_cols):
|
||||||
|
red_cols = [f'红球{i}' for i in range(1, 7)]
|
||||||
|
if all(c in data_df.columns for c in red_cols) and '蓝球' in data_df.columns:
|
||||||
|
data_df['号码'] = data_df.apply(
|
||||||
|
lambda row: _build_number_string(row, red_cols), axis=1)
|
||||||
|
|
||||||
|
num_cols = len(data_df.columns)
|
||||||
|
if not any(c in data_df.columns for c in LEGACY_COLUMNS[:3]):
|
||||||
|
data_df.columns = LEGACY_COLUMNS[:min(num_cols, len(LEGACY_COLUMNS))] + \
|
||||||
|
[f'col_{i}' for i in range(min(num_cols, len(LEGACY_COLUMNS)), num_cols)]
|
||||||
|
|
||||||
|
data_df = data_df.reset_index(drop=True)
|
||||||
|
return data_df
|
||||||
|
|
||||||
|
|
||||||
|
def _build_number_string(row, red_cols):
|
||||||
|
"""将分列红球 + 蓝球拼接为 14 位号码字符串。"""
|
||||||
|
parts = []
|
||||||
|
for c in red_cols:
|
||||||
|
val = row.get(c)
|
||||||
|
if pd.isna(val):
|
||||||
|
return None
|
||||||
|
s = str(int(val)) if isinstance(val, (int, float)) else str(val).strip()
|
||||||
|
parts.append(s.zfill(2))
|
||||||
|
blue_val = row.get('蓝球')
|
||||||
|
if pd.isna(blue_val):
|
||||||
|
return None
|
||||||
|
blue_s = str(int(blue_val)) if isinstance(blue_val, (int, float)) else str(blue_val).strip()
|
||||||
|
return ''.join(parts) + blue_s.zfill(2)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_number_string(numbers_str):
|
||||||
|
"""解析号码字符串为 (红球列表, 蓝球)。
|
||||||
|
|
||||||
|
支持以下格式:
|
||||||
|
- 拼接字符串: '08121821243001' (6红球×2位 + 1蓝球×2位)
|
||||||
|
- 加号分隔: '03,12,16,22,25,28+10'
|
||||||
|
- 空格/逗号分隔: '08 12 18 21 24 30 01'
|
||||||
|
"""
|
||||||
|
if not numbers_str or pd.isna(numbers_str):
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
s = str(numbers_str).strip()
|
||||||
|
|
||||||
|
# 情况1: 纯拼接字符串(14位或以上,无分隔符)
|
||||||
|
if re.match(r'^\d{14,}$', s):
|
||||||
|
red_balls = [int(s[i:i+2]) for i in range(0, 12, 2)]
|
||||||
|
blue_ball = int(s[12:14])
|
||||||
|
if all(1 <= b <= 33 for b in red_balls) and 1 <= blue_ball <= 16:
|
||||||
|
return red_balls, blue_ball
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
# 情况2: 加号分隔
|
||||||
|
if '+' in s:
|
||||||
|
parts = s.replace(',', ' ').replace('+', ' ').split()
|
||||||
|
if len(parts) >= 7:
|
||||||
|
try:
|
||||||
|
red_balls = [int(x) for x in parts[:6]]
|
||||||
|
blue_ball = int(parts[6])
|
||||||
|
if all(1 <= b <= 33 for b in red_balls) and 1 <= blue_ball <= 16:
|
||||||
|
return red_balls, blue_ball
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
# 情况3: 正则提取数字
|
||||||
|
number_list = re.findall(r'\d+', s)
|
||||||
|
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 <= b <= 33 for b in red_balls) and 1 <= blue_ball <= 16:
|
||||||
|
return red_balls, blue_ball
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
|
||||||
|
def compute_statistics(history_file):
|
||||||
|
"""从历史数据 Excel 计算统计信息,返回字典。"""
|
||||||
|
if not os.path.exists(history_file):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
data_df = load_history_dataframe(history_file)
|
||||||
|
|
||||||
|
red_ball_counts = Counter()
|
||||||
|
blue_ball_counts = Counter()
|
||||||
|
sum_values = []
|
||||||
|
span_values = []
|
||||||
|
|
||||||
|
for _, row in data_df.iterrows():
|
||||||
|
s = str(row.get('号码', '')).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.get('奇偶比', '')).strip()
|
||||||
|
sz = str(row.get('大小比', '')).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
|
||||||
+14
-21
@@ -243,36 +243,29 @@ class DoubleColorBallGenerator:
|
|||||||
df = df.copy()
|
df = df.copy()
|
||||||
df['号码'] = df.apply(build_number_string, axis=1)
|
df['号码'] = df.apply(build_number_string, axis=1)
|
||||||
|
|
||||||
# 重命名列到标准列名(保留原始列)
|
# 重命名列到标准列名 — 仅映射有语义对应关系的列
|
||||||
# 格式A -> 格式B 映射:
|
# 格式A -> 格式B 语义映射:
|
||||||
# 期号 -> 开奖时间(其实存的是日期)
|
# 期号 -> 开奖时间(实际存的是日期)
|
||||||
# 开奖日期 -> 期数(其实存的是期号数字)
|
# 开奖日期 -> 期数(实际存的是期号数字)
|
||||||
# 红球1 -> 号码(已在上面构建)
|
# 特别号 -> 跨度
|
||||||
# 特别说 -> 跨度
|
# 红球 1~6 和 蓝球 用于构建"号码"列后不再映射
|
||||||
# 其他列按顺序映射
|
# 格式A不含开机号/和值特征/奇偶比/大小比/奇偶形态等字段,留空
|
||||||
rename_map = {}
|
rename_map = {}
|
||||||
if '期号' in df.columns:
|
if '期号' in df.columns:
|
||||||
rename_map['期号'] = '开奖时间'
|
rename_map['期号'] = '开奖时间'
|
||||||
if '开奖日期' in df.columns:
|
if '开奖日期' in df.columns:
|
||||||
rename_map['开奖日期'] = '期数'
|
rename_map['开奖日期'] = '期数'
|
||||||
if '蓝球' in df.columns and '特别号' in df.columns:
|
if '特别号' in df.columns:
|
||||||
rename_map['特别号'] = '跨度'
|
rename_map['特别号'] = '跨度'
|
||||||
# 蓝球在格式B中不单独存在,尽量复用
|
|
||||||
# 但不开机号无直接对应
|
|
||||||
if '红球 2' in df.columns:
|
|
||||||
rename_map['红球 2'] = '开机号'
|
|
||||||
if '红球 3' in df.columns:
|
|
||||||
rename_map['红球 3'] = '和值特征'
|
|
||||||
if '红球 4' in df.columns:
|
|
||||||
rename_map['红球 4'] = '奇偶比'
|
|
||||||
if '红球 5' in df.columns:
|
|
||||||
rename_map['红球 5'] = '大小比'
|
|
||||||
if '红球 6' in df.columns:
|
|
||||||
rename_map['红球 6'] = '奇偶形态'
|
|
||||||
|
|
||||||
df = df.rename(columns=rename_map)
|
df = df.rename(columns=rename_map)
|
||||||
|
|
||||||
# 确保所有标准列都存在(补缺失列)
|
# 删除已用于构建号码列的原始分列(避免数据重复)
|
||||||
|
for col in [f'红球 {i}' for i in range(1, 7)] + ['蓝球']:
|
||||||
|
if col in df.columns:
|
||||||
|
df = df.drop(columns=[col])
|
||||||
|
|
||||||
|
# 确保所有标准列都存在(格式A缺失的字段留空)
|
||||||
for col in standard_columns:
|
for col in standard_columns:
|
||||||
if col not in df.columns:
|
if col not in df.columns:
|
||||||
df[col] = ''
|
df[col] = ''
|
||||||
|
|||||||
+13
-3
@@ -1,9 +1,19 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
双色球数据抓取 Web 服务
|
双色球数据抓取 Web 服务 [已废弃]
|
||||||
提供 Web 界面执行抓取任务和查看实时结果
|
|
||||||
监听 0.0.0.0,支持局域网访问
|
⚠️ 此模块已废弃,功能已整合到 app.py 统一入口中。
|
||||||
|
请使用 app.py 作为主服务(端口 8085),它已包含:
|
||||||
|
- /fetch 路由(抓取控制台)
|
||||||
|
- /api/fetch/status(抓取状态)
|
||||||
|
- /api/fetch/execute(触发抓取)
|
||||||
|
|
||||||
|
本文件仅保留用于历史参考,不应再独立部署。
|
||||||
|
|
||||||
|
原始功能:
|
||||||
|
提供Web界面执行抓取任务和查看实时结果
|
||||||
|
监听 0.0.0.0,支持局域网访问
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import Flask, send_from_directory, jsonify
|
from flask import Flask, send_from_directory, jsonify
|
||||||
|
|||||||
Reference in New Issue
Block a user