简介一套基于Python与Django的高考志愿填报参考系统完整源码及SQL数据库包面向需要搭建高考志愿检索平台的学生、教师或开发者也适合作为Python Web与数据入库项目的实践案例。系统支持以高校城市、高考排名、高校层次、专业等条件组合查询高校与专业信息并内置将近年高考录取数据从Excel自动写入数据库的Python脚本满足本地数据更新与二次开发需求。压缩包共包含132个文件大小约3.38MB主要类型包括Python源码、sql数据库文件、js/css前端资源、html页面、Dockerfile部署配置以及xlsx数据样例、png/jpg界面截图、readme使用说明等前端静态文件与后端逻辑分离目录结构清晰。目前已有571人学习下载读者可获得完整项目代码、数据库脚本与Docker一键启动方案适合课程设计、毕设参考或高考数据可视化探索。1. 高考志愿填报参考系统这套带 SQL 数据库的 Python 源码到底能干什么每年六月出分到提交志愿之间只有几天很多人抱着 Excel 翻三年录取数据翻到半夜。这套基于 Python 实现的高考志愿填报参考系统把院校、专业、历年分数线和位次数据落到 SQL 数据库里用 Python 做查询与推荐几分钟生成冲稳保三档候选名单。压缩包里除了 .py 源码还带 .sql 数据库文件表结构和样例数据齐全导入 MySQL 就能跑。适合两类人开发者在毕业设计里需要一套“数据库 查询 推荐”完整链路懂一点 Python 的家长想自己给孩子做志愿参考工具。推荐逻辑不是黑匣子梯度参数就在源码里改完生效。这套系统的价值不是“预测录取”而是把三年数据变成可查询、可排序、可验证的本地数据库。下面按拆包顺序讲四个重点库表设计、位次匹配、冲稳保算法、运行排错。2. 数据库设计先行三张核心表的字段、索引和常用 SQL2.1 为什么这类系统要用 MySQL 而不是 Excel 或 SQLite先回答一个很多人会问的问题数据量不大为什么非得上数据库志愿填报数据有三个特点。一是维度固定但组合多省份、科类、年份、院校、专业、录取分数、位次交叉以后是几万到几十万行。二是查询必须多条件过滤比如“2024 年河南省物理类位次 10000 以内能冲的学校”。三是最关键的需要跨表关联院校和专业是两张独立表录取线又是一张表只能用主键关联。Excel 用 VLOOKUP 做两表关联还凑合三张表连环关联基本是灾难而且几万行数据每次打开都卡。SQLite 其实能跑单文件部署也方便但它在并发写上有全局锁。填志愿那几天如果家长和考生同时开着系统查数据SQLite 容易出现 database is locked 的报错。源码里既然带了 .sql 脚本目标就是 MySQL5.7 或 8.0 都行。MySQL 在这个场景下最大的好处是数据文件独立于程序程序崩了数据还在Navicat 这类图形工具可以直接看表、改数据、导 Excel不用写 SQL 也能维护。这个选型在源码结构里体现得很清楚——建表脚本、样例数据、Python 连接层是三层分开的。2.2 建表脚本院校、专业、录取线三张表怎么定字段打开 .sql 文件核心就是三张表school院校、major专业、admission_line录取线。我拆包后第一件事就是按自己的理解重写了一遍建表语句字段与源码基本一致下面是精简版-- 院校表一个学校一条记录 CREATE TABLE school ( school_id INT PRIMARY KEY AUTO_INCREMENT, school_name VARCHAR(64) NOT NULL, province VARCHAR(16) COMMENT 院校所在省份, level VARCHAR(8) COMMENT 985/211/双一流/普通, tags VARCHAR(128) COMMENT 备注如部属、中外合作, UNIQUE KEY uk_school_name (school_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;-- 专业表一个学校对应多个专业简化成一对多 CREATE TABLE major ( major_id INT PRIMARY KEY AUTO_INCREMENT, school_id INT NOT NULL, major_name VARCHAR(64) NOT NULL, category VARCHAR(16) COMMENT 专业大类理工/文史/医学等, KEY idx_major_name (major_name), CONSTRAINT fk_major_school FOREIGN KEY (school_id) REFERENCES school(school_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;-- 录取线表核心数据表省市 科类 年份 位次 CREATE TABLE admission_line ( id INT PRIMARY KEY AUTO_INCREMENT, school_id INT NOT NULL, major_id INT NULL COMMENT 按专业录取时才有值, province VARCHAR(16) NOT NULL COMMENT 招生省份不是院校所在地, subject_type VARCHAR(8) NOT NULL COMMENT 物理类/历史类/文科/理科, year SMALLINT NOT NULL, plan_count INT COMMENT 该专业在该省的招生计划数, min_score INT COMMENT 最低录取分, min_rank INT COMMENT 最低录取位次核心字段, avg_score INT COMMENT 平均录取分用于参考, KEY idx_school_year (school_id, year), KEY idx_rank_query (province, subject_type, year, min_rank), CONSTRAINT fk_line_school FOREIGN KEY (school_id) REFERENCES school(school_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;说一下字段设计的几个关键点。第一admission_line 表里的 idx_rank_query 是“省份 科类 年份 位次”的组合索引顺序和后续 Python 查询的 WHERE 条件完全一致这是位次匹配能秒出的前提。第二min_rank 比 min_score 重要每年分数随试卷难度波动位次是相对稳定的排序后面专门讲这个。第三major_id 允许为空因为不少省份录取线只公布到院校专业组没有具体到专业字段必须可空否则导数据时天天报错。第四所有字符字段全部用 utf8mb4不要用 utf8否则遇到生僻字院校名会报 Incorrect string value 错误。2.3 最常用的三条 SQL多条件过滤、关联查询、统计建好表之后系统里高频出现的 SQL 基本就三种。第一种是按省份、科类、年份过滤的查询这是所有页面和接口的底子SELECT s.school_name, m.major_name, a.min_score, a.min_rank FROM admission_line a JOIN school s ON a.school_id s.school_id LEFT JOIN major m ON a.major_id m.major_id WHERE a.province 河南省 AND a.subject_type 物理类 AND a.year 2024 ORDER BY a.min_rank ASC;这里用 JOIN 而不是在应用层拼数据是因为三张表的数据粒度不同一条录取线记录对应一个学校的一个专业。如果先把三张表各自读进内存再在 Python 里做匹配几万行数据的内存占用和代码复杂度都会失控SQL 里做关联本身就是数据库最擅长的事。第二种是统计类查询比如看某个位次段内有多少可报院校SELECT COUNT(*) AS cnt FROM admission_line WHERE province 河南省 AND subject_type 物理类 AND year 2024 AND min_rank BETWEEN 8000 AND 12000;第三种是排查用查询数据导入后检查某个学校是否重复SELECT school_id, COUNT(*) FROM admission_line GROUP BY school_id, year, province HAVING COUNT(*) 10;这类 SQL 在源码里对应数据库这一层的“增删改查”能力。拆包后我建议你把这三条 SQL 先在 Navicat 里跑一遍确认 .sql 文件导入成功、数据没有明显缺失再去看 Python 代码。如果 SQL 层数据就有问题后面所有推荐结果都是错的这个排查顺序能帮你少走很多弯路。3. 位次匹配原理与 Python 查询分数不可比位次才可比3.1 位次法 vs 线差法为什么推荐系统首选位次志愿填报领域有两套主流口径线差法和位次法。线差法是把院校录取分和省控线的差值算出来考生用自己的分数减批次线得到线差两者比较。位次法是把考生的全省排名和院校往年的最低录取位次比较。为什么这套系统用位次法看一组数据就明白了。假设某省 2023 年物理类一本线 480 分某校最低录取分 520 分线差 40 分2024 年试卷变简单一本线涨到 510 分该校最低录取分 555 分线差还是 45 分看起来差不多。但两年的一分一段表完全不同2023 年 520 分对应的位次是 150002024 年 555 分对应的位次只有 9000。如果只用线差法你会认为这所学校“很稳”实际按位次看它已经涨到一个你够不着的水平了。这就是位次法的价值分数是年际波动的位次是刚性排序的。位次法的假设是“一个考生群体里处于同一相对位置的考生在下一年的表现大致相同”。它也不是完美每年招生计划变化、新校区扩招、专业冷热交替都会让位次失真所以源码里系统用的是近三年位次的中间值或加权值而不是只看一年这个细节比算法本身更值得抄。| 方法 | 原理 | 优点 | 短板 | 适用场景 | | 线差法 | 录取分与批次线的差值 | 计算简单当年即可用 | 受试卷难度和划线影响大 | 批次线稳定的年份、粗略参考 | | 位次法 | 录取最低位次与考生位次比较 | 跨年份可比性强 | 对招生计划变化敏感 | 推荐系统的核心依据 |源码里默认用位次法同时把线差作为辅助字段放在数据里推荐时用加权方式把两个维度揉一起后面讲算法时细说。3.2 Python 连接 MySQLpymysql 查询实现与参数说明系统的数据访问层用的是 pymysql没有上 ORM原因很实际这个项目查询逻辑不复杂手写 SQL 加 DictCursor 足够少一层依赖就少一堆版本兼容问题。核心代码如下import pymysql DB_CONFIG { host: 127.0.0.1, port: 3306, user: root, password: 123456, database: gaokao, charset: utf8mb4, cursorclass: pymysql.cursors.DictCursor, # 返回 dict按字段名取值 } def get_conn(): 获取数据库连接用完必须 close return pymysql.connect(**DB_CONFIG) def query_admission_rank(province, subject_type, year, my_rank, limit50): 按考生位次查询可报院校返回列表 sql SELECT s.school_name, m.major_name, a.min_score, a.min_rank FROM admission_line a JOIN school s ON a.school_id s.school_id LEFT JOIN major m ON a.major_id m.major_id WHERE a.province %s AND a.subject_type %s AND a.year %s AND a.min_rank %s ORDER BY a.min_rank ASC LIMIT %s conn get_conn() try: with conn.cursor() as cur: cur.execute(sql, (province, subject_type, year, my_rank, limit)) return cur.fetchall() finally: conn.close()参数说明。DB_CONFIG 里 charset 必须是 utf8mb4和服务端保持一致否则中文全是问号。cursorclass 用 DictCursor返回的每一行是 dict按字段名取值比如 row[min_rank]比元组好读太多。SQL 里的 %s 是占位符参数以元组形式传给 execute绝对不能直接拼接字符串既防注入也能让 MySQL 走预编译查询更快。LIMIT 默认 50是因为“位次比考生大”这个条件通常能捞出几百条先取前 50 条排序看第一页就行。3.3 查询层优化内存缓存避免重复查库填志愿是高频交互场景用户会反复改分数、改科类、改省份每次都查一次库数据库压力是一回事更烦的是每次都要等 50~200 毫秒。源码里在查询层加了一层简单的字典缓存代码大概是这样_cache {} def cached_query(key, query_func): if key in _cache: return _cache[key] result query_func() _cache[key] result return result def build_key(province, subject_type, year, my_rank): return f{province}|{subject_type}|{year}|{my_rank}key 由省份、科类、年份、位次四元组拼成同一个 key 第二次直接命中缓存。注意位次必须进 key否则用户把位次从 10000 改成 8000 后拿到的还是旧数据这个坑我在拆包调试时踩过打印了半天才发现是缓存没失效。4. 冲稳保推荐算法梯度系数怎么设才能既敢冲又不滑档4.1 冲稳保的数学定义全是位次的比值冲稳保在志愿圈是常识但源码里把它变成了明确的数值规则。定义考生位次 R某校某专业往年的最低录取位次 S判断逻辑是这样冲S 小于 R但差距在 15% 以内。学校往年录取的最低位次比考生靠前考生是贴着线去冲能进就是赚到。稳S 在 R 到 1.2 倍 R 之间。学校往年录取位次比考生靠后一点考生有一定余量大概率能录。保S 在 1.2 倍 R 到 1.6 倍 R 之间。学校往年录取位次明显低于考生位次属于兜底。过滤S 小于 0.85 倍 R冲不进去S 大于 1.6 倍 R过于保守浪费志愿。这几个系数是源码的默认值隐含假设是位次误差 15% 以内的院校可以博一把误差超过 60% 的院校对考生来说太亏。实际使用中我一般会把保底放宽到 1.8尤其考生心理承受力一般、家里坚持“必须有学上”的时候保底宁多勿少。| 梯度 | 位次条件 | 默认系数 | 建议志愿数 | | 冲 | 0.85R ≤ S R | c_low0.85 | 4~6 所 | | 稳 | R ≤ S ≤ 1.2R | w_up1.2 | 6~8 所 | | 保 | 1.2R S ≤ 1.6R | b_up1.6 | 4~6 所 |4.2 推荐引擎代码分类、排序、去重源码里推荐引擎的核心是一个分类函数加一个调度函数。分类函数长这样def classify(school_min_rank, my_rank, c_low0.85, w_up1.2, b_up1.6): 返回 冲/稳/保 或 None过滤 if school_min_rank my_rank * c_low: return None # 位次差太远冲不进去 if school_min_rank my_rank: return chong # 冲一冲 if school_min_rank my_rank * w_up: return wen # 稳一稳 if school_min_rank my_rank * b_up: return bao # 保一保 return None # 过于保守浪费志愿调度函数负责把查询结果按梯度分组同时做两个关键处理按近三年位次均值排序而不是按单年值同一学校只保留位次最好的两个专业避免一个学校占掉整个志愿表。def build_plan(query_result, my_rank): buckets {chong: [], wen: [], bao: []} for row in query_result: tag classify(row[min_rank], my_rank) if tag is None: continue buckets[tag].append(row) # 每个梯度内按三年平均位次排序 for tag in buckets: buckets[tag].sort(keylambda r: r[avg_rank_3y] or 1e9) # 去重同一学校最多保留 2 个专业 seen {} for tag in buckets: kept [] for row in buckets[tag]: if seen.get(row[school_id], 0) 2: continue seen[row[school_id]] seen.get(row[school_id], 0) 1 kept.append(row) buckets[tag] kept return buckets这里 avg_rank_3y 是数据准备阶段算好的近三年平均位次。为什么要用三年均值而不是最新一年因为单年位次容易被大小年效应带偏——某校某年突然爆冷位次大跌第二年大概率回弹只看上年数据会让你误判成“稳”实际是“冲”。用均值会牺牲一点灵敏度但大幅减少误导这个取舍是我认为这套源码里最值得抄的部分。提示avg_rank_3y 需要在导入数据时预计算并落到表里不要在查询时现算否则每次推荐都要扫三张表做聚合慢一个数量级。4.3 输出排序与人工复核建议生成三档后源码把结果输出成表格冲的按位次从高到低排稳的按“录取位次 / 考生位次”的比值从小到大排保的按位次从大到小排。这个排序对应实际填志愿的顺序志愿表里冲的放前面保的放最后。排完以后建议把三档结果抄到一张表里人工过一遍重点看 plan_count招生计划数特别小的专业比如只招 2 个人即使位次匹配也要慎重录取波动极大这种属于数据上正确但实践上要人工干预的地方。5. 运行避坑指南SQL 导入失败、中文乱码和查询慢的排错现场这一章是我实际运行这套源码时踩过的坑按“现象 → 原因 → 解决”写供你对照排错。5.1 导入 .sql 报 1064 语法错误脚本执行到一半中断现象用 Navicat 或命令行执行 .sql 文件跑到一半报 ERROR 1064 (42000): You have an error in your SQL syntax前面的表建好了后面的表没建。原因.sql 文件里混入了和当前 MySQL 版本不兼容的语法最常见的是 IF NOT EXISTS 与旧版本冲突另一个高频原因是文件里有中文注释命令行导入时没指定字符集注释里的中文被解析成乱码语法。解决命令行导入时显式指定字符集mysql -uroot -p --default-character-setutf8mb4 gaokao gaokao.sql。如果还报 1064用 Navicat 打开 .sql 文件定位到报错行把有问题的语句拆出来单跑。稳妥做法是把建库、建表、插入数据拆成三段分别执行哪段挂了重跑哪段不用整个文件重来。5.2 Python 读出来全是问号现象SQL 查出来的是正常数据但 print 到控制台全是 ???原因三层里有一层字符集不对。连接层 DB_CONFIG 里 charset 没设或者设了 utf8 而数据库是 utf8mb4控制台层是 Windows 的 GBK 终端在解码 UTF-8 输出。解决DB_CONFIG 里 charset 固定写 utf8mb4不要用 utf8。终端如果是 Windows运行前先执行 chcp 65001 切到 UTF-8 代码页或者把查询结果写进 CSV 用 Excel 看。三层字符集必须统一库、连接、终端。5.3 查询慢同样的条件第一次要 2 秒后面才快现象带 WHERE province河南省 AND min_rank8000 的查询第一次跑要 1~2 秒第二次开始 100 毫秒以内。原因第一次慢是正常的索引预热和磁盘冷读但如果每次都慢说明组合索引没生效。最常见的原因是查询条件里对索引列做了函数运算比如 year 字段存成了字符串代码里写 YEAR(a.year)2024索引就废了变成全表扫描。解决把字段类型对齐year 用 SMALLINT查询直接写 a.year2024。用 EXPLAIN SELECT ... 看 type 列出现 ALL 就是全表扫描回去检查索引列有没有被函数包裹。这类慢 SQL 优化核心就一句话让 WHERE 条件保持索引列的原始形态。5.4 推荐结果里混着“外省学校”现象考生填的是河南省推荐表里却出现省外院校的名字。原因admission_line 表里 province 字段是“招生省份”不是院校所在地两个概念容易搞混。省外院校也会在河南招生对河南考生来说它们完全合法这不是 bug是数据口径问题。解决确认过滤条件用的是招生省份 a.province 河南省而不是院校所在地 s.province 河南省。如果入库时两个字段已经混了用 UPDATE 把 school 表的省份和 admission_line 的省份分开维护避免以后每次查询都要现场区分。5.5 程序跑起来了但推荐数量明显偏少现象冲稳保三档加一起不到 10 所明显不够填一张志愿表。原因最常见的是 LIMIT 设置太小query_admission_rank 里 LIMIT 50 只捞了前 50 条而保底档位的学校位次靠后没进前 50 就被截断了。解决查询时不要提前 LIMIT或者把 LIMIT 放大到 200把位次大于考生位次 1.6 倍以内的记录全部取出来再在 Python 端做分类。排序和过滤的职责要分开SQL 负责取全集Python 负责分类和截断。6. 让数据活起来批量导入新年度数据与回测验证推荐质量6.1 每年六月数据发布后用 pandas 批量入库这套系统真正耐用靠的是每年更新数据。考试院公布的一分一段表和院校投档线通常是 PDF 或 Excel手动逐条录入不现实。我一般把表格转成 CSV列对齐源表字段后用 pandas 一次性灌进临时表再合并import pandas as pd from sqlalchemy import create_engine df pd.read_csv(admission_2024.csv) engine create_engine(mysqlpymysql://root:123456127.0.0.1:3306/gaokao?charsetutf8mb4) df.to_sql(admission_temp, engine, if_existsreplace, indexFalse)导入后必须做两件事一是把 school_name 关联到 school_id二是核对 min_rank 不能有 0 或空值因为 0 位次会让 classify 函数把整所学校判进“冲”直接污染推荐结果。我一般先跑一条检查 SQLSELECT COUNT(*) FROM admission_temp WHERE min_rank IS NULL确认没问题再往正式表里插。6.2 回测拿上一年的数据验证推荐质量更新完数据最后一步是用上一年数据回测这一步很多人不做但它是判断这套算法在你所在省份是否可用的唯一标准def backtest(predict_year, actual_year): 用 predict_year 的数据推荐用 actual_year 的录取结果验证 recommendations build_plan(query_data(predict_year)) hit 0 total len(recommendations[wen]) for row in recommendations[wen]: actual query_min_rank(row[school_id], actual_year) if actual and actual row[my_rank]: hit 1 print(f稳档命中率: {hit}/{total})命中的定义是“考生位次大于等于学校该年录取最低位次”也就是按去年标准确实能进。如果稳档命中率低于八成先别急着用这套系统检查是不是数据年份弄串了或者梯度系数在你所在省份偏激进。这套源码和 .sql 数据库文件是打包在一起的下载后按第 2 章的顺序导入 MySQL装好 pymysql 依赖就能完整复现。从那以后我每年更新数据都会强制跑一遍回测脚本命中率过线才把推荐结果给孩子看不拿志愿这种事赌运气。希望帮到你。本文还有配套的精品资源点击获取