Files
gaokao-volunteer-system/scripts/gaokao-checker

548 lines
21 KiB
Plaintext
Raw Normal View History

"""
高考志愿填报规范检查器 V2.0
支持多省份自动识别
"""
import re
import json
import sys
from datetime import datetime
# 各省规则库Phase 1.5: 委托 truth loader保留 legacy 接口)
from pathlib import Path
import os
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from data.rules.loader import RuleLoader
TRUTH_ROOT = Path(os.environ.get("GAOKAO_RULES_TRUTH_ROOT", str(PROJECT_ROOT / "rules" / "_truth")))
LEGACY_RULE_KEYS = (
"mode",
"batch",
"max_volunteers",
"max_majors_per_group",
"has_adjustment",
"adjustment_scope",
"retrieval_rule",
"collection_count",
"subject_mode",
"official_url",
"exam_subject_total",
)
def _scalar_from_loaded_value(value_dict, key):
return value_dict.get(key)
def _build_legacy_rule_map(truth_root):
loader = RuleLoader.from_truth_root(truth_root)
province_rules = {}
for province in loader.active_provinces():
loaded_rules = {
rule.rule_id.split(".", 1)[1]: rule.value
for rule in loader.list_province_rules(province)
}
province_rules[province] = {
key: _scalar_from_loaded_value(loaded_rules[key], key)
for key in LEGACY_RULE_KEYS
if key in loaded_rules
}
return province_rules
PROVINCE_RULES = _build_legacy_rule_map(TRUTH_ROOT)
# 省份别名
PROVINCE_ALIASES = {
"湘": "湖南",
"粤": "广东",
"鄂": "湖北",
"苏": "江苏",
"闽": "福建",
"皖": "安徽",
"赣": "江西",
"甘": "甘肃",
"陇": "甘肃",
"黑": "黑龙江",
"桂": "广西",
"京": "北京",
"沪": "上海",
"津": "天津",
"琼": "海南",
"浙": "浙江",
"鲁": "山东",
"冀": "河北",
"渝": "重庆",
"辽": "辽宁",
"黔": "贵州",
"青": "青海",
"吉": "吉林",
"豫": "河南",
"川": "四川",
"蜀": "四川",
"新": "新疆",
"滇": "云南",
"云": "云南",
"藏": "西藏",
}
def detect_province(text):
"""
从文本中自动检测省份。
优先返回文本中最早出现的省份线索,避免被后面的院校名误导。
"""
matches = []
# 1. 支持规则库中的省份全称
for prov in PROVINCE_RULES.keys():
idx = text.find(prov)
if idx != -1:
matches.append((idx, prov))
# 2. 匹配简称
for alias, prov in PROVINCE_ALIASES.items():
pattern = f"({alias}[省市区]?)|(省{alias})"
found = re.search(pattern, text)
if found:
matches.append((found.start(), prov))
# 3. 匹配更广义的省份全称,但仅在 truth 规则库支持时返回
prov_full_names = {
"北京": "北京",
"天津": "天津",
"河北": "河北",
"山西": "山西",
"内蒙古": "内蒙古",
"辽宁": "辽宁",
"吉林": "吉林",
"黑龙江": "黑龙江",
"上海": "上海",
"江苏": "江苏",
"浙江": "浙江",
"安徽": "安徽",
"福建": "福建",
"江西": "江西",
"山东": "山东",
"河南": "河南",
"湖北": "湖北",
"湖南": "湖南",
"广东": "广东",
"广西": "广西",
"海南": "海南",
"重庆": "重庆",
"四川": "四川",
"贵州": "贵州",
"云南": "云南",
"西藏": "西藏",
"陕西": "陕西",
"甘肃": "甘肃",
"青海": "青海",
"宁夏": "宁夏",
"新疆": "新疆",
}
for full_name in prov_full_names.keys():
idx = text.find(full_name)
if idx != -1 and full_name in PROVINCE_RULES:
matches.append((idx, full_name))
if not matches:
return None
matches.sort(key=lambda item: item[0])
return matches[0][1]
class GaokaoSpecCheckerV2:
"""
高考志愿填报规范检查器 V2.0
支持多省份自动识别
"""
def __init__(self, province=None):
self.province = province
self.province_rule = None
self.errors = {
"fatal": [],
"serious": [],
"warning": [],
}
def auto_detect_and_check(self, text):
"""
自动检测省份并检查
"""
# 自动检测省份
if not self.province:
self.province = detect_province(text)
if not self.province:
return self._report_no_province()
if self.province not in PROVINCE_RULES:
return self._report_unsupported_province()
self.province_rule = PROVINCE_RULES[self.province]
# 执行检查
self._check_volunteer_unit(text)
self._check_volunteer_count(text)
self._check_majors_per_group(text)
self._check_adjustment_rule(text)
self._check_data_accuracy(text)
self._check_subject_requirements(text)
self._check_risk_disclosure(text)
return self._generate_report()
def _check_volunteer_unit(self, text):
"""检查志愿单位"""
max_v = self.province_rule["max_volunteers"]
mode = self.province_rule["mode"]
if mode == "院校专业组":
# 检查1是否说"学校"或"院校"
wrong_patterns = [
f"{max_v}个学校",
f"{max_v}所学校",
f"{max_v}个院校",
]
for pattern in wrong_patterns:
if pattern in text:
self.errors["fatal"].append({
"rule": f"志愿单位错误({self.province}",
"description": f"{self.province}是{mode}模式,应该是{self.province_rule['max_volunteers']}个'{mode}',不是{max_v}个'学校'或'院校'",
"fix": f"改为'{max_v}个院校专业组'"
})
break
# 检查2模式本身
if "院校专业组" not in text and "专业组" not in text:
self.errors["serious"].append({
"rule": f"未提及'{mode}'概念({self.province}",
"description": f"{self.province}采用{mode}模式,应在方案中明确",
"fix": "明确使用'院校专业组'概念"
})
elif mode == "专业+学校":
# 浙江、山东等模式
if "专业组" in text and "组内" in text:
self.errors["fatal"].append({
"rule": f"模式错误({self.province}",
"description": f"{self.province}是'专业+学校'模式,不是'院校专业组'模式,无调剂选项",
"fix": "改为'专业+学校',删除'组内服从'等概念"
})
if "调剂" in text and "无" not in text.split("调剂")[0][-10:]:
# 简单检测:如果提到调剂但没说"无"
if "不服从" not in text and "无需" not in text and "没有" not in text:
self.errors["serious"].append({
"rule": f"调剂概念错误({self.province}",
"description": f"{self.province}采用'专业+学校'模式,**没有调剂选项**",
"fix": "删除所有'服从调剂'相关描述"
})
def _check_volunteer_count(self, text):
"""检查志愿数量"""
max_v = self.province_rule["max_volunteers"]
# 提取方案中提到的志愿数
count_patterns = [
r'共(\d+)个',
r'填报(\d+)个',
r'填了(\d+)个',
]
for pattern in count_patterns:
matches = re.findall(pattern, text)
for match in matches:
count = int(match)
if count > max_v:
self.errors["fatal"].append({
"rule": f"志愿数量超标({self.province}",
"description": f"方案提到{count}个志愿,超过{self.province}本批次的{max_v}个上限",
"fix": f"志愿数不超过{max_v}个"
})
elif count < max_v * 0.5 and "少" not in text:
self.warnings = getattr(self, 'warnings', [])
self.warnings.append({
"rule": f"志愿数量较少({self.province}",
"description": f"方案只填了{count}个,建议填满{max_v}个(除非明确不需要)",
"fix": f"建议填满{max_v}个志愿"
})
def _check_majors_per_group(self, text):
"""检查每组专业数"""
max_m = self.province_rule["max_majors_per_group"]
mode = self.province_rule["mode"]
if mode == "院校专业组" and max_m > 1:
# 院校专业组模式每组最多6个专业
if "6个专业" not in text and "六个专业" not in text:
self.errors["warning"].append({
"rule": f"专业数说明缺失({self.province}",
"description": f"未说明每个专业组最多{max_m}个专业",
"fix": f"明确说明每组最多{max_m}个专业"
})
elif mode == "专业+学校":
# 专业+学校模式每志愿1个专业
if "1个专业" not in text and "1所学校" not in text:
self.errors["warning"].append({
"rule": f"专业数说明缺失({self.province}",
"description": f"未说明{self.province}是'专业+学校'模式每志愿只填1个专业",
"fix": "明确'每个志愿1个专业'"
})
def _check_adjustment_rule(self, text):
"""检查调剂规则"""
if not self.province_rule["has_adjustment"]:
# 无调剂模式
if "服从调剂" in text and "无需" not in text and "无调剂" not in text:
self.errors["fatal"].append({
"rule": f"调剂规则错误({self.province}",
"description": f"{self.province}采用'专业+学校'模式,**没有调剂选项**",
"fix": "删除所有'服从调剂'相关描述"
})
else:
# 有调剂模式
adjustment_scope = self.province_rule["adjustment_scope"]
if "服从调剂" in text and "全部专业" in text:
if adjustment_scope == "组内专业":
self.errors["fatal"].append({
"rule": f"调剂范围错误({self.province}",
"description": f"{self.province}的调剂范围是'组内专业',不是'全部专业'",
"fix": "改为'组内专业调剂'"
})
def _check_data_accuracy(self, text):
"""检查数据准确性"""
# 主观概率
prob_pattern = r'(\d{2,3})\s*%\s*[\u4e00-\u9fa5]*(?:录取|概率|机会|把握)'
matches = re.findall(prob_pattern, text)
if matches:
self.errors["serious"].append({
"rule": "主观概率估算",
"description": f"方案中含主观概率{set(matches)},未基于真实数据",
"fix": "删除主观概率改用2025年位次作为参考"
})
# 2026年位次未说明
if "位次" in text and "2026" in text and "待官方" not in text and "以官方为准" not in text:
self.errors["serious"].append({
"rule": "2026年位次",
"description": "2026年位次待官方公布6月25日出分后不应假设",
"fix": "明确'2026年位次待官方公布'"
})
def _check_subject_requirements(self, text):
"""检查选科要求"""
if self.province_rule["subject_mode"] == "3+1+2":
# 检查是否有"物+化+生"一刀切
if re.search(r'会计.{0,20}物.{0,5}化.{0,5}生', text):
self.errors["serious"].append({
"rule": "选科要求一刀切",
"description": "财经类专业选科要求因校而异,不能假设都要求'物+化+生'",
"fix": "逐校核实选科要求"
})
def _check_risk_disclosure(self, text):
"""检查风险提示"""
risk_keywords = ["退档", "风险", "调剂", "体检", "单科"]
has_risk = any(kw in text for kw in risk_keywords)
if not has_risk:
self.errors["serious"].append({
"rule": "风险提示缺失",
"description": "方案未明确说明退档风险(体检/单科/不服从调剂)",
"fix": "增加风险提示章节"
})
def _report_no_province(self):
"""未检测到省份的报告"""
return """
╔══════════════════════════════════════════════════════════════════╗
║ ⚠️ 未检测到省份信息 ║
╚══════════════════════════════════════════════════════════════════╝
【问题】
方案中未明确省份信息,无法进行针对性检查。
【支持检测的省份】
北京、天津、河北、山西、内蒙古、辽宁、吉林、黑龙江
上海、江苏、浙江、安徽、福建、江西、山东、河南
湖北、湖南、广东、广西、海南、重庆、四川、贵州
云南、西藏、陕西、甘肃、青海、宁夏、新疆
【解决方式】
请在方案中明确省份信息,例如:
"湖南考生578分..."
"浙江省630分..."
"""
def _report_unsupported_province(self):
"""省份不支持的报告"""
return f"""
╔══════════════════════════════════════════════════════════════════╗
║ ⚠️ 暂不支持 {self.province} ║
╚══════════════════════════════════════════════════════════════════╝
【问题】
当前检查器暂不支持{self.province}的具体规则检查。
【已支持的省份】
{', '.join(sorted(PROVINCE_RULES.keys()))}
【后续计划】
将持续添加更多省份支持。
"""
def _generate_report(self):
"""生成检查报告"""
report = f"""
╔══════════════════════════════════════════════════════════════════╗
║ ✅ 志愿方案规范检查报告 ║
╠══════════════════════════════════════════════════════════════════╣
║ 检测省份:{self.province} ║
║ 志愿模式:{self.province_rule['mode']} ║
║ 志愿数量:{self.province_rule['max_volunteers']}个({self.province_rule['batch']}
║ 每组专业:{self.province_rule['max_majors_per_group']}个 ║
║ 调剂选项:{'有' if self.province_rule['has_adjustment'] else '无'} ║
║ 调剂范围:{self.province_rule['adjustment_scope']} ║
║ 选科模式:{self.province_rule['subject_mode']} ║
║ 检查时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ║
╚══════════════════════════════════════════════════════════════════╝
"""
if self.errors["fatal"]:
report += "\n🔴 【致命错误】\n" + "─" * 70 + "\n"
for i, err in enumerate(self.errors["fatal"], 1):
report += f"""
{i}. {err['rule']}
❌ 问题:{err['description']}
✅ 修正:{err['fix']}
"""
if self.errors["serious"]:
report += "\n🟡 【严重错误】\n" + "─" * 70 + "\n"
for i, err in enumerate(self.errors["serious"], 1):
report += f"""
{i}. {err['rule']}
⚠️ 问题:{err['description']}
🔧 修正:{err['fix']}
"""
if self.errors["warning"]:
report += "\n🟢 【一般警告】\n" + "─" * 70 + "\n"
for i, warn in enumerate(self.errors["warning"], 1):
report += f"""
{i}. {warn['rule']}
💡 建议:{warn['description']}
📌 做法:{warn['fix']}
"""
total = sum(len(v) for v in self.errors.values())
report += f"""
═══════════════════════════════════════════════════════════════════
📊 【检查统计】
═══════════════════════════════════════════════════════════════════
🔴 致命错误:{len(self.errors['fatal'])} 个
🟡 严重错误:{len(self.errors['serious'])} 个
🟢 一般警告:{len(self.errors['warning'])} 个
📊 问题总数:{total} 个
"""
if total == 0:
report += "\n 🎉 方案基本合规!\n"
elif len(self.errors["fatal"]) > 0:
report += "\n ❌ 必须修改致命错误后才能使用\n"
else:
report += "\n ⚠️ 建议补充完善后使用\n"
report += f"""
═══════════════════════════════════════════════════════════════════
📌 【重要提醒】
═══════════════════════════════════════════════════════════════════
• 最终以{self.province}省教育考试院官方信息为准
• 官方网址:{self.province_rule['official_url']}
• 2026年招生计划6月15-20日公布
• 2026年实际位次6月25日出分后确定
═══════════════════════════════════════════════════════════════════
"""
return report
# 主函数
if __name__ == "__main__":
if len(sys.argv) < 2:
# 默认测试
print("用法: python spec_checker_v2.py <方案文件> [省份]")
print("或: python spec_checker_v2.py (无参数时显示测试)")
print()
# 测试:湖南方案
print("=" * 70)
print("测试1湖南方案错误版")
print("=" * 70)
bad_plan = """
湖南578分考生志愿方案
本次共填报45个学校志愿
志愿01江西财经大学会计学
录取概率35%
"""
checker = GaokaoSpecCheckerV2()
print(checker.auto_detect_and_check(bad_plan))
# 测试:浙江方案
print("\n\n")
print("=" * 70)
print("测试2浙江方案专业+学校模式)")
print("=" * 70)
zj_plan = """
浙江省630分选科物化生
本次共填报80个专业+学校志愿:
志愿01浙江大学计算机科学与技术
志愿02浙江工业大学软件工程
每个志愿填1个专业。
"""
checker = GaokaoSpecCheckerV2()
print(checker.auto_detect_and_check(zj_plan))
# 测试:山东方案
print("\n\n")
print("=" * 70)
print("测试3山东方案")
print("=" * 70)
sd_plan = """
山东高考620分
填报96个志愿
01-山东大学-会计学
02-中国海洋大学-金融学
"""
checker = GaokaoSpecCheckerV2()
print(checker.auto_detect_and_check(sd_plan))
else:
# 从文件读取方案
filename = sys.argv[1]
province = sys.argv[2] if len(sys.argv) > 2 else None
with open(filename, 'r', encoding='utf-8') as f:
plan = f.read()
checker = GaokaoSpecCheckerV2(province)
print(checker.auto_detect_and_check(plan))