fix: 修复历史数据Excel格式兼容问题 + 完善开发文档

核心修复:
- lottery.py: load_history_data() 添加多格式Excel检测逻辑
  支持 格式A(双行header: 新列名+旧列名) 和 格式B(标准列名)
- lottery.py: parse_numbers() 新增拼接字符串(14位无分隔符)直接解析
  避免 re.findall 将整个号码串视为单个数字的问题
- app.py: load_history_dataframe() 同步修复多格式兼容逻辑

新增:
- docs/开发文档-双色球WebUI-v1.0.md: 完整开发文档
- deploy/backup.sh: 备份脚本

测试结果:
- 120条历史数据全部正确解析
- 号码生成API正常工作
- 全部API接口测试通过

Issue: BIZ-75
This commit is contained in:
2026-07-03 23:05:58 +08:00
parent cf4b5764b5
commit 5d5e77000e
4 changed files with 437 additions and 34 deletions
+156 -12
View File
@@ -48,35 +48,106 @@ class DoubleColorBallGenerator:
# 读取Excel文件
print(f"正在读取文件: {self.history_file}")
try:
self.history_data = pd.read_excel(self.history_file)
raw_df = pd.read_excel(self.history_file, header=None)
except Exception as excel_error:
print(f"读取Excel文件失败: {excel_error}")
return False
# 检查数据是否为空
if self.history_data.empty:
if raw_df.empty:
print("错误: 历史数据文件为空")
return False
print(f"加载成功,共{len(self.history_data)}条历史记录")
print(f"数据列: {list(self.history_data.columns)}")
# 检查是否包含必要的列
if '号码' not in self.history_data.columns:
print("错误: 历史数据文件缺少'号码'")
# 兼容多种 Excel 格式:
# 格式Afetch_data.py 当前输出): Row0=新列名(期号|开奖日期|红球1...|蓝球|特别号), Row1=旧列名(开奖时间|期数|号码|...), Row2+=数据
# 格式B(标准格式): Row0=列名(开奖时间|期数|号码|开机号|...), Row1+=数据
# 格式C(分列含旧 header: Row0=旧列名, Row1+=数据 但无"号码"列
# 标准列名(lottery.py 期望的列)
legacy_columns = ['开奖时间', '期数', '号码', '开机号', '和值特征', '奇偶比', '大小比', '奇偶形态', '跨度', '其他']
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_header_in_row0 = any(col in row0_vals for col in ['开奖时间', '期数', '号码'])
has_legacy_header_in_row1 = any(col in row1_vals for col in ['开奖时间', '期数', '号码'])
has_new_header_in_row0 = any(col in row0_vals for col in ['期号', '开奖日期', '红球 1'])
if has_new_header_in_row0 and has_legacy_header_in_row1:
# 格式ARow0=新列名, Row1=旧列名, Row2+=数据
# 用旧列名(Row1)作为列名,因为 lottery.py 期望"号码"列
self.history_data = raw_df.iloc[2:].copy()
num_cols = len(self.history_data.columns)
self.history_data.columns = legacy_columns[:min(num_cols, len(legacy_columns))] + [f'col_{i}' for i in range(min(num_cols, len(legacy_columns)), num_cols)]
self.history_data = self.history_data.reset_index(drop=True)
print(f"加载成功(格式A: 新旧 header 双行),共{len(self.history_data)}条历史记录")
print(f"数据列: {list(self.history_data.columns)}")
elif has_legacy_header_in_row0:
# 格式BRow0=标准列名, Row1+=数据
self.history_data = raw_df.iloc[1:].copy()
num_cols = len(self.history_data.columns)
self.history_data.columns = legacy_columns[:min(num_cols, len(legacy_columns))] + [f'col_{i}' for i in range(min(num_cols, len(legacy_columns)), num_cols)]
self.history_data = self.history_data.reset_index(drop=True)
print(f"加载成功(格式B: 标准列名),共{len(self.history_data)}条历史记录")
print(f"数据列: {list(self.history_data.columns)}")
else:
# 格式C:检测不到旧列名,尝试直接用 pandas 读取
self.history_data = pd.read_excel(self.history_file)
print(f"加载成功(默认读取),共{len(self.history_data)}条历史记录")
print(f"数据列: {list(self.history_data.columns)}")
# 如果没有"号码"列但有分列红球,尝试标准化
if '号码' not in self.history_data.columns:
if any(c in self.history_data.columns for c in ['红球 1', '红球1']):
self._normalize_history_format()
if self.history_data.empty:
print("错误: 历史数据文件为空")
return False
# 解析号码列
def parse_numbers(row):
"""解析单行号码数据"""
"""解析单行号码数据
支持以下格式:
- 拼接字符串: '08121821243001' (6红球×2位 + 1蓝球×2位)
- 空格/逗号分隔: '08 12 18 21 24 30 01'
- 加号分隔: '08,12,18,21,24,30+01'
"""
try:
# 处理号码字符串 - 直接转换为字符串然后分割
if pd.isna(row['号码']):
return [], 0
numbers_str = str(row['号码'])
# 使用正则表达式提取所有数字
numbers_str = str(row['号码']).strip()
# 情况1: 纯拼接字符串(14位或以上,无分隔符)
# 例如 '08121821243001' = [08,12,18,21,24,30] + [01]
if re.match(r'^\d{14,}$', numbers_str):
red_balls = [int(numbers_str[i:i+2]) for i in range(0, 12, 2)]
blue_ball = int(numbers_str[12:14])
if all(1 <= b <= 33 for b in red_balls) and 1 <= blue_ball <= 16:
return red_balls, blue_ball
else:
print(f"警告: 号码范围异常: {red_balls} + {blue_ball}")
return [], 0
# 情况2: 加号分隔(如 '03,12,16,22,25,28+10'
if '+' in numbers_str:
parts = numbers_str.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
# 情况3: 使用正则表达式提取所有数字组
number_list = re.findall(r'\d+', numbers_str)
if len(number_list) >= 7:
@@ -140,6 +211,79 @@ class DoubleColorBallGenerator:
print(traceback.format_exc())
return False
def _normalize_history_format(self):
"""将格式A(分列红球)转换为格式B(统一号码列 + 标准列名)。
格式A列名: 期号 | 开奖日期 | 红球 1 | 红球 2 | 红球 3 | 红球 4 | 红球 5 | 红球 6 | 蓝球 | 特别号
格式B列名: 开奖时间 | 期数 | 号码 | 开机号 | 和值特征 | 奇偶比 | 大小比 | 奇偶形态 | 跨度 | 其他
在 self.history_data 上原地操作,构建 '号码' 列和标准列名。
"""
df = self.history_data
standard_columns = ['开奖时间', '期数', '号码', '开机号', '和值特征', '奇偶比', '大小比', '奇偶形态', '跨度', '其他']
# 构建号码列:将 红球1~6 + 蓝球 拼接为 14 位字符串
red_cols = [f'红球 {i}' for i in range(1, 7)]
blue_col = '蓝球'
def build_number_string(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(blue_col)
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)
df = df.copy()
df['号码'] = df.apply(build_number_string, axis=1)
# 重命名列到标准列名(保留原始列)
# 格式A -> 格式B 映射:
# 期号 -> 开奖时间(其实存的是日期)
# 开奖日期 -> 期数(其实存的是期号数字)
# 红球1 -> 号码(已在上面构建)
# 特别说 -> 跨度
# 其他列按顺序映射
rename_map = {}
if '期号' in df.columns:
rename_map['期号'] = '开奖时间'
if '开奖日期' in df.columns:
rename_map['开奖日期'] = '期数'
if '蓝球' in df.columns and '特别号' in df.columns:
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)
# 确保所有标准列都存在(补缺失列)
for col in standard_columns:
if col not in df.columns:
df[col] = ''
# 调整列顺序
df = df[[c for c in standard_columns if c in df.columns] + [c for c in df.columns if c not in standard_columns]]
self.history_data = df.reset_index(drop=True)
print(f"已标准化数据格式,共 {len(df)} 条记录")
print(f"标准化后列名: {list(df.columns)}")
def _calculate_statistics(self):
"""计算统计数据"""
if self.history_data is None or len(self.history_data) == 0: