chengjie e3a067fb19 0 1 週間 前
..
README.md 8048dc25d6 增加秒过分数线数据导入程序 1 週間 前
audit_mps_score_quota_2026.py 8048dc25d6 增加秒过分数线数据导入程序 1 週間 前
audit_mps_score_school_quota_totals_2026.py 8048dc25d6 增加秒过分数线数据导入程序 1 週間 前
fix_mps_score_school_quota_2026_bad_targets.py 8048dc25d6 增加秒过分数线数据导入程序 1 週間 前
fix_mps_score_school_quota_problems_2026.py 88cd236ab1 秒过分数线数据导入 2 週間 前
import_mps_score_2026.py 88cd236ab1 秒过分数线数据导入 2 週間 前
import_mps_score_quota_2026.py 88cd236ab1 秒过分数线数据导入 2 週間 前
import_mps_score_quota_manual_2026.py 88cd236ab1 秒过分数线数据导入 2 週間 前
import_mps_score_school_quota_2026.py 88cd236ab1 秒过分数线数据导入 2 週間 前
import_mps_score_school_quota_hongkou_2026.py 88cd236ab1 秒过分数线数据导入 2 週間 前
import_mps_score_school_quota_supplement_2026.py 88cd236ab1 秒过分数线数据导入 2 週間 前
mps_score_school_quota_2026_bad_targets_backup.json 8048dc25d6 增加秒过分数线数据导入程序 1 週間 前
mps_score_school_quota_2026_problems.json 88cd236ab1 秒过分数线数据导入 2 週間 前
mps_score_school_quota_2026_qingpu_reparse_backup.json 8048dc25d6 增加秒过分数线数据导入程序 1 週間 前
mps_score_school_quota_2026_supplement_problems.json 88cd236ab1 秒过分数线数据导入 2 週間 前
research_mps_score_school_quota_2026.py 8048dc25d6 增加秒过分数线数据导入程序 1 週間 前
需求.md e3a067fb19 0 1 週間 前

README.md

上海中考招生计划与成绩导入说明

本文档记录本项目每年从 PDF/图片整理上海中考招生计划、成绩,并导入 MySQL 表 kylx365_db.MPS_Score 的需求、步骤和注意事项。
当前已完成 2026 年“计划”中的 1、2、3:自主招生、名额到区、名额到校。

年度工作范围

每年需要处理两大类数据。

一、计划

  1. 自主招生
  2. 名额到区
  3. 名额到校
  4. 1-15 志愿

二、成绩

  1. 自主招生
  2. 名额到区
  3. 名额到校
  4. 1-15 志愿

2026 年当前状态:

  • 计划/自主招生:已导入
  • 计划/名额到区:已导入
  • 计划/名额到校:已导入
  • 计划/1-15 志愿:待官方文件发布后导入
  • 成绩四类:预计 7 月中旬后导入

数据库与核心表

目标数据库: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 驱动连接,后续最好抽成单独的本地配置文件或环境变量。

DistrictID 对照

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:计划人数
  • ScoreTotalScore1Score2Score3Score4:计划导入时填 0
  • ScoreTotalDifferenceValue:计划导入时填 0
  • PlanNumDifferenceValue:当前计划数减去上一年同维度计划数
  • OrderID:当前计划导入填 0
  • SchoolNumberSchoolNumber2:当前计划导入填空字符串
  • SchoolOfGraduation1:当前计划导入填 "0"

名额到校额外规则:

  • SchoolOfGraduation:初中学校 MPS_School.ID
  • SchoolFullNameJunior:必须使用初中 ID 对应的 MPS_School.SchoolFullName
  • 一条数据的唯一业务维度可按 ScoreYear + ScoreType + DistrictID + SchoolOfGraduation + SchoolTarget 理解。
  • 不能用 PDF 里的简称直接写入 SchoolFullNameJuniorSchoolFullName

