本文档记录本项目每年从 PDF/图片整理上海中考招生计划、成绩,并导入 MySQL 表 kylx365_db.MPS_Score 的需求、步骤和注意事项。
当前已完成 2026 年“计划”中的自主招生、名额到区、名额到校,以及黄浦、徐汇、宝山 1-15 志愿。
每年需要处理两大类数据。
一、计划
二、成绩
2026 年当前状态:
目标数据库:kylx365_db
核心表:
MPS_School:学校表,所有学校相关信息以此表为准。MPS_Score:计划与成绩表,所有导入结果写入此表。常用参照查询:
SELECT *
FROM kylx365_db.MPS_School
WHERE SchoolType1 = '高中';
SELECT *
FROM kylx365_db.MPS_Score
WHERE ScoreYear = '2025'
AND ScoreType = '名额到校'
AND DistrictID = 1;
数据库连接信息不要写入 README 或提交到仓库。当前脚本里使用本机已有配置和 PyMySQL 驱动连接,后续最好抽成单独的本地配置文件或环境变量。
1 黄浦区
2 徐汇区
3 长宁区
4 静安区
5 普陀区
6 虹口区
7 杨浦区
8 闵行区
9 宝山区
10 嘉定区
11 浦东新区
12 金山区
13 松江区
14 青浦区
15 奉贤区
16 崇明区
计划类导入一般只写计划数,不写成绩。
通用字段规则:
ScoreYear:年份,例如 2026ScoreType:自主招生、名额到区、名额到校、1-15志愿DistrictID:对应区 IDSchoolTarget:高中学校 MPS_School.ID,以字符串写入SchoolFullName:必须使用高中 ID 对应的 MPS_School.SchoolFullNamePlanNum:计划人数ScoreTotal、Score1、Score2、Score3、Score4:计划导入时填 0ScoreTotalDifferenceValue:计划导入时填 0PlanNumDifferenceValue:当前计划数减去上一年同维度计划数OrderID:当前计划导入填 0SchoolNumber、SchoolNumber2:当前计划导入填空字符串SchoolOfGraduation1:当前计划导入填 "0"名额到校额外规则:
SchoolOfGraduation:初中学校 MPS_School.IDSchoolFullNameJunior:必须使用初中 ID 对应的 MPS_School.SchoolFullNameScoreYear + ScoreType + DistrictID + SchoolOfGraduation + SchoolTarget 理解。SchoolFullNameJunior 或 SchoolFullName。自主招生额外规则:
1学科2体育3艺术4国际(本市)5国际(非本市)SchoolTargetRemark2 可参考上一年同学校同类别备注;体育/艺术通常沿用“市级优秀体育学生”“市级艺术骨干学生”等说明。名额到区额外规则:
SchoolOfGraduation = 0SchoolFullNameJunior = NULLSchoolTargetRemark = ""1-15 志愿额外规则:
DistrictID 表示招生计划所属区,不是高中所在区;例如黄浦区表中的外区高中仍写 DistrictID = 1。SchoolOfGraduation = NULL、SchoolFullNameJunior = NULL。SchoolTargetRemark 和 SchoolTargetRemark2。MPS_School,并在写入前校验高中 ID 与全称。每一类数据都按以下流程做:
MPS_Score 总行数、总计划数、分区汇总。MPS_School 后,再运行补录脚本,只补缺失行,并刷新问题清单。重要原则:
MPS_School,再重新匹配导入。优先级:
SchoolFullName 匹配。SchoolShortName、SchoolOtherName 匹配。学校名称常见问题:
本次经验:
SchoolOtherName 很适合放改名后的现名或曾用名。2026 懿德中学问题复盘:
上海市浦东新区懿德中学 时发现名额到校目标高中不对。上海市浦东复旦附中分校 和 上海中学东校,旧脚本分别写成了 复旦大学附属中学 和 上海市上海中学。上海市浦东复旦附中分校 先命中 复旦附中,上海中学东校 先命中 上海中学,导致分校/东校被主校抢走。华二普陀、宝山 华二宝山 / 上师附中宝山、浦东 上海中学东校 / 浦东复旦附中分校 / 华二临港奉贤分校、松江 松江二中 / 华二松江分校、奉贤 华二临港奉贤分校。华二 抢在 华二普陀 前面。pdfplumber.extract_tables() 的表格状态续接。extract_text() 的自然文本顺序识别高中段落,会把视觉上同行的内容拆错。例如 PDF 表格中 102056 上海交通大学附属中学 / 181021 上海市青浦区思源中学 / 1 是同一行,但文本抽取顺序会先输出 181021 上海市青浦区思源中学 1,再输出 102056 上海交通大学附属中学,导致思源中学被错误归到上一段 上海市上海中学。182002 复旦大学附属中学青浦分校 的合并单元格从第 1 页“青浦区实验中学 18”开始延续到第 2 页,但代码文字接近第 1 页底部;182001 青浦高级中学 和 183002 朱家角中学 也有同样现象。上海市上海中学 只对应 上海市青浦区凤溪中学;上海交通大学附属中学 对应 上海市青浦区思源中学 和 上海市青浦区实验中学。99 行、计划数 766。其中 复旦大学附属中学青浦分校 为 31 行、146;上海市青浦高级中学 为 31 行、314;上海市朱家角中学 为 31 行、300。数据库必须与 PDF 按“初中 ID + 高中 ID + 计划数”逐行集合比对,不能只核对全区总数,因为错误分段不会改变全区计划总数。mps_score_school_quota_2026_bad_targets_backup.json,再重建普陀、宝山、浦东、松江、青浦、奉贤 6 个区数据;后续单独备份青浦旧数据到 mps_score_school_quota_2026_qingpu_reparse_backup.json。第二次青浦跨页修正前的数据备份在 mps_score_school_quota_2026_qingpu_cross_page_fix_backup.json。ScoreYear + ScoreType + DistrictID + SchoolOfGraduation + SchoolTarget 不应重复。以后处理名额到校数据时,下面项目全部通过后才能写入数据库:
extract_text() 输出。SUM(PlanNum)。记录数突然减半、翻倍,或相邻高中记录数呈异常连续关系时必须停下检查。COUNT(*) 和 SUM(PlanNum) 只能作为基础检查,不能作为正确性证明。初中计划归错高中时,全区总数通常不会变化。初中学校 ID + 高中学校 ID + PlanNum。写库后从数据库重新查询同一集合,要求两边完全相等,不能有缺失项或多余项。ScoreYear + ScoreType + DistrictID + SchoolOfGraduation + SchoolTarget 必须唯一。SchoolTarget ID。青浦此次问题说明,导入程序至少应设置三道独立防线:
脚本分为三类:主流程脚本、公共解析/补录脚本、2026 一次性补充脚本。后续年度工作时,主流程和公共脚本可以复制改年份;一次性补充脚本主要用于追溯 2026 的特殊处理,不建议直接运行到新年份。
自主招生:
import_mps_score_2026.pyScoreType = '自主招生'。名额到区:
import_mps_score_quota_2026.py--dry-run。import_mps_score_quota_manual_2026.py 做手工/OCR 补充。名额到区官方总表审核:
audit_mps_score_quota_2026.py:读取官方《2026全市高中名额到区招生计划统计表》PDF,并与数据库 2026 名额到区 按高中汇总结果比对。MPS_School.SchoolNumber,再比较官方计划数与数据库 SUM(PlanNum)。名额到校:
research_mps_score_school_quota_2026.py已支持编号匹配、全称/简称/别名匹配、括号内“现名”匹配、同区唯一包含式匹配。
import_mps_score_school_quota_2026.py
主导入脚本,读取 16 个区名额到校 PDF。
支持 --dry-run。
解析不确定的数据写入 mps_score_school_quota_2026_problems.json。
import_mps_score_school_quota_supplement_2026.py
用于补充处理徐汇、嘉定等表格/OCR特殊区。
import_mps_score_school_quota_hongkou_2026.py
用于处理虹口图片读图后的结构化数据。
fix_mps_score_school_quota_problems_2026.py
当 MPS_School 中新增/修正学校后,重新解析问题区并补插当前能匹配的数据。
会跳过数据库中已存在的 DistrictID + SchoolOfGraduation + SchoolTarget 组合。
会刷新 mps_score_school_quota_2026_problems.json,只保留仍未解决的问题。
1-15 志愿:
import_mps_score_1_15_2026.py--dry-run,写入前校验学校 ID、全称、行数、计划数和学校唯一性。--district 1、--district 2、--district 9 选择区,可重复传入处理多个区。PlanNum = 0 的记录。因此当前行的 SUM(PlanNumDifferenceValue) 可能与两年全区总计划差不同。2026 一次性补充脚本:
import_mps_score_quota_manual_2026.py:用于 2026 名额到区图片/OCR特殊区的手工补录,不是新年份通用入口。import_mps_score_school_quota_hongkou_2026.py:用于 2026 虹口名额到校图片读图后的手工矩阵导入,不是新年份通用入口。import_mps_score_school_quota_supplement_2026.py:包含 2026 徐汇手工矩阵和嘉定特殊 PDF 解析;其中 collect_jiading 目前仍被 fix_mps_score_school_quota_problems_2026.py 引用,所以不要单独删除。生成物:
__pycache__/ 和 *.pyc 是 Python 运行缓存,不属于业务数据或脚本,已在主仓库 .gitignore 中忽略。名额到校区内合计审核:
audit_mps_score_school_quota_totals_2026.py:读取各区名额到校 PDF 中明确存在的“合计”行,与数据库 2026 名额到校 按区/高中汇总结果比对。计划/自主招生:
ScoreYear = 2026ScoreType = 自主招生计划/名额到区:
ScoreYear = 2026ScoreType = 名额到区计划/名额到校:
ScoreYear = 2026ScoreType = 名额到校mps_score_school_quota_2026_problems.json 已清空计划/1-15 志愿:
ScoreYear = 2026ScoreType = 1-15志愿修正记录:
mps_score_school_quota_2026_bad_targets_backup.json。2026 名额到校最终分区汇总:
| DistrictID | 区 | 行数 | 计划数 |
|---|---|---|---|
| 1 | 黄浦区 | 217 | 996 |
| 2 | 徐汇区 | 221 | 899 |
| 3 | 长宁区 | 63 | 418 |
| 4 | 静安区 | 271 | 1102 |
| 5 | 普陀区 | 179 | 736 |
| 6 | 虹口区 | 80 | 488 |
| 7 | 杨浦区 | 144 | 707 |
| 8 | 闵行区 | 460 | 1290 |
| 9 | 宝山区 | 343 | 1076 |
| 10 | 嘉定区 | 130 | 612 |
| 11 | 浦东新区 | 1260 | 2095 |
| 12 | 金山区 | 56 | 355 |
| 13 | 松江区 | 190 | 779 |
| 14 | 青浦区 | 99 | 766 |
| 15 | 奉贤区 | 131 | 345 |
| 16 | 崇明区 | 50 | 223 |
总量:
SELECT COUNT(*) AS c, SUM(PlanNum) AS total
FROM MPS_Score
WHERE ScoreYear = '2026'
AND ScoreType = '名额到校';
分区:
SELECT DistrictID, COUNT(*) AS c, SUM(PlanNum) AS total
FROM MPS_Score
WHERE ScoreYear = '2026'
AND ScoreType = '名额到校'
GROUP BY DistrictID
ORDER BY DistrictID;
检查某区上一年参照:
SELECT *
FROM MPS_Score
WHERE ScoreYear = '2025'
AND ScoreType = '名额到校'
AND DistrictID = 1
ORDER BY ID;
查初中学校名称:
SELECT ID, DistrictID, SchoolNumber, SchoolFullName, SchoolShortName, SchoolOtherName
FROM MPS_School
WHERE SchoolType1 = '初中'
AND (
SchoolFullName LIKE '%学校名关键词%'
OR SchoolShortName LIKE '%学校名关键词%'
OR SchoolOtherName LIKE '%学校名关键词%'
);
把脚本从 2026 复制到新年份后,至少检查这些常量:
YEARPREVIOUS_YEARBASE_DIR导入前必须确认目标年份目标类型没有已有数据,或脚本明确支持跳过/补录。
不要为了重新跑脚本而删除数据库旧数据,除非明确确认要重做且已备份。
计划/1-15 志愿:
成绩导入:
ScoreTotal、Score1、Score2、Score3、Score4 等字段,不能沿用计划类全部填 0 的规则。