# 上海中考招生计划与成绩导入说明 本文档记录本项目每年从 PDF/图片整理上海中考招生计划、成绩,并导入 MySQL 表 `kylx365_db.MPS_Score` 的需求、步骤和注意事项。 当前已完成 2026 年“计划”中的自主招生、名额到区、名额到校,以及黄浦、徐汇、静安、普陀、虹口、杨浦、宝山、浦东、青浦 1-15 志愿。 ## 年度工作范围 每年需要处理两大类数据。 一、计划 1. 自主招生 2. 名额到区 3. 名额到校 4. 1-15 志愿 二、成绩 1. 自主招生 2. 名额到区 3. 名额到校 4. 1-15 志愿 2026 年当前状态: - 计划/自主招生:已导入 - 计划/名额到区:已导入 - 计划/名额到校:已导入 - 计划/1-15 志愿:黄浦、徐汇、静安、普陀、虹口、杨浦、宝山、浦东、青浦已导入,其他区待文件 - 成绩四类:预计 7 月中旬后导入 ## 数据库与核心表 目标数据库:`kylx365_db` 核心表: - `MPS_School`:学校表,所有学校相关信息以此表为准。 - `MPS_Score`:计划与成绩表,所有导入结果写入此表。 常用参照查询: ```sql SELECT * FROM kylx365_db.MPS_School WHERE SchoolType1 = '高中'; ``` ```sql SELECT * FROM kylx365_db.MPS_Score WHERE ScoreYear = '2025' AND ScoreType = '名额到校' AND DistrictID = 1; ``` 数据库连接信息不要写入 README 或提交到仓库。当前脚本里使用本机已有配置和 PyMySQL 驱动连接,后续最好抽成单独的本地配置文件或环境变量。 ## DistrictID 对照 ```text 1 黄浦区 2 徐汇区 3 长宁区 4 静安区 5 普陀区 6 虹口区 7 杨浦区 8 闵行区 9 宝山区 10 嘉定区 11 浦东新区 12 金山区 13 松江区 14 青浦区 15 奉贤区 16 崇明区 ``` ## MPS_Score 写入规则 计划类导入一般只写计划数,不写成绩。 通用字段规则: - `ScoreYear`:年份,例如 `2026` - `ScoreType`:`自主招生`、`名额到区`、`名额到校`、`1-15志愿` - `DistrictID`:对应区 ID - `SchoolTarget`:高中学校 `MPS_School.ID`,以字符串写入 - `SchoolFullName`:必须使用高中 ID 对应的 `MPS_School.SchoolFullName` - `PlanNum`:计划人数 - `ScoreTotal`、`Score1`、`Score2`、`Score3`、`Score4`:计划导入时填 `0` - `ScoreTotalDifferenceValue`:计划导入时填 `0` - `PlanNumDifferenceValue`:当前计划数减去上一年同维度计划数 - `OrderID`:当前计划导入填 `0` - `SchoolNumber`、`SchoolNumber2`:当前计划导入填空字符串 - `SchoolOfGraduation1`:当前计划导入填 `"0"` 名额到校额外规则: - `SchoolOfGraduation`:初中学校 `MPS_School.ID` - `SchoolFullNameJunior`:必须使用初中 ID 对应的 `MPS_School.SchoolFullName` - 一条数据的唯一业务维度可按 `ScoreYear + ScoreType + DistrictID + SchoolOfGraduation + SchoolTarget` 理解。 - 不能用 PDF 里的简称直接写入 `SchoolFullNameJunior` 或 `SchoolFullName`。 自主招生额外规则: - 普通自主招生拆成: - `1学科` - `2体育` - `3艺术` - 国际课程班/中外合作办学拆成: - `4国际(本市)` - `5国际(非本市)` - `SchoolTargetRemark2` 可参考上一年同学校同类别备注;体育/艺术通常沿用“市级优秀体育学生”“市级艺术骨干学生”等说明。 名额到区额外规则: - `SchoolOfGraduation = 0` - `SchoolFullNameJunior = NULL` - `SchoolTargetRemark = ""` - 维度是“区 + 高中”。 1-15 志愿额外规则: - 每个区的数据由“本区高中”和“外区高中”共同组成。 - `DistrictID` 表示招生计划所属区,不是高中所在区;例如黄浦区表中的外区高中仍写 `DistrictID = 1`。 - 维度是“招生区 + 高中 + SchoolTargetRemark”。 - `SchoolOfGraduation = NULL`、`SchoolFullNameJunior = NULL`。 - 艺术班等特殊计划沿用上一年 `SchoolTargetRemark` 和 `SchoolTargetRemark2`。 - 图片没有学校代码时,必须用学校简称逐一匹配 `MPS_School`,并在写入前校验高中 ID 与全称。 ## 总体操作流程 每一类数据都按以下流程做: 1. 先研究上一年 PDF 与上一年数据库数据,确认字段含义和写入形态。 2. 读取新一年 PDF/图片,优先用表格解析;表格解析失败或 PDF 其实是图片时再 OCR/人工读图。 3. 先匹配学校,不确定的数据不要导入,写入问题清单。 4. 先 dry-run 或打印 ready 汇总,核对每区行数和计划数。 5. 只插入新数据,不删除、不修改已有数据。 6. 导入后查询 `MPS_Score` 总行数、总计划数、分区汇总。 7. 对问题学校更新 `MPS_School` 后,再运行补录脚本,只补缺失行,并刷新问题清单。 重要原则: - 凡是弄不清楚的,先不入库,放入 JSON 问题清单。 - 若某个区解析问题较多,整个区可以先不动,等其他区处理完再单独解决。 - 每次补录必须跳过已存在业务 key,避免重复插入。 - 新增/改名学校优先修正 `MPS_School`,再重新匹配导入。 ## PDF/图片解析经验 优先级: 1. 有 6 位学校编号:优先用编号匹配。 2. 有学校全称:用 `SchoolFullName` 匹配。 3. 有简称或别名:用 `SchoolShortName`、`SchoolOtherName` 匹配。 4. 仍不能唯一匹配:列为问题数据。 学校名称常见问题: - PDF 中会使用简称,而且初中简称比高中多。 - 有学校改名,PDF 可能写成“原名(现新名/校区)”。 - 有新增学校,学校表中原本没有。 - OCR 可能把换行、空格、序号、备注混进学校名。 - 部分 PDF 表格中的学校名可能被拆成多行,需要清理换行再匹配。 本次经验: - 高中通常有 6 位编号,匹配相对稳定。 - 名额到校的初中数量多,名称最容易出问题。 - `SchoolOtherName` 很适合放改名后的现名或曾用名。 - 对“原名(现某某)”这种文本,匹配时应同时尝试原名、括号内现名、去括号名称。 - 图片清晰时可以 OCR/读图解决,但要把结果转成结构化行,再按学校表 ID 入库。 2026 懿德中学问题复盘: - 触发点:复查浦东新区 `上海市浦东新区懿德中学` 时发现名额到校目标高中不对。 - 直接错误:PDF 原始行中最后两列应为 `上海市浦东复旦附中分校` 和 `上海中学东校`,旧脚本分别写成了 `复旦大学附属中学` 和 `上海市上海中学`。 - 根因:高中表头没有 6 位代码时,旧逻辑先做简称别名匹配,`上海市浦东复旦附中分校` 先命中 `复旦附中`,`上海中学东校` 先命中 `上海中学`,导致分校/东校被主校抢走。 - 同类影响:普陀 `华二普陀`、宝山 `华二宝山` / `上师附中宝山`、浦东 `上海中学东校` / `浦东复旦附中分校` / `华二临港奉贤分校`、松江 `松江二中` / `华二松江分校`、奉贤 `华二临港奉贤分校`。 - 修正原则:学校匹配顺序必须是“6 位代码优先,其次精确全称/简称/别名字段,最后才用简称兜底”;简称兜底还要按别名长度从长到短匹配,避免 `华二` 抢在 `华二普陀` 前面。 - 额外问题:青浦区名额到校 PDF 是长表跨页,高中段落在表格抽取中会丢失,不能只依赖 `pdfplumber.extract_tables()` 的表格状态续接。 - 第一次青浦修正仍有隐患:用 `extract_text()` 的自然文本顺序识别高中段落,会把视觉上同行的内容拆错。例如 PDF 表格中 `102056 上海交通大学附属中学 / 181021 上海市青浦区思源中学 / 1` 是同一行,但文本抽取顺序会先输出 `181021 上海市青浦区思源中学 1`,再输出 `102056 上海交通大学附属中学`,导致思源中学被错误归到上一段 `上海市上海中学`。 - 第二次青浦复查发现,仅使用左侧高中代码文字坐标仍不可靠。跨页合并单元格中的高中代码和名称可能显示在整个合并区域的垂直中心,其 PDF 文字坐标并不是该高中数据段的起始行。例如 `182002 复旦大学附属中学青浦分校` 的合并单元格从第 1 页“青浦区实验中学 18”开始延续到第 2 页,但代码文字接近第 1 页底部;`182001 青浦高级中学` 和 `183002 朱家角中学` 也有同样现象。 - 最终修正方式:青浦区用贯穿五列的水平分隔线确定高中数据段的真实起始行,高中代码只用于确定分隔线之后是哪所高中;跨页时先恢复页面顶部的延续高中,再处理页内分隔线。不能使用高中名称或代码在合并单元格中的垂直中心坐标作为切段位置。 - 青浦修正结果示例:`上海市上海中学` 只对应 `上海市青浦区凤溪中学`;`上海交通大学附属中学` 对应 `上海市青浦区思源中学` 和 `上海市青浦区实验中学`。 - 2026 青浦最终核对:PDF 共 `99` 行、计划数 `766`。其中 `复旦大学附属中学青浦分校` 为 `31` 行、`146`;`上海市青浦高级中学` 为 `31` 行、`314`;`上海市朱家角中学` 为 `31` 行、`300`。数据库必须与 PDF 按“初中 ID + 高中 ID + 计划数”逐行集合比对,不能只核对全区总数,因为错误分段不会改变全区计划总数。 - 同类风险扫描:已扫描 16 个区 PDF,仅青浦存在“左侧高中段落 + 右侧三列表格 + 文本顺序错位”的版式;其他区未发现同类结构。 - 修正方式:备份受影响区旧数据到 `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`。 - 后续要求:遇到“分校、校区、东校、宝山、普陀、松江、临港奉贤”等表头,必须人工抽样检查目标高中 ID;遇到长表跨页或左右分栏版式,不能相信纯文本抽取顺序,必须结合表格行坐标或人工抽样;导入后必须做重复业务 key 检查,即 `ScoreYear + ScoreType + DistrictID + SchoolOfGraduation + SchoolTarget` 不应重复。 ### 名额到校强制验收清单 以后处理名额到校数据时,下面项目全部通过后才能写入数据库: 1. 先渲染并目视检查 PDF 每一页,确认是普通矩阵表、长表、跨页表还是图片,不能只看 `extract_text()` 输出。 2. 如果高中名称或代码使用纵向合并单元格,必须寻找真实表格边界,例如贯穿整表的水平分隔线;文字在合并区域中的坐标只能用于识别学校,不能用于确定数据段起点。 3. 跨页表必须分别确认:上一页最后一所高中、下一页顶部延续高中、页面中途切换高中,以及每个切换点前后的第一条初中记录。 4. 解析后先做只读汇总,至少输出每所高中的记录数和 `SUM(PlanNum)`。记录数突然减半、翻倍,或相邻高中记录数呈异常连续关系时必须停下检查。 5. 全区 `COUNT(*)` 和 `SUM(PlanNum)` 只能作为基础检查,不能作为正确性证明。初中计划归错高中时,全区总数通常不会变化。 6. 写库前生成标准化明细集合:`初中学校 ID + 高中学校 ID + PlanNum`。写库后从数据库重新查询同一集合,要求两边完全相等,不能有缺失项或多余项。 7. 检查重复业务键:`ScoreYear + ScoreType + DistrictID + SchoolOfGraduation + SchoolTarget` 必须唯一。 8. 对每所高中人工抽查首行、末行和跨页边界行;对“分校、校区、东校”等名称额外核对 `SchoolTarget` ID。 9. 如果 PDF 有高中小计、区合计或另有官方统计表,必须同时核对;没有官方小计时,保留本次解析生成的逐高中汇总作为审计基线。 10. 任何重新导入都必须先备份该区原始数据库记录,并在一个事务中完成删除、插入和校验;任一检查失败立即回滚。 青浦此次问题说明,导入程序至少应设置三道独立防线: - 版式防线:识别跨页、合并单元格和真实表格分隔线。 - 汇总防线:检查每所高中的记录数与计划数,而不只检查全区总数。 - 明细防线:比较 PDF 解析结果与数据库的逐行业务集合。 ## 当前脚本说明 脚本分为三类:主流程脚本、公共解析/补录脚本、2026 一次性补充脚本。后续年度工作时,主流程和公共脚本可以复制改年份;一次性补充脚本主要用于追溯 2026 的特殊处理,不建议直接运行到新年份。 自主招生: - `import_mps_score_2026.py` - 读取 2026 自主招生计划 PDF 与国际课程班/中外合作办学 PDF。 - 导入 `ScoreType = '自主招生'`。 - 脚本会在已有 2026 自主招生数据时拒绝再次插入。 名额到区: - `import_mps_score_quota_2026.py` - 读取 16 个区的名额到区 PDF。 - 支持 `--dry-run`。 - 如果某区已存在数据,会跳过并报告。 - 对图片或解析失败区,使用 `import_mps_score_quota_manual_2026.py` 做手工/OCR 补充。 名额到区官方总表审核: - `audit_mps_score_quota_2026.py`:读取官方《2026全市高中名额到区招生计划统计表》PDF,并与数据库 `2026 名额到区` 按高中汇总结果比对。 - 审核口径:以 PDF 中高中 6 位招生代码为准,对应 `MPS_School.SchoolNumber`,再比较官方计划数与数据库 `SUM(PlanNum)`。 - 当前审核结果:官方 77 所、计划数 7171;数据库 77 所、计划数 7171;逐校差异 0。 - 经验:只要官方发布全市/全区统计表,就应作为最终验算口径。它不能证明每一条初中分配都正确,但能快速发现高中目标映射错误、漏导、重复导入、计划数错位等系统性问题。 名额到校: - `research_mps_score_school_quota_2026.py` - 负责学校加载、名称清洗、PDF 表格解析、学校匹配。 - 已支持编号匹配、全称/简称/别名匹配、括号内“现名”匹配、同区唯一包含式匹配。 - `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 4`、`--district 5`、`--district 6`、`--district 7`、`--district 9`、`--district 11`、`--district 14` 选择区,可重复传入处理多个区。 - 如果所选区已有 2026 数据会拒绝再次插入。 - 写入后在同一事务内按学校、备注、计划数、同比差值逐行集合校验,失败自动回滚。 - 黄浦区第二张图片、徐汇区第二张图片标题写成“2025 年”,但计划数与新增学校均为 2026 数据,按 2026 导入。 - 某些上一年学校在本年不再出现时,不额外插入 `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 名额到校` 按区/高中汇总结果比对。 - 当前可自动审核区:长宁区、宝山区、金山区、松江区、崇明区。 - 审核结果:上述 5 个区 PDF 合计与数据库逐项一致,差异 0。 - 注意:有些 PDF 的合计行包含水印或序号类数字,例如金山、松江合计行中不参与总计的数字,脚本中已显式忽略;没有明确合计行的区不纳入此脚本自动审核。 - 经验:名额到校审核要优先找 PDF 自带的“合计/总计”行。能自动审核的区,应至少核对区总计和高中列合计;没有合计行的区,也要尽量通过官方后续统计表、人工抽样、重复 key 检查来补充验证。 - 边界:合计审核只能证明“高中列汇总”和“区总量”正确,不能完全证明每个初中分配行都正确;因此它应与学校匹配日志、问题清单、重复 key 检查一起使用。 ## 2026 已完成结果 计划/自主招生: - `ScoreYear = 2026` - `ScoreType = 自主招生` - 已导入 265 行 - 计划数合计 7813 计划/名额到区: - `ScoreYear = 2026` - `ScoreType = 名额到区` - 已导入 947 行 - 计划数合计 7171 计划/名额到校: - `ScoreYear = 2026` - `ScoreType = 名额到校` - 已导入 3894 行 - 计划数合计 12887 - 问题清单 `mps_score_school_quota_2026_problems.json` 已清空 计划/1-15 志愿: - 黄浦、徐汇、静安、普陀、虹口、杨浦、宝山、浦东、青浦已导入 - `ScoreYear = 2026` - `ScoreType = 1-15志愿` - 已导入 591 行 - 计划数合计 40833 - 黄浦区:67 行、1512 人;本区 1365、外区 147 - 徐汇区:68 行、3839 人;本区 3529、外区 310 - 静安区:63 行、3250 人;本区 3150、外区 100 - 普陀区:68 行、3058 人;本区 2758、外区 300 - 虹口区:50 行、1947 人;本区 1868、外区 79 - 杨浦区:55 行、3243 人;本区 3061、外区 182 - 宝山区:59 行、5322 人;本区 5091、外区 231 - 浦东新区:107 行、16629 人;本区 16354、外区 275 - 青浦区:54 行、2033 人;本区 1841、外区 192 - 相比 2025 年全区总计划:黄浦净增 40、徐汇净增 377、静安净增 216、普陀净增 263、虹口净增 42、杨浦净增 392、宝山净增 462、浦东净增 1412、青浦净增 343 - 重复业务 key 为 0 - 其他 7 个区待图片文件 修正记录: - 2026-06-01 修正名额到校部分高中目标误匹配问题。原因是分校/校区/东校表头先命中了主校简称别名;同时修正青浦长表跨页高中段落解析。修正前旧数据已备份到 `mps_score_school_quota_2026_bad_targets_backup.json`。 - 2026-06-07 再次修正青浦跨页合并单元格分段。旧逻辑误用高中代码文字在合并区域中的垂直中心坐标,造成复旦青浦分校、青浦高级中学、朱家角中学连续错位。改为以贯穿五列的水平分隔线确定段落起点,并完成 PDF 与数据库逐行集合核对。 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 | ## 常用核验 SQL 总量: ```sql SELECT COUNT(*) AS c, SUM(PlanNum) AS total FROM MPS_Score WHERE ScoreYear = '2026' AND ScoreType = '名额到校'; ``` 分区: ```sql SELECT DistrictID, COUNT(*) AS c, SUM(PlanNum) AS total FROM MPS_Score WHERE ScoreYear = '2026' AND ScoreType = '名额到校' GROUP BY DistrictID ORDER BY DistrictID; ``` 检查某区上一年参照: ```sql SELECT * FROM MPS_Score WHERE ScoreYear = '2025' AND ScoreType = '名额到校' AND DistrictID = 1 ORDER BY ID; ``` 查初中学校名称: ```sql SELECT ID, DistrictID, SchoolNumber, SchoolFullName, SchoolShortName, SchoolOtherName FROM MPS_School WHERE SchoolType1 = '初中' AND ( SchoolFullName LIKE '%学校名关键词%' OR SchoolShortName LIKE '%学校名关键词%' OR SchoolOtherName LIKE '%学校名关键词%' ); ``` ## 明年复制脚本时要改的地方 把脚本从 2026 复制到新年份后,至少检查这些常量: - `YEAR` - `PREVIOUS_YEAR` - `BASE_DIR` - PDF 文件名 - 问题 JSON 文件名 - 特殊区手工数据脚本中的高中代码、初中代码、计划矩阵 - 自主招生中国际课程班 PDF 名称 导入前必须确认目标年份目标类型没有已有数据,或脚本明确支持跳过/补录。 不要为了重新跑脚本而删除数据库旧数据,除非明确确认要重做且已备份。 ## 待办 计划/1-15 志愿: - 黄浦、徐汇、静安、普陀、虹口、杨浦、宝山、浦东、青浦已完成。 - 等其他区 2026 图片文件后继续处理。 成绩导入: - 预计 7 月中旬后开始。 - 四类成绩都要先研究上一年数据。 - 成绩类导入会涉及 `ScoreTotal`、`Score1`、`Score2`、`Score3`、`Score4` 等字段,不能沿用计划类全部填 0 的规则。 - 成绩导入前要明确每个分数列的含义、缺考/无分/未录取的表示方式,以及是否需要计算差值。