自主招生额外规则:

  • 普通自主招生拆成:
    • 1学科
    • 2体育
    • 3艺术
  • 国际课程班/中外合作办学拆成:
    • 4国际(本市)
    • 5国际(非本市)
  • SchoolTargetRemark2 可参考上一年同学校同类别备注;体育/艺术通常沿用“市级优秀体育学生”“市级艺术骨干学生”等说明。

名额到区额外规则:

  • SchoolOfGraduation = 0
  • SchoolFullNameJunior = NULL
  • SchoolTargetRemark = ""
  • 维度是“区 + 高中”。

总体操作流程

每一类数据都按以下流程做:

  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. 有简称或别名:用 SchoolShortNameSchoolOtherName 匹配。
  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 上海交通大学附属中学,导致思源中学被错误归到上一段 上海市上海中学
  • 最终修正方式:青浦区改用 pdfplumber.find_tables() 的行坐标作为主依据,再用左侧高中代码文字的坐标判断“从哪一行开始切换高中”。这样可以处理页 1 同行高中代码、页 2 跨页延续、页 3/页 4 中途切换高中段落等情况。
  • 青浦修正结果示例:上海市上海中学 只对应 上海市青浦区凤溪中学上海交通大学附属中学 对应 上海市青浦区思源中学上海市青浦区实验中学
  • 同类风险扫描:已扫描 16 个区 PDF,仅青浦存在“左侧高中段落 + 右侧三列表格 + 文本顺序错位”的版式;其他区未发现同类结构。
  • 修正方式:备份受影响区旧数据到 mps_score_school_quota_2026_bad_targets_backup.json,再重建普陀、宝山、浦东、松江、青浦、奉贤 6 个区数据;后续又单独备份青浦旧数据到 mps_score_school_quota_2026_qingpu_reparse_backup.json,并重建青浦区数据。
  • 后续要求:遇到“分校、校区、东校、宝山、普陀、松江、临港奉贤”等表头,必须人工抽样检查目标高中 ID;遇到长表跨页或左右分栏版式,不能相信纯文本抽取顺序,必须结合表格行坐标或人工抽样;导入后必须做重复业务 key 检查,即 ScoreYear + ScoreType + DistrictID + SchoolOfGraduation + SchoolTarget 不应重复。

当前脚本说明

脚本分为三类:主流程脚本、公共解析/补录脚本、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,只保留仍未解决的问题。

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 = 名额到校
  • 已导入 3893 行
  • 计划数合计 12887
  • 问题清单 mps_score_school_quota_2026_problems.json 已清空

修正记录:

  • 2026-06-01 修正名额到校部分高中目标误匹配问题。原因是分校/校区/东校表头先命中了主校简称别名;同时修正青浦长表跨页高中段落解析。修正前旧数据已备份到 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 青浦区 98 766
15 奉贤区 131 345
16 崇明区 50 223

常用核验 SQL

总量:

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 复制到新年份后,至少检查这些常量:

  • YEAR
  • PREVIOUS_YEAR
  • BASE_DIR
  • PDF 文件名
  • 问题 JSON 文件名
  • 特殊区手工数据脚本中的高中代码、初中代码、计划矩阵
  • 自主招生中国际课程班 PDF 名称

导入前必须确认目标年份目标类型没有已有数据,或脚本明确支持跳过/补录。
不要为了重新跑脚本而删除数据库旧数据,除非明确确认要重做且已备份。

待办

计划/1-15 志愿:

  • 等 2026 官方文件发布后处理。
  • 需要先研究 2025 的 PDF 与数据库写入形态,再决定 ScoreType、字段、维度和差值计算。

成绩导入:

  • 预计 7 月中旬后开始。
  • 四类成绩都要先研究上一年数据。
  • 成绩类导入会涉及 ScoreTotalScore1Score2Score3Score4 等字段,不能沿用计划类全部填 0 的规则。
  • 成绩导入前要明确每个分数列的含义、缺考/无分/未录取的表示方式,以及是否需要计算差值。