""" 高考志愿填报规范检查器 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))