This commit is contained in:
JA
2026-05-28 20:23:32 +08:00
commit f6c8230248
7 changed files with 402827 additions and 0 deletions

485
data_validator.py Normal file
View File

@@ -0,0 +1,485 @@
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())