Files
config_Test/data_validator.py
2026-05-28 20:23:32 +08:00

485 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import csv
import re
from collections import defaultdict
def format_number(num):
if num >= 100000000:
return f"{num // 100000000}亿"
elif num >= 10000:
return f"{num // 10000}"
else:
return str(num)
TYPE_DESCRIPTIONS = {
1: "招募",
2: "英雄升级",
3: "英雄升阶",
4: "主角升级",
5: "装备升级",
6: "通过",
7: "击败",
8: "商店购买",
9: "消耗",
10: "消耗",
11: "登录",
12: "",
13: "挑战",
14: "挑战",
15: "获得",
16: "领取",
17: "世界频道发言"
}
TYPE_UNITS = {
1: "",
2: "",
3: "",
4: "",
5: "",
6: "关主线关卡",
7: "波敌人",
8: "",
9: "金币",
10: "勾玉",
11: "",
12: "次宝箱",
13: "次副本",
14: "次竞技场",
15: "个英雄",
16: "次游历奖励",
17: ""
}
TYPE_DESCRIPTIONS_ACHIEVEMENT = {
1: "累计招募",
2: "英雄累计升级",
3: "英雄累计升阶",
4: "主角累计升级",
5: "装备累计升级",
6: "累计通关",
7: "累计通关",
8: "商店累计消费",
9: "累计消耗",
10: "累计消耗",
11: "累计登录",
12: "",
13: "累计挑战副本",
14: "累计挑战竞技场",
15: "累计获得",
16: "累计领取",
17: "累计消耗",
18: "累计点赞好友",
19: "累计英雄委托",
20: "累计击败",
21: "战力达到",
22: "主角到达",
23: "累计委托"
}
TYPE_UNITS_ACHIEVEMENT = {
1: "",
2: "",
3: "",
4: "",
5: "",
6: "关主线关卡",
7: "波敌人",
8: "",
9: "金币",
10: "钻石",
11: "",
12: "",
13: "",
14: "",
15: "位英雄",
16: "次挂机奖励",
17: "",
18: "",
19: "",
20: "个敌人",
21: "",
22: "",
23: ""
}
TYPE_TO_OPENUI = {
1: 2,
2: 8,
3: 8,
4: 8,
5: 7,
6: 1,
7: 1,
8: 3,
9: 3,
10: 3,
11: 1,
12: 1,
13: 4,
14: 10,
15: 2,
16: 5,
17: 6
}
OPENUI_TO_DESC = {
1: "UI_MainPanel",
2: "UI_DrawCardPanel",
3: "UI_ShopPanel",
4: "UI_FBPanel",
5: "UI_IdlePanel",
6: "UI_ChatPanel",
7: "UI_EquipPanel",
8: "UI_HeroPanel",
9: "UI_FriendPanel",
10: "UI_ArePanel"
}
CATEGORY_SCORE_MAP = {
2: 20,
5: 20,
6: 20,
10: 20,
9: 25
}
CATEGORY_DESCRIPTIONS = {
1: "主线任务",
2: "日常任务",
3: "通行证任务",
4: "修炼任务",
5: "周常任务",
6: "公会任务",
7: "委托任务",
8: "称号任务",
9: "成就任务",
10: "七日任务"
}
class ValidationError:
def __init__(self, row_num, field, error_type, message):
self.row_num = row_num
self.field = field
self.error_type = error_type
self.message = message
def __repr__(self):
return f"{self.row_num} [{self.field}] {self.error_type}: {self.message}"
class QuestValidator:
def __init__(self):
self.errors = []
self.all_ids = set()
self.id_to_row = {}
self.category_ids = defaultdict(list)
def _add_error(self, row_num, field, error_type, message):
self.errors.append(ValidationError(row_num, field, error_type, message))
def _parse_int(self, value, default=None):
try:
return int(value)
except (ValueError, TypeError):
return default
def _parse_str(self, value):
if value is None:
return ""
return str(value).strip()
def validate_id(self, row_num, id_val, category):
id_str = str(id_val)
if len(id_str) != 5:
self._add_error(row_num, "id", "格式错误", f"id必须为5位数字当前值: {id_val}")
return False
if not id_str.isdigit():
self._add_error(row_num, "id", "格式错误", f"id必须为纯数字当前值: {id_val}")
return False
expected_category = int(id_str[0])
if expected_category != category:
self._add_error(row_num, "id", "逻辑错误", f"id第一位({expected_category})与category({category})不匹配")
return False
return True
def validate_category(self, row_num, category):
if category not in CATEGORY_DESCRIPTIONS:
self._add_error(row_num, "category", "枚举错误", f"category值{category}不在允许范围1-10内")
return False
return True
def validate_next(self, row_num, next_val, category):
if next_val < 0:
self._add_error(row_num, "next", "范围错误", f"next不能为负数当前值: {next_val}")
return False
if category != 1:
if next_val != 0:
self._add_error(row_num, "next", "规则错误", f"category≠1时next必须为0当前值: {next_val}")
return False
else:
if next_val != 0 and next_val not in self.all_ids:
self._add_error(row_num, "next", "引用错误", f"next={next_val}引用的id不存在")
return False
return True
def validate_desc(self, row_num, desc, type_val, target, category):
desc = self._parse_str(desc)
if not desc:
self._add_error(row_num, "desc", "空值错误", "desc字段不能为空")
return False
if not desc.endswith(""):
self._add_error(row_num, "desc", "格式错误", f"desc必须以中文句号''结尾,当前值: '{desc}'")
return False
if category == 9:
if type_val not in TYPE_DESCRIPTIONS_ACHIEVEMENT:
return True
formatted_target = format_number(target)
expected_desc = f"{TYPE_DESCRIPTIONS_ACHIEVEMENT[type_val]}{formatted_target}{TYPE_UNITS_ACHIEVEMENT[type_val]}"
else:
if type_val not in TYPE_DESCRIPTIONS:
return True
formatted_target = format_number(target)
expected_desc = f"{TYPE_DESCRIPTIONS[type_val]}{formatted_target}{TYPE_UNITS[type_val]}"
if desc != expected_desc:
self._add_error(row_num, "desc", "逻辑错误", f"desc与type/target不匹配期望: '{expected_desc}',实际: '{desc}'")
return False
return True
def validate_type(self, row_num, type_val, category):
if category == 9:
if type_val < 1 or type_val > 23:
self._add_error(row_num, "type", "枚举错误", f"category=9(成就)时type值{type_val}不在允许范围1-23内")
return False
else:
if type_val < 1 or type_val > 17:
self._add_error(row_num, "type", "枚举错误", f"category≠9时type值{type_val}不在允许范围1-17内")
return False
return True
def validate_target(self, row_num, target):
if target < 1:
self._add_error(row_num, "target", "范围错误", f"target必须为正整数当前值: {target}")
return False
return True
def validate_openUI_type(self, row_num, openUI_type, type_val):
if openUI_type < 1 or openUI_type > 10:
self._add_error(row_num, "openUI_type", "枚举错误", f"openUI_type值{openUI_type}不在允许范围1-10内")
return False
if type_val in TYPE_TO_OPENUI:
expected = TYPE_TO_OPENUI[type_val]
if openUI_type != expected:
self._add_error(row_num, "openUI_type", "映射错误", f"type={type_val}时openUI_type应为{expected},当前值: {openUI_type}")
return False
return True
def validate_openUI_desc(self, row_num, openUI_desc, openUI_type):
if openUI_type in OPENUI_TO_DESC:
expected = OPENUI_TO_DESC[openUI_type]
if self._parse_str(openUI_desc) != expected:
self._add_error(row_num, "openUI_desc", "映射错误", f"openUI_type={openUI_type}时openUI_desc应为'{expected}',当前值: '{openUI_desc}'")
return False
return True
def validate_score(self, row_num, score, category):
expected = CATEGORY_SCORE_MAP.get(category, 0)
if score != expected:
self._add_error(row_num, "score", "计算错误", f"category={category}({CATEGORY_DESCRIPTIONS.get(category,'未知')})时score应为{expected},当前值: {score}")
return False
return True
def validate_auto(self, row_num, auto):
if auto != 0:
self._add_error(row_num, "auto", "固定值错误", f"auto必须为0当前值: {auto}")
return False
return True
def validate_extra(self, row_num, extra):
extra_str = self._parse_str(extra).lower()
if extra_str != "" and extra_str != "null":
self._add_error(row_num, "extra", "固定值错误", f"extra必须为null当前值: {extra}")
return False
return True
def validate_id_continuity(self):
for category, ids in self.category_ids.items():
sorted_ids = sorted(ids)
expected_base = category * 10000
for i, id_val in enumerate(sorted_ids):
expected = expected_base + (i + 1)
if id_val != expected:
row_num = self.id_to_row[id_val]
self._add_error(row_num, "id", "连续性错误", f"category={category}的id不连续期望: {expected},实际: {id_val}")
def validate_row(self, row_num, row):
errors_before = len(self.errors)
id_val = self._parse_int(row.get('id'))
category = self._parse_int(row.get('category'))
next_val = self._parse_int(row.get('next'), 0)
desc = row.get('desc')
type_val = self._parse_int(row.get('type'))
target = self._parse_int(row.get('target'))
openUI_type = self._parse_int(row.get('openUI_type'))
openUI_desc = row.get('openUI_desc')
score = self._parse_int(row.get('score'), 0)
auto = self._parse_int(row.get('auto'), 0)
extra = row.get('extra')
if id_val is None:
self._add_error(row_num, "id", "空值错误", "id字段不能为空")
return
if category is None:
self._add_error(row_num, "category", "空值错误", "category字段不能为空")
return
self.validate_id(row_num, id_val, category)
self.validate_category(row_num, category)
self.validate_type(row_num, type_val, category)
if type_val is not None:
if (category == 9 and type_val >= 1 and type_val <= 23) or (category != 9 and type_val >= 1 and type_val <= 17):
if target is None:
self._add_error(row_num, "target", "空值错误", "target字段不能为空")
else:
self.validate_target(row_num, target)
self.validate_desc(row_num, desc, type_val, target, category)
if openUI_type is not None:
self.validate_openUI_type(row_num, openUI_type, type_val)
self.validate_openUI_desc(row_num, openUI_desc, openUI_type)
self.validate_next(row_num, next_val, category)
self.validate_score(row_num, score, category)
self.validate_auto(row_num, auto)
self.validate_extra(row_num, extra)
return len(self.errors) == errors_before
def validate_file(self, file_path):
self.errors = []
self.all_ids = set()
self.id_to_row = {}
self.category_ids = defaultdict(list)
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
if len(lines) < 3:
return self.errors
fieldnames = lines[2].strip().split(',')
data_lines = lines[3:]
reader = csv.DictReader(data_lines, fieldnames=fieldnames)
row_num = 4
all_rows = []
for row in reader:
all_rows.append((row_num, row))
id_val = self._parse_int(row.get('id'))
category = self._parse_int(row.get('category'))
if id_val is not None and category is not None:
self.all_ids.add(id_val)
self.category_ids[category].append(id_val)
self.id_to_row[id_val] = row_num
row_num += 1
for row_num, row in all_rows:
self.validate_row(row_num, row)
self.validate_id_continuity()
return self.errors
def generate_report(self, file_path):
errors = self.validate_file(file_path)
report = []
report.append("=" * 80)
report.append("Quest 表数据校验报告")
report.append("=" * 80)
report.append(f"校验文件: {file_path}")
report.append(f"校验时间: {self._get_timestamp()}")
report.append("")
if not errors:
report.append("[OK] 所有数据校验通过")
report.append("")
report.append("校验统计:")
report.append(f" - 校验记录数: {len(self.all_ids)}")
report.append(f" - 错误数: 0")
for category, ids in sorted(self.category_ids.items()):
report.append(f" - {CATEGORY_DESCRIPTIONS.get(category, f'category={category}')}: {len(ids)}")
else:
report.append(f"[FAIL] 发现 {len(errors)} 个错误")
report.append("")
error_by_type = defaultdict(list)
for error in errors:
error_by_type[error.error_type].append(error)
for error_type, error_list in sorted(error_by_type.items(), key=lambda x: len(x[1]), reverse=True):
report.append(f"{error_type}】({len(error_list)}个)")
report.append("-" * 60)
for error in error_list[:10]:
report.append(f" {error}")
if len(error_list) > 10:
report.append(f" ... 还有 {len(error_list) - 10} 个错误")
report.append("")
report.append("错误类型统计:")
report.append("-" * 60)
for error_type, error_list in sorted(error_by_type.items(), key=lambda x: len(x[1]), reverse=True):
report.append(f" {error_type}: {len(error_list)}")
report.append("=" * 80)
return "\n".join(report)
def _get_timestamp(self):
from datetime import datetime
return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def main():
import argparse
parser = argparse.ArgumentParser(description='Quest表数据校验工具')
parser.add_argument('-f', '--file', required=True, help='要校验的CSV文件路径')
parser.add_argument('-o', '--output', help='校验报告输出文件路径')
args = parser.parse_args()
validator = QuestValidator()
report = validator.generate_report(args.file)
print(report)
if args.output:
with open(args.output, 'w', encoding='utf-8') as f:
f.write(report)
print(f"\n报告已保存至: {args.output}")
return len(validator.errors)
if __name__ == '__main__':
exit(main())