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:
+156
-12
@@ -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 格式:
|
||||
# 格式A(fetch_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:
|
||||
# 格式A:Row0=新列名, 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:
|
||||
# 格式B:Row0=标准列名, 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:
|
||||
|
||||
Reference in New Issue
Block a user