"""增量补齐2025年青海/山西一分一段与西藏/新疆控制线。 只修改脚本中明确列出的省份和数据集;所有来源均保存原件、SHA-256 和导入审计记录。 """ import argparse import hashlib import os import subprocess import uuid from io import StringIO from pathlib import Path import pandas as pd YEAR = 2025 def sql_string(value): if value is None: return "NULL" text = str(value).replace("\\", "\\\\").replace("'", "\\'") text = text.replace("\x00", "").replace("\r", " ").replace("\n", " ") return f"'{text}'" def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest() def parse_score_rows(path): html_text = path.read_text(encoding="utf-8") candidates = [] for table in pd.read_html(StringIO(html_text)): if table.shape[1] not in {3, 7}: continue rows = [] triples = [(0, 1, 2)] if table.shape[1] == 3 else [(0, 1, 2), (4, 5, 6)] for score_column, same_column, cumulative_column in triples: for _, row in table.iterrows(): score_label = str(row.iloc[score_column]) if "以下" in score_label or "缺考" in score_label: continue import re match = re.match(r"^\s*(\d+)(?:\s*[-—]\s*\d+)?", score_label) if not match: continue try: score = int(match.group(1)) same = int(float(row.iloc[same_column])) cumulative = int(float(row.iloc[cumulative_column])) except (TypeError, ValueError): continue rows.append((score, same, cumulative)) if len(rows) >= 100: candidates.append(rows) if len(candidates) != 1: raise ValueError(f"{path} 可解析的一分一段表数量异常:{len(candidates)}") rows = candidates[0] if len({row[0] for row in rows}) != len(rows): raise ValueError(f"{path} 存在重复分数") if any(rows[index][2] != rows[index - 1][2] + rows[index][1] for index in range(1, len(rows))): raise ValueError(f"{path} 累计人数校验失败") return [ (score, same, cumulative, max(1, cumulative - same + 1), cumulative, index + 1) for index, (score, same, cumulative) in enumerate(rows) ] def mysql_command(args, sql, capture=False): if not os.environ.get("MYSQL_PWD"): raise RuntimeError("必须通过 MYSQL_PWD 环境变量提供数据库密码") command = [ args.mysql_bin, "--protocol=TCP", "-h", args.host, "-P", str(args.port), "-u", args.user, "--default-character-set=utf8mb4", "-D", args.database, ] if capture: command.extend(["-N", "-B"]) process = subprocess.run(command, input=sql, text=True, encoding="utf-8", capture_output=True) if process.returncode: raise RuntimeError(process.stderr.strip() or process.stdout.strip()) return process.stdout def source_sql(path, source_type, publisher, title, landing_url, file_url, published_on, notes): sha256 = digest(path) return f""" INSERT INTO data_sources ( source_type, publisher, title, landing_page_url, file_url, published_on, retrieved_at, sha256, raw_file_path, notes ) VALUES ( {sql_string(source_type)}, {sql_string(publisher)}, {sql_string(title)}, {sql_string(landing_url)}, {sql_string(file_url)}, {sql_string(published_on)}, NOW(), {sql_string(sha256)}, {sql_string(str(path.resolve()))}, {sql_string(notes)} ) AS incoming ON DUPLICATE KEY UPDATE source_type=incoming.source_type, publisher=incoming.publisher, title=incoming.title, landing_page_url=incoming.landing_page_url, file_url=incoming.file_url, published_on=incoming.published_on, retrieved_at=incoming.retrieved_at, raw_file_path=incoming.raw_file_path, notes=incoming.notes; SET @source_id=(SELECT source_id FROM data_sources WHERE sha256={sql_string(sha256)}); """ def ingestion_sql(dataset_name, rows_read, message): run_id = str(uuid.uuid4()) return f""" INSERT INTO ingestion_runs ( ingestion_run_id, source_id, dataset_name, dataset_year, started_at, finished_at, status, rows_read, rows_inserted, rows_updated, rows_rejected, message ) VALUES ( {sql_string(run_id)}, @source_id, {sql_string(dataset_name)}, {YEAR}, NOW(), NOW(), 'success', {rows_read}, {rows_read}, 0, 0, {sql_string(message)} ); """ def score_values(province, subject, rows): return [ "(" + ",".join([ sql_string(province), str(YEAR), sql_string("普通类"), sql_string(subject), str(score), str(same), str(cumulative), str(rank_from), str(rank_to), str(sequence), "@source_id", ]) + ")" for score, same, cumulative, rank_from, rank_to, sequence in rows ] def insert_score_sql(province, subject, rows): statements = [] values = score_values(province, subject, rows) for start in range(0, len(values), 300): statements.append(""" INSERT INTO score_rank_points ( province_name, admission_year, admission_category, subject_category, score, same_score_count, cumulative_count, rank_from, rank_to, source_sequence, source_id ) VALUES """ + ",\n".join(values[start:start + 300]) + ";") return "\n".join(statements) def coverage_sql(province, dataset, status, count, notes, subject="", category=""): return f""" INSERT INTO dataset_coverage ( dataset_name, province_name, admission_year, admission_category, subject_category, segment_code, coverage_status, record_count, notes, source_id ) VALUES ( {sql_string(dataset)}, {sql_string(province)}, {YEAR}, {sql_string(category)}, {sql_string(subject)}, '', {sql_string(status)}, {count}, {sql_string(notes)}, @source_id ) AS incoming ON DUPLICATE KEY UPDATE coverage_status=incoming.coverage_status, record_count=incoming.record_count, notes=incoming.notes, source_id=incoming.source_id; """ def control_values(province, records): return ",\n".join( "(" + ",".join([ sql_string(province), str(YEAR), sql_string(category), sql_string(subject), sql_string(level), str(culture), "NULL" if professional is None else str(professional), sql_string(notes), "@source_id", ]) + ")" for category, subject, level, culture, professional, notes in records ) TIBET_CONTROLS = [ (f"普通类({candidate}类考生)", subject, level, score, None, "普通生源;A/B类考生口径依原公告。") for candidate, values in { "A": {"文史类": [("本科一批", 338), ("本科二批", 304), ("专科批", 255)], "理工类": [("本科一批", 300), ("本科二批", 266), ("专科批", 222)]}, "B": {"文史类": [("本科一批", 410), ("本科二批", 315), ("专科批", 255)], "理工类": [("本科一批", 400), ("本科二批", 305), ("专科批", 222)]}, }.items() for subject, levels in values.items() for level, score in levels ] + [ (f"艺术体育类({candidate}类考生)", subject, level, score, None, "文化成绩控制线;艺术类与体育类专业成绩及排序规则依原公告。") for candidate, values in { "A": {"文史类": [("本科", 228), ("专科", 179)], "理工类": [("本科", 200), ("专科", 155)]}, "B": {"文史类": [("本科", 236), ("专科", 179)], "理工类": [("本科", 229), ("专科", 155)]}, }.items() for subject, levels in values.items() for level, score in levels ] + [ ("部队生源", "文史类", "本科", 257, None, "部队生源招生录取最低控制分数线。"), ("部队生源", "理工类", "本科", 205, None, "部队生源招生录取最低控制分数线。"), ] XINJIANG_CONTROLS = [ (category, subject, level, score, None, "普通高考文化课最低投档控制分数线。") for category, values in { "普通类计划": {"文科": [("本科一批次", 451), ("本科二批次", 330), ("高职(专科)批次", 140)], "理科": [("本科一批次", 421), ("本科二批次", 280), ("高职(专科)批次", 140)]}, "单列类计划": {"文科": [("本科一批次", 382), ("本科二批次", 319), ("高职(专科)批次", 140)], "理科": [("本科一批次", 350), ("本科二批次", 276), ("高职(专科)批次", 140)]}, }.items() for subject, levels in values.items() for level, score in levels ] + [ (f"体育类({plan}计划)", "不分科类", level, culture, professional, "文化课、术科成绩均须达到控制线。") for plan, values in { "普通类": [("本科提前批次", 245, 86), ("高职(专科)提前批次", 140, 82)], "单列类": [("本科提前批次", 240, 89), ("高职(专科)提前批次", 140, 82)], }.items() for level, culture, professional in values ] + [ (f"艺术类({plan}计划)", specialty, level, culture, professional, "文化课、术科成绩均须达到控制线;美术本科另要求21门科目成绩各不低于60分。" if specialty == "美术与设计类" and level == "本科提前批次B段" else "文化课、术科成绩均须达到控制线。") for plan, culture_undergrad, culture_vocational in [("普通类", 210, 140), ("单列类", 207, 140)] for level, culture, specialties in [ ("本科提前批次B段", culture_undergrad, [ ("美术与设计类", 180), ("音乐类", 170), ("播音与主持类", 185), ("书法类", 190), ("舞蹈类", 145), ("戏剧影视导演方向", 185), ("戏剧影视表演方向", 185), ("服装表演方向", 180), ]), ("高职(专科)提前批次", culture_vocational, [ ("美术与设计类", 170), ("音乐类", 170), ("播音与主持类", 185), ("书法类", 190), ("舞蹈类", 145), ("戏剧影视导演方向", 185), ("戏剧影视表演方向", 185), ("服装表演方向", 180), ]), ] for specialty, professional in specialties ] QINGHAI_CONTROLS = [ ("普通类", subject, level, score, None, "普通班口径;本科含国家、地方、省内专项及普通民族班、民族预科班。") for subject, levels in { "历史类": [("特殊类型", 450), ("本科批", 405), ("高职(专科)批", 150)], "物理类": [("特殊类型", 420), ("本科批", 350), ("高职(专科)批", 150)], }.items() for level, score in levels ] + [ ("高校民族语言授课(藏文)", "历史类", "本科普通班及高职(专科)普通班", 366, None, "藏文投档成绩,适用于本科普通班(含民族预科班)和高职(专科)普通班。"), ("高校民族语言授课(藏文)", "物理类", "本科普通班及高职(专科)普通班", 333, None, "藏文投档成绩,适用于本科普通班(含民族预科班)和高职(专科)普通班。"), ("高校民族语言授课(蒙文)", "历史类", "本科普通班", 280, None, "蒙文投档成绩,计算口径依原通知。"), ("高校民族语言授课(蒙文)", "物理类", "本科普通班", 300, None, "蒙文投档成绩,计算口径依原通知。"), ("体育类", "历史类", "本科普通班", 385, None, "省统考合格考生投档成绩,非单独文化课线。"), ("体育类", "历史类", "高职(专科)普通班", 300, None, "省统考合格考生投档成绩,非单独文化课线。"), ("体育类", "物理类", "本科普通班(含普通民族班)", 335, None, "省统考合格考生投档成绩,非单独文化课线。"), ("体育类", "物理类", "高职(专科)普通班", 224, None, "省统考合格考生投档成绩,非单独文化课线。"), ] + [ ("艺术类", subject, f"{level}-{specialty}", score, None, "艺术类专业省级统考合格考生的投档成绩,计算方法依原通知。") for specialty in [ "音乐类", "美术与设计类", "舞蹈类", "播音与主持类", "书法类", "戏剧影视导演", "服装表演", ] for subject, score_undergrad in [("历史类", 304), ("物理类", 263)] for level, score in [("本科", score_undergrad), ("高职(专科)", 150)] ] + [ ("艺术类", subject, f"{level}-戏剧影视表演", score, None, "艺术类专业省级统考合格考生的投档成绩,计算方法依原通知。") for subject, score_undergrad in [("历史类", 296), ("物理类", 263)] for level, score in [("本科", score_undergrad), ("高职(专科)", 150)] ] SHANXI_ART_PROFESSIONAL = [ ("音乐表演(声乐)", 230), ("音乐教育(声乐主项)", 208), ("音乐表演(器乐)", 240), ("音乐教育(器乐主项)", 230), ("戏剧影视表演", 240), ("戏剧影视导演", 247), ("服装表演", 228), ("播音与主持", 241), ("美术与设计", 197), ("舞蹈", 224), ("书法", 236), ] SHANXI_CONTROLS = [ ("普通类", "历史类", "普通本科批", 443, None, "普通本科录取最低控制分数线。"), ("普通类", "历史类", "特殊类型", 534, None, "特殊类型录取控制分数线。"), ("普通类", "物理类", "普通本科批", 419, None, "普通本科录取最低控制分数线。"), ("普通类", "物理类", "特殊类型", 507, None, "特殊类型录取控制分数线。"), ("艺术类", "历史类", "艺术本科提前批-校考", 443, None, "专业成绩须校考合格。"), ("艺术类", "物理类", "艺术本科提前批-校考", 419, None, "专业成绩须校考合格。"), ("艺术类", "历史类", "艺术本科提前批-戏曲省际联考", 222, 180, "文化课与省际联考专业成绩控制线。"), ("艺术类", "物理类", "艺术本科提前批-戏曲省际联考", 210, 180, "文化课与省际联考专业成绩控制线。"), ] + [ ("艺术类", subject, f"艺术本科批-{specialty}", culture, professional, "文化课与专业省级统考成绩均须达到控制线。") for subject, culture in [("历史类", 332), ("物理类", 314)] for specialty, professional in SHANXI_ART_PROFESSIONAL ] + [ ("体育类", "历史类", "体育本科批", 354, 85, "文化课与体育专业成绩控制线。"), ("体育类", "物理类", "体育本科批", 335, 85, "文化课与体育专业成绩控制线。"), ("普通类", "历史类", "普通专科(高职)提前批及普通专科(高职)批", 100, None, "专科(高职)控制线。"), ("普通类", "物理类", "普通专科(高职)提前批及普通专科(高职)批", 100, None, "专科(高职)控制线。"), ("艺术类", "历史类", "艺术专科(高职)批", 75, None, "须取得相应科类专业统考成绩。"), ("艺术类", "物理类", "艺术专科(高职)批", 75, None, "须取得相应科类专业统考成绩。"), ("体育类", "历史类", "体育专科(高职)批", 80, None, "须取得体育专业考试成绩。"), ("体育类", "物理类", "体育专科(高职)批", 80, None, "须取得体育专业考试成绩。"), ] def validate_shanxi_existing(args): sql = """ SELECT subject_category, COUNT(*), CAST(MAX(score) AS UNSIGNED), CAST(MIN(score) AS UNSIGNED), MAX(cumulative_count) FROM score_rank_points WHERE province_name='山西省' AND admission_year=2025 AND subject_category LIKE '未标注序列%' GROUP BY subject_category ORDER BY subject_category; """ rows = [line.split("\t") for line in mysql_command(args, sql, capture=True).strip().splitlines()] expected = [ ["未标注序列1", "285", "703", "419", "120285"], ["未标注序列2", "228", "670", "443", "35877"], ] if rows == expected: return "unlabeled" # 允许脚本重复运行,重复运行时应已经是完整的物理/历史分类。 sql = """ SELECT subject_category, COUNT(*), CAST(MAX(score) AS UNSIGNED), CAST(MIN(score) AS UNSIGNED), MAX(cumulative_count) FROM score_rank_points WHERE province_name='山西省' AND admission_year=2025 AND subject_category IN ('物理类','历史类') GROUP BY subject_category ORDER BY subject_category; """ labeled = [line.split("\t") for line in mysql_command(args, sql, capture=True).strip().splitlines()] expected_labeled = [ ["历史类", "229", "671", "443", "35877"], ["物理类", "286", "704", "419", "120285"], ] if labeled == expected_labeled: return "labeled" raise ValueError(f"山西既有序列不符合预期,停止修改:{rows or labeled}") def main(): parser = argparse.ArgumentParser(description="增量导入2025年缺失的官方公开数据") parser.add_argument("--mysql-bin", required=True) parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=3306) parser.add_argument("--user", default="root") parser.add_argument("--database", default="zhiyuantong") parser.add_argument("--root", default=str(Path(__file__).resolve().parents[1])) args = parser.parse_args() root = Path(args.root) qh_history_path = root / "data/raw/qinghai/2025/score_rank_历史类_2025.html" qh_physics_path = root / "data/raw/qinghai/2025/score_rank_物理类_2025.html" sx_physics_path = root / "data/raw/shanxi/2025/score_rank_物理类_2025.html" sx_history_image = root / "data/raw/shanxi/2025/score_rank_历史类_2025.png" tibet_path = root / "data/raw/tibet/2025/control_lines_2025.html" xinjiang_image = root / "data/raw/xinjiang/2025/control_lines_2025.jpg" qinghai_controls_pdf = root / "data/raw/qinghai/2025/control_lines_2025.pdf" shanxi_controls_bachelor = root / "data/raw/shanxi/2025/control_lines_bachelor_2025.html" shanxi_controls_vocational = root / "data/raw/shanxi/2025/control_lines_vocational_2025.html" for path in [ qh_history_path, qh_physics_path, sx_physics_path, sx_history_image, tibet_path, xinjiang_image, qinghai_controls_pdf, shanxi_controls_bachelor, shanxi_controls_vocational, ]: if not path.exists(): raise FileNotFoundError(path) qh_history = parse_score_rows(qh_history_path) qh_physics = parse_score_rows(qh_physics_path) sx_physics = parse_score_rows(sx_physics_path) if (len(qh_history), len(qh_physics), len(sx_physics)) != (638, 662, 286): raise ValueError("一分一段解析行数与已核验快照不一致") shanxi_state = validate_shanxi_existing(args) statements = ["SET NAMES utf8mb4;", "SET time_zone='+08:00';", "START TRANSACTION;"] statements.append("DELETE FROM score_rank_points WHERE province_name='青海省' AND admission_year=2025 AND admission_category='普通类';") statements.append("DELETE FROM dataset_coverage WHERE province_name='青海省' AND admission_year=2025 AND dataset_name='一分一段';") for subject, path, rows, url in [ ("历史类", qh_history_path, qh_history, "https://gaokao.eol.cn/qing_hai/dongtai/202506/t20250625_2677002.shtml"), ("物理类", qh_physics_path, qh_physics, "https://gaokao.eol.cn/qing_hai/dongtai/202506/t20250625_2677001.shtml"), ]: statements.append(source_sql( path, "official_republication_html", "中国教育在线(转载青海省教育招生考试院公开数据)", f"青海省2025年普通高考一分一段表({subject})", url, None, "2025-06-25", "公开转载页正文表格;累计人数逐行校验通过,原件及SHA-256已归档。", )) statements.append(insert_score_sql("青海省", subject, rows)) statements.append(coverage_sql( "青海省", "一分一段", "complete_public_snapshot", len(rows), f"完整导入公开表{len(rows)}个分数点;累计人数逐行校验通过,最高分段按公开隐私合并口径保存。", subject, "普通类", )) statements.append(ingestion_sql(f"qinghai_2025_score_rank_{subject}", len(rows), f"青海{subject}一分一段导入并校验通过")) statements.append("DELETE FROM dataset_coverage WHERE province_name='山西省' AND admission_year=2025 AND dataset_name='一分一段';") statements.append(source_sql( sx_physics_path, "official_republication_html", "中国教育在线(转载山西省招生考试管理中心公开数据)", "山西省2025年普通高考成绩(物理科目组合)一分一段统计表", "https://gaokao.eol.cn/shan_xi/dongtai/202506/t20250626_2677293.shtml", None, "2025-06-26", "公开转载页正文表格;与原未标注序列逐分核对一致,并补回704分最高汇总行。", )) statements.append("DELETE FROM score_rank_points WHERE province_name='山西省' AND admission_year=2025 AND subject_category IN ('未标注序列1','物理类');") statements.append(insert_score_sql("山西省", "物理类", sx_physics)) statements.append(coverage_sql( "山西省", "一分一段", "complete_public_snapshot", len(sx_physics), "物理科目组合公开表286个分数点;累计人数逐行校验通过。", "物理类", "普通类", )) statements.append(ingestion_sql("shanxi_2025_score_rank_物理类", len(sx_physics), "山西物理类序列完成分类并补回最高分汇总行")) statements.append(source_sql( sx_history_image, "official_republication_image", "中国教育在线(图片水印标注山西省招生考试管理中心)", "山西省2025年普通高考成绩(历史科目组合)一分一段统计表", "https://gaokao.eol.cn/shan_xi/dongtai/202506/t20250626_2677335.shtml", "https://t4.chei.com.cn/news/img/2293390948.png", "2025-06-26", "公开图片表;与原未标注序列逐分核对一致,并人工核验671分以上汇总行。", )) if shanxi_state == "unlabeled": statements.append(""" UPDATE score_rank_points SET subject_category='历史类', source_id=@source_id, source_sequence=source_sequence-286 WHERE province_name='山西省' AND admission_year=2025 AND subject_category='未标注序列2'; INSERT INTO score_rank_points ( province_name, admission_year, admission_category, subject_category, score, same_score_count, cumulative_count, rank_from, rank_to, source_sequence, source_id ) VALUES ('山西省',2025,'普通类','历史类',671,3,11,9,11,1,@source_id); """) else: statements.append("UPDATE score_rank_points SET source_id=@source_id WHERE province_name='山西省' AND admission_year=2025 AND subject_category='历史类';") statements.append(coverage_sql( "山西省", "一分一段", "complete_public_snapshot", 229, "历史科目组合公开表229个分数点;原序列逐分核对一致,已补回671分以上隐私汇总行。", "历史类", "普通类", )) statements.append(ingestion_sql("shanxi_2025_score_rank_历史类", 229, "山西历史类序列完成分类并补回最高分汇总行")) for province, path, source_type, publisher, title, landing_url, file_url, records in [ ("西藏自治区", tibet_path, "official_html", "西藏自治区人民政府(信息来源:西藏自治区教育考试院)", "西藏自治区2025年普通高考分数线", "https://www.xizang.gov.cn/xwzx_406/bmkx/202506/t20250626_486252.html", None, TIBET_CONTROLS), ("新疆维吾尔自治区", xinjiang_image, "official_image", "新疆维吾尔自治区教育考试院", "新疆2025年普通高校招生各批次最低投档控制分数线", "https://www.xjzk.gov.cn/c/2025-06-25/494441.shtml", "https://www.xjzk.gov.cn/upload/resources/image/2025/06/25/29511.jpg", XINJIANG_CONTROLS), ]: statements.append(source_sql( path, source_type, publisher, title, landing_url, file_url, "2025-06-25", "官方公开页或官方图片;已保存原件、SHA-256并人工核对字段。", )) statements.append(f"DELETE FROM control_score_lines WHERE province_name={sql_string(province)} AND admission_year={YEAR};") statements.append(""" INSERT INTO control_score_lines ( province_name, admission_year, admission_category, subject_category, education_level, culture_score, professional_score, notes, source_id ) VALUES """ + control_values(province, records) + ";") statements.append(f"DELETE FROM dataset_coverage WHERE province_name={sql_string(province)} AND admission_year={YEAR} AND dataset_name='普通类控制线';") statements.append(coverage_sql( province, "普通类控制线", "complete_public_snapshot", len(records), f"已按官方2025年控制线公告完整结构化{len(records)}条,包含公告中公开的普通、艺体及特殊类别口径。", )) statements.append(ingestion_sql(f"{province}_2025_control_lines", len(records), f"{province}官方控制线完整导入")) statements.append(source_sql( qinghai_controls_pdf, "official_pdf_republication", "青海省教育招生考试院", "关于发布2025年普通高等学校在青招生录取控制分数线的通知", "https://gaokao.chsi.com.cn/gkxx/zc/ss/202506/20250625/2293390791.html", "https://imgs.app.gaokaozhitongche.com/resource/pdf/paper/202506/20250625043757_gmn4h2.pdf", "2025-06-25", "省考试院通知的公开PDF镜像;4页均已渲染人工核验,原件及SHA-256已归档。", )) statements.append("DELETE FROM control_score_lines WHERE province_name='青海省' AND admission_year=2025;") statements.append(""" INSERT INTO control_score_lines ( province_name, admission_year, admission_category, subject_category, education_level, culture_score, professional_score, notes, source_id ) VALUES """ + control_values("青海省", QINGHAI_CONTROLS) + ";") statements.append("DELETE FROM dataset_coverage WHERE province_name='青海省' AND admission_year=2025 AND dataset_name='普通类控制线';") statements.append(coverage_sql( "青海省", "普通类控制线", "complete_public_snapshot", len(QINGHAI_CONTROLS), f"已按省考试院通知完整结构化{len(QINGHAI_CONTROLS)}条,包含普通、民族语言、体育和艺术口径。", )) statements.append(ingestion_sql("qinghai_2025_control_lines", len(QINGHAI_CONTROLS), "青海官方控制线完整导入")) statements.append(source_sql( shanxi_controls_bachelor, "official_republication_html", "山西省教育考试服务中心(来源:山西省招生考试管理中心)", "山西省2025年普通高校招生本科录取最低控制分数线公告", "https://oa.sxjyksfw.cn/news/ptgk/20250625/n2025062569813774.html", None, "2025-06-25", "官方教育考试服务网转载省招考中心公告;本科、艺术和体育控制线。", )) statements.append("DELETE FROM control_score_lines WHERE province_name='山西省' AND admission_year=2025;") bachelor_count = len(SHANXI_CONTROLS) - 6 statements.append(""" INSERT INTO control_score_lines ( province_name, admission_year, admission_category, subject_category, education_level, culture_score, professional_score, notes, source_id ) VALUES """ + control_values("山西省", SHANXI_CONTROLS[:bachelor_count]) + ";") statements.append(ingestion_sql("shanxi_2025_control_lines_bachelor", bachelor_count, "山西本科、艺术和体育控制线导入")) statements.append(source_sql( shanxi_controls_vocational, "official_republication_html", "山西省教育考试服务中心(来源:山西省招生考试管理中心)", "山西省2025年普通高校招生专科(高职)批录取最低控制分数线公告", "https://oa.sxjyksfw.cn/news/ptgk/20250814/n2025081480893780.html", None, "2025-08-14", "官方教育考试服务网转载省招考中心公告;普通、艺术和体育专科控制线。", )) statements.append(""" INSERT INTO control_score_lines ( province_name, admission_year, admission_category, subject_category, education_level, culture_score, professional_score, notes, source_id ) VALUES """ + control_values("山西省", SHANXI_CONTROLS[bachelor_count:]) + ";") statements.append("DELETE FROM dataset_coverage WHERE province_name='山西省' AND admission_year=2025 AND dataset_name='普通类控制线';") statements.append(coverage_sql( "山西省", "普通类控制线", "complete_public_snapshot", len(SHANXI_CONTROLS), f"已按本科与专科两份官方公告完整结构化{len(SHANXI_CONTROLS)}条,包含普通、艺术和体育口径。", )) statements.append(ingestion_sql("shanxi_2025_control_lines_vocational", 6, "山西普通、艺术和体育专科控制线导入")) statements.append(""" UPDATE collection_scopes SET collection_status='partial', priority_level='high', status_note='已补齐2025年普通类历史/物理一分一段公开表;计划、投档线及批次规则继续补充。', first_collected_at=COALESCE(first_collected_at,NOW()), last_collected_at=NOW() WHERE province_name='青海省' AND admission_year=2025; UPDATE collection_scopes SET collection_status='partial', priority_level='high', status_note='已完成2025年普通类历史/物理一分一段科类纠正及最高分汇总行补齐;专业录取线继续补充。', first_collected_at=COALESCE(first_collected_at,NOW()), last_collected_at=NOW() WHERE province_name='山西省' AND admission_year=2025; UPDATE collection_scopes SET collection_status='partial', priority_level='high', status_note='已补齐2025年官方控制线;一分一段及其他公开招录数据继续补充。', first_collected_at=COALESCE(first_collected_at,NOW()), last_collected_at=NOW() WHERE province_name IN ('西藏自治区','新疆维吾尔自治区') AND admission_year=2025; """) statements.append("COMMIT;") mysql_command(args, "\n".join(statements)) print( f"导入完成:青海一分一段{len(qh_history) + len(qh_physics)}条;" f"山西一分一段{len(sx_physics) + 229}条;西藏控制线{len(TIBET_CONTROLS)}条;" f"新疆控制线{len(XINJIANG_CONTROLS)}条。" ) if __name__ == "__main__": main()