You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
185 lines
10 KiB
185 lines
10 KiB
"""用人工复核映射修正招生来源学校名,并删除确定误入的数据行。"""
|
|
|
|
import argparse
|
|
import csv
|
|
import hashlib
|
|
import os
|
|
import subprocess
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
|
|
def sql_string(value):
|
|
if value is None or value == "":
|
|
return "NULL"
|
|
return "'" + str(value).replace("\\", "\\\\").replace("'", "\\'") + "'"
|
|
|
|
|
|
def mysql_run(args, sql, capture=False):
|
|
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"])
|
|
result = subprocess.run(command, input=sql, text=True, encoding="utf-8", capture_output=True)
|
|
if result.returncode:
|
|
raise RuntimeError(result.stderr.strip() or result.stdout.strip())
|
|
return result.stdout
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="清理2025招生来源学校名称异常")
|
|
parser.add_argument("--corrections-file", required=True)
|
|
parser.add_argument("--invalid-file", required=True)
|
|
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")
|
|
args = parser.parse_args()
|
|
if not os.environ.get("MYSQL_PWD"):
|
|
raise RuntimeError("必须通过 MYSQL_PWD 环境变量提供数据库密码")
|
|
|
|
corrections_path = Path(args.corrections_file)
|
|
invalid_path = Path(args.invalid_file)
|
|
with corrections_path.open(encoding="utf-8-sig", newline="") as stream:
|
|
corrections = list(csv.DictReader(stream))
|
|
with invalid_path.open(encoding="utf-8-sig", newline="") as stream:
|
|
invalid = list(csv.DictReader(stream))
|
|
if not corrections or set(corrections[0]) != {"raw_name", "target_school_name", "correction_type", "notes"}:
|
|
raise ValueError("名称修正规则字段不符合预期")
|
|
if not invalid or set(invalid[0]) != {"raw_name", "reason"}:
|
|
raise ValueError("无效行规则字段不符合预期")
|
|
if len({row["raw_name"] for row in corrections}) != len(corrections):
|
|
raise ValueError("名称修正规则存在重复来源名称")
|
|
|
|
targets = sorted({row["target_school_name"] for row in corrections})
|
|
target_sql = ",".join(sql_string(name) for name in targets)
|
|
found_text = mysql_run(
|
|
args,
|
|
f"SELECT official_name,COUNT(*) FROM schools WHERE official_name IN ({target_sql}) GROUP BY official_name;",
|
|
capture=True,
|
|
)
|
|
found = {line.split("\t")[0]: int(line.split("\t")[1]) for line in found_text.strip().splitlines()}
|
|
missing = [name for name in targets if found.get(name) != 1]
|
|
if missing:
|
|
raise ValueError(f"目标学校不存在或不唯一:{missing}")
|
|
|
|
digest = hashlib.sha256(corrections_path.read_bytes() + b"\n" + invalid_path.read_bytes()).hexdigest()
|
|
run_id = str(uuid.uuid4())
|
|
correction_values = ",\n".join(
|
|
"(" + ",".join(sql_string(row[key]) for key in [
|
|
"raw_name", "target_school_name", "correction_type", "notes"
|
|
]) + ")" for row in corrections
|
|
)
|
|
invalid_values = ",\n".join(
|
|
"(" + ",".join(sql_string(row[key]) for key in ["raw_name", "reason"]) + ")"
|
|
for row in invalid
|
|
)
|
|
sql = f"""
|
|
SET NAMES utf8mb4;
|
|
SET time_zone='+08:00';
|
|
START TRANSACTION;
|
|
INSERT INTO data_sources (
|
|
source_type,publisher,title,retrieved_at,sha256,raw_file_path,notes
|
|
) VALUES (
|
|
'curated_mapping','志愿通数据治理','2025招生来源学校名称异常复核规则',NOW(),
|
|
{sql_string(digest)},{sql_string(str(corrections_path.resolve()) + ';' + str(invalid_path.resolve()))},
|
|
'逐条保留错别字、截断、旧称、二级机构和无效行的复核依据;无效行删除前已有全库备份。'
|
|
) AS incoming ON DUPLICATE KEY UPDATE 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(digest)});
|
|
INSERT INTO ingestion_runs (
|
|
ingestion_run_id,source_id,dataset_name,dataset_year,started_at,status,rows_read,rows_rejected
|
|
) VALUES ({sql_string(run_id)},@source_id,'school_name_anomaly_cleanup',2025,NOW(),'running',
|
|
{len(corrections) + len(invalid)},0);
|
|
|
|
CREATE TEMPORARY TABLE tmp_school_name_corrections (
|
|
raw_name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '招生来源中的异常学校名称原值',
|
|
target_school_name VARCHAR(255) NOT NULL COMMENT '人工复核后的学校主体正式名称',
|
|
correction_type VARCHAR(32) NOT NULL COMMENT '异常类型:旧称错字截断标点或二级机构',
|
|
notes VARCHAR(512) NOT NULL COMMENT '人工复核依据',
|
|
PRIMARY KEY(raw_name)
|
|
) ENGINE=InnoDB COMMENT='招生来源学校名称异常修正临时表';
|
|
INSERT INTO tmp_school_name_corrections VALUES
|
|
{correction_values};
|
|
|
|
INSERT IGNORE INTO school_names (
|
|
school_id,school_name,name_type,valid_from_year,valid_to_year,source_id
|
|
)
|
|
SELECT s.school_id,c.raw_name,
|
|
CASE WHEN c.correction_type='former' THEN 'former'
|
|
WHEN c.correction_type='secondary_unit' THEN 'campus_alias'
|
|
ELSE 'source_variant' END,
|
|
2025,CASE WHEN c.correction_type='former' THEN 2025 ELSE NULL END,@source_id
|
|
FROM tmp_school_name_corrections c JOIN schools s ON s.official_name=c.target_school_name;
|
|
|
|
SET @groups_before=(SELECT COUNT(*) FROM admission_groups WHERE school_id IS NULL);
|
|
UPDATE admission_groups target JOIN tmp_school_name_corrections c ON c.raw_name=target.school_name_raw
|
|
JOIN schools s ON s.official_name=c.target_school_name
|
|
SET target.school_id=s.school_id WHERE target.school_id IS NULL;
|
|
SET @matched_groups=@groups_before-(SELECT COUNT(*) FROM admission_groups WHERE school_id IS NULL);
|
|
|
|
SET @major_before=(SELECT COUNT(*) FROM admission_major_lines WHERE school_id IS NULL);
|
|
UPDATE admission_major_lines target JOIN tmp_school_name_corrections c ON c.raw_name=target.school_name_raw
|
|
JOIN schools s ON s.official_name=c.target_school_name
|
|
SET target.school_id=s.school_id WHERE target.school_id IS NULL;
|
|
SET @matched_major=@major_before-(SELECT COUNT(*) FROM admission_major_lines WHERE school_id IS NULL);
|
|
|
|
SET @cutoff_before=(SELECT COUNT(*) FROM admission_cutoff_lines WHERE school_id IS NULL);
|
|
UPDATE admission_cutoff_lines target JOIN tmp_school_name_corrections c ON c.raw_name=target.school_name_raw
|
|
JOIN schools s ON s.official_name=c.target_school_name
|
|
SET target.school_id=s.school_id WHERE target.school_id IS NULL;
|
|
SET @matched_cutoff=@cutoff_before-(SELECT COUNT(*) FROM admission_cutoff_lines WHERE school_id IS NULL);
|
|
|
|
CREATE TEMPORARY TABLE tmp_invalid_school_rows (
|
|
raw_name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '误入学校名称字段的原始文本',
|
|
reason VARCHAR(512) NOT NULL COMMENT '判定无效数据行的原因',
|
|
PRIMARY KEY(raw_name)
|
|
) ENGINE=InnoDB COMMENT='确认无效的招生来源行清理规则临时表';
|
|
INSERT INTO tmp_invalid_school_rows VALUES
|
|
{invalid_values};
|
|
|
|
SET @deleted_groups=(SELECT COUNT(*) FROM admission_groups g JOIN tmp_invalid_school_rows i ON i.raw_name=g.school_name_raw);
|
|
DELETE g FROM admission_groups g JOIN tmp_invalid_school_rows i ON i.raw_name=g.school_name_raw;
|
|
SET @deleted_major=(SELECT COUNT(*) FROM admission_major_lines m JOIN tmp_invalid_school_rows i ON i.raw_name=m.school_name_raw);
|
|
DELETE m FROM admission_major_lines m JOIN tmp_invalid_school_rows i ON i.raw_name=m.school_name_raw;
|
|
SET @deleted_cutoff=(SELECT COUNT(*) FROM admission_cutoff_lines c JOIN tmp_invalid_school_rows i ON i.raw_name=c.school_name_raw);
|
|
DELETE c FROM admission_cutoff_lines c JOIN tmp_invalid_school_rows i ON i.raw_name=c.school_name_raw;
|
|
|
|
INSERT INTO data_quality_issues (
|
|
entity_type,entity_key,issue_code,severity,field_name,issue_message,
|
|
issue_fingerprint,resolution_status,detected_at,resolved_at,resolved_by,resolution_note
|
|
)
|
|
SELECT 'admission_group',CAST(g.admission_group_id AS CHAR),'COMPOSITE_ADMISSION_UNIT','info','school_id',
|
|
'该招生单元下的专业分别由多所民航院校承办,不应绑定单一学校ID。',
|
|
SHA2(CONCAT('admission_group|',g.admission_group_id,'|COMPOSITE_ADMISSION_UNIT'),256),
|
|
'false_positive',NOW(),NOW(),'system_import',
|
|
'保留school_id为空;7个专业计划的承办院校记录在专业名称与备注中。'
|
|
FROM admission_groups g
|
|
WHERE g.admission_year=2025 AND g.province_name='上海市'
|
|
AND g.school_name_raw='民航飞行技术类'
|
|
ON DUPLICATE KEY UPDATE resolution_status='false_positive',resolved_at=NOW(),
|
|
resolved_by='system_import',resolution_note=VALUES(resolution_note);
|
|
|
|
UPDATE dataset_coverage SET record_count=(
|
|
SELECT COUNT(*) FROM admission_cutoff_lines l
|
|
WHERE l.admission_year=2025 AND l.province_name='山西省'
|
|
),notes='GaokaoCompass 2025院校投档线CSV原始1097条;259条缺院校名称未入业务表,31条误入表头已清理,保留807条有效记录且全部关联稳定学校ID。'
|
|
WHERE dataset_name='院校专业组投档线' AND admission_year=2025
|
|
AND province_name='山西省';
|
|
|
|
UPDATE ingestion_runs SET finished_at=NOW(),status='success',
|
|
rows_updated=@matched_groups+@matched_major+@matched_cutoff,
|
|
rows_rejected=@deleted_groups+@deleted_major+@deleted_cutoff,
|
|
message=CONCAT('补匹配专业组',@matched_groups,'条、专业线',@matched_major,
|
|
'条、投档线',@matched_cutoff,'条;删除误入表头或类别行',
|
|
@deleted_groups+@deleted_major+@deleted_cutoff,'条。')
|
|
WHERE ingestion_run_id={sql_string(run_id)};
|
|
COMMIT;
|
|
"""
|
|
mysql_run(args, sql)
|
|
print(mysql_run(args, "SELECT message FROM ingestion_runs WHERE ingestion_run_id=" + sql_string(run_id) + ";", True).strip())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|