简介:这是一套面向高考考生、家长及教育信息化开发者的志愿填报参考系统源码,基于Python与Django构建,可依据高校城市、高考排名、高校层次、专业等条件检索匹配的院校与专业信息,帮助使用者快速定位志愿填报方向。资源包共132个文件,约3.38MB,涵盖14个py业务脚本、15个js与12个css前端资源、5个html模板、3个sql数据库文件,以及xlsx录取数据、图片、字体和Dockerfile等,结构完整,兼顾后端逻辑与页面展示。系统包含两大模块:一是借助Python读取Excel并自动写入数据库,实现近年高考录取数据的批量录入;二是基于Django搭建Web服务,提供志愿检索与查询功能。部署方面提供Docker镜像,可通过docker pull与docker run快速启动并访问8000端口。目前已有571人学习下载,适合想研究Django项目实战、数据入库流程或搭建志愿参考工具的读者参考借鉴。
1. 高考志愿填报参考系统:从一份 Python+SQL 源码里能拆出什么
每年六月,总有一批家长和考生对着几百页的招生计划汇编发愁。分数出来了,位次也查到了,可「我这个分能上哪」这个问题,靠翻书和拍脑袋根本回答不了。基于 Python 实现的高考志愿填报参考系统,本质上就是把这个决策过程数据化:把历年院校投档线、专业录取位次、招生计划这些散落的信息收进一张 SQL 数据库,再用 Python 做查询、比对和推荐。它解决的不是「替你填志愿」,而是把「冲稳保」三档候选院校快速筛出来,让你把精力花在真正需要判断的地方。这套源码适合两类人:一类是想拿它当课程设计或毕业设计的在校生,Python 加 SQL 的组合门槛不高、结构清晰;另一类是真的想给自己或亲戚孩子做一套查询工具的家长型开发者,改改数据就能用。下面我按「数据怎么建、代码怎么跑、坑在哪」的顺序,把这份源码拆开讲透。
2. 先把数据库建起来:SQL 表结构与志愿数据的组织方式
2.1 为什么志愿系统一定要用关系型数据库
志愿填报的核心是「多对多匹配」:一个考生分数对应多所可报院校,一所院校又对应多个专业和历年分数线。这种结构用 Excel 硬扛,一旦要按位次区间、省份、科类、年份四个维度交叉筛选,公式就会失控。关系型数据库的价值在于把「院校」「专业」「录取分数」「招生计划」拆成独立表,用外键关联,查询时用 SQL 的 WHERE 和 JOIN 组合条件。常见做法是建四张核心表:院校表存学校基本信息和所在省份,专业表存专业名称和所属院校,录取分数表按年份存最低分和最低位次,招生计划表存当年各专业计划人数。这样设计的好处是,换一年数据只需要往分数表和计划表里追加记录,不用动表结构。
我一般会把院校 ID 设成自增主键,专业表用「院校 ID + 专业代码」做联合唯一约束,防止同一学校同一专业重复插入。录取分数表则用「院校 ID + 专业 ID + 年份 + 省份 + 科类」做唯一约束,这是整个库最容易出脏数据的地方——同一所学校在不同省份的录取线差异极大,如果不把省份和科类写进约束,查询结果会串省。
2.2 建库建表的 SQL 语句与字段说明
下面这段 SQL 是这套系统最基础的建表脚本,以 MySQL 语法为例,SQL Server 和 SQLite 稍作调整即可。注意字符集用 utf8mb4,否则院校名称里的生僻字会变成问号。
-- 创建数据库,字符集必须支持中文 CREATE DATABASE gaokao_ref DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE gaokao_ref; -- 院校表:存学校基本信息 CREATE TABLE college ( college_id INT AUTO_INCREMENT PRIMARY KEY COMMENT '院校自增ID', college_name VARCHAR(100) NOT NULL COMMENT '院校名称', province VARCHAR(20) NOT NULL COMMENT '院校所在省份', city VARCHAR(30) COMMENT '所在城市', level VARCHAR(20) COMMENT '办学层次:本科/专科', is_985 TINYINT DEFAULT 0 COMMENT '是否985:0否1是', is_211 TINYINT DEFAULT 0 COMMENT '是否211:0否1是' ) COMMENT='院校信息表'; -- 专业表:存专业及所属院校 CREATE TABLE major ( major_id INT AUTO_INCREMENT PRIMARY KEY COMMENT '专业自增ID', college_id INT NOT NULL COMMENT '所属院校ID', major_name VARCHAR(80) NOT NULL COMMENT '专业名称', major_code VARCHAR(20) COMMENT '专业代码', UNIQUE KEY uk_college_major (college_id, major_code), FOREIGN KEY (college_id) REFERENCES college(college_id) ) COMMENT='专业信息表'; -- 录取分数表:按年份、省份、科类存最低分和位次 CREATE TABLE admission_score ( score_id INT AUTO_INCREMENT PRIMARY KEY, college_id INT NOT NULL COMMENT '院校ID', major_id INT COMMENT '专业ID,为空表示院校最低线', year SMALLINT NOT NULL COMMENT '录取年份', province VARCHAR(20) NOT NULL COMMENT '生源省份', subject_type VARCHAR(10) NOT NULL COMMENT '科类:文科/理科/物理/历史', min_score SMALLINT COMMENT '最低录取分', min_rank INT COMMENT '最低录取位次', UNIQUE KEY uk_score (college_id, major_id, year, province, subject_type) ) COMMENT='历年录取分数表';字段设计上有两个点值得展开。第一,min_rank位次字段比分数更重要,因为每年试卷难度不同,分数会浮动,但位次相对稳定,推荐算法应该以位次为主、分数为辅。第二,subject_type用字符串而不是数字枚举,是为了兼容新高考的「物理/历史」和传统高考的「文科/理科」,查询时直接传字符串,不用维护映射表。建完表后,用SHOW CREATE TABLE admission_score检查一遍唯一约束是否生效,这一步别省,我见过太多人因为约束没建好,导入数据时重复插了几万条,后面查询结果翻倍。
2.3 数据从哪来、怎么导进去
数据来源通常是各省教育考试院公布的历年投档线 PDF 或 Excel。手工录入不现实,常见做法是先用 Python 的 pandas 把 Excel 读成 DataFrame,清洗掉合并单元格和空行,再通过 SQLAlchemy 或 pymysql 批量插入。这里给一个批量插入的骨架,重点看executemany的用法和事务提交。
import pymysql import pandas as pd # 读取清洗后的 Excel,列名需与表字段对应 df = pd.read_excel("admission_2023.xlsx") df = df.dropna(subset=["college_name", "min_score"]) # 丢掉关键字段为空的行 conn = pymysql.connect(host="localhost", user="root", password="your_pwd", database="gaokao_ref", charset="utf8mb4") cursor = conn.cursor() # 先插院校,拿到自增ID,再插分数,避免外键找不到 college_rows = df[["college_name", "province"]].drop_duplicates() cursor.executemany( "INSERT IGNORE INTO college (college_name, province) VALUES (%s, %s)", college_rows.values.tolist() ) conn.commit() # 必须提交,否则后续查询看不到刚插的数据 # 构建院校名到ID的映射,用于分数表关联 cursor.execute("SELECT college_id, college_name FROM college") name2id = {name: cid for cid, name in cursor.fetchall()} score_rows = [] for _, row in df.iterrows(): cid = name2id.get(row["college_name"]) if cid: score_rows.append((cid, row["year"], row["province"], row["subject_type"], row["min_score"], row["min_rank"])) cursor.executemany( "INSERT IGNORE INTO admission_score " "(college_id, year, province, subject_type, min_score, min_rank) " "VALUES (%s, %s, %s, %s, %s, %s)", score_rows ) conn.commit() cursor.close() conn.close()这段代码的关键在INSERT IGNORE和executemany。INSERT IGNORE配合唯一约束,重复数据会被自动跳过,不用先查再插,省一次数据库往返。executemany把上千条插入合并成一次网络传输,比循环单条插入快一个数量级。参数上,charset必须和建库时一致,否则中文院校名会乱码;conn.commit()不能漏,pymysql 默认不开自动提交,不提交的话数据只在当前连接可见,换个连接查就是空的,这个坑新手几乎必踩。
3. 用 Python 把查询和推荐跑起来:核心逻辑与参数调优
3.1 位次法推荐的算法思路
志愿推荐的主流方法是位次法:拿考生今年的位次,去历年录取位次表里找匹配区间。具体来说,如果考生位次是 15000,那么「冲」的院校是历年录取位次在 12000 到 15000 之间的,「稳」的是 15000 到 20000,「保」的是 20000 到 28000。这个区间不是拍脑袋定的,而是根据位次波动幅度反推的。位次越靠前,波动越小,区间可以窄一些;位次越靠后,同分人数越多,波动越大,区间要放宽。我一般用比例系数来控制:冲的系数取 0.8 到 1.0,稳的取 1.0 到 1.3,保的取 1.3 到 1.8,再乘以考生位次得到边界。
为什么不用分数直接比对?因为每年一本线不同,2023 年的 580 分和 2024 年的 580 分可能差着几千个位次。位次是排除了试卷难度后的相对位置,跨年可比性更强。但位次法也有边界:新高考改革首年、招生计划大幅增减的年份,历史位次参考价值会下降,这时候要结合招生计划人数做修正,计划扩招的院校位次可能下移,反之则上移。
3.2 查询函数的实现与三个必调参数
下面这个函数接收考生位次、省份、科类,返回冲稳保三档院校列表。代码里用参数化查询防 SQL 注入,这是任何接收用户输入的系统都必须做的。
def recommend(college_cursor, rank, province, subject_type, year=2023): """ rank: 考生位次 province: 生源省份 subject_type: 科类 year: 参考年份 返回: {'chong': [...], 'wen': [...], 'bao': [...]} """ # 三档位次区间系数,可按省份竞争程度微调 bands = { "chong": (0.8, 1.0), "wen": (1.0, 1.3), "bao": (1.3, 1.8), } result = {} for tag, (low, high) in bands.items(): low_rank = int(rank * low) high_rank = int(rank * high) # 位次越小排名越靠前,所以区间是 [low_rank, high_rank] sql = """ SELECT c.college_name, c.province, s.min_score, s.min_rank FROM admission_score s JOIN college c ON s.college_id = c.college_id WHERE s.province = %s AND s.subject_type = %s AND s.year = %s AND s.min_rank BETWEEN %s AND %s ORDER BY s.min_rank ASC LIMIT 30 """ college_cursor.execute(sql, (province, subject_type, year, low_rank, high_rank)) result[tag] = college_cursor.fetchall() return result三个必调参数分别是区间系数、参考年份和 LIMIT 条数。区间系数决定推荐是激进还是保守,竞争激烈的省份可以把冲的系数下限调到 0.85,避免推荐一堆够不着的学校。参考年份建议取最近三年做加权,而不是只看一年,因为单年数据可能有异常波动,代码里可以循环三年把结果合并去重。LIMIT 条数控制返回量,30 条是经验值,太少覆盖不全,太多用户看不过来。另外注意min_rank BETWEEN的边界,位次是整数,区间端点要取整,浮点数直接比较会漏掉边界记录。
3.3 把结果做成可视化界面
命令行输出对家长不友好,常见做法是用 tkinter 或 Streamlit 套一层界面。Streamlit 最省事,几行代码就能把 DataFrame 渲染成可排序的表格。
import streamlit as st import pymysql st.title("高考志愿填报参考") rank = st.number_input("请输入你的位次", min_value=1, value=15000) province = st.selectbox("生源省份", ["河南", "山东", "河北", "四川"]) subject = st.selectbox("科类", ["物理", "历史", "理科", "文科"]) if st.button("开始推荐"): conn = pymysql.connect(host="localhost", user="root", password="your_pwd", database="gaokao_ref", charset="utf8mb4") cursor = conn.cursor() res = recommend(cursor, rank, province, subject) for tag, label in [("chong", "冲"), ("wen", "稳"), ("bao", "保")]: st.subheader(f"{label}一冲({len(res[tag])}所)") st.table(res[tag]) conn.close()Streamlit 的好处是改完代码自动热重载,调界面不用重启。st.table适合静态展示,如果想让用户点列头排序,换成st.dataframe。参数上,number_input的min_value设成 1 防止输入 0 导致区间计算出错。这套界面跑在本地浏览器,不涉及任何外部服务,数据全在自己机器上,对隐私敏感的家长比较友好。
4. 避坑与排查:这份源码最容易翻车的五个地方
4.1 导入数据后查询为空
现象是代码没报错,但推荐结果一条都没有。原因通常是conn.commit()漏了,或者导入时用的连接和查询时用的连接不是同一个库。解决方法是导入后立刻用SELECT COUNT(*) FROM admission_score确认行数,如果为 0 就检查提交语句;如果行数正常但查询为空,检查province和subject_type的值是否和导入时完全一致,比如导入写的是「理科」,查询传的是「理」,字符串不匹配就查不到。
4.2 中文院校名显示成问号
现象是数据库里存进去的院校名变成???或乱码。原因是建库或建表时字符集用了latin1或utf8(三字节),而生僻字需要utf8mb4(四字节)。解决办法是建库时就指定utf8mb4,连接时charset参数也写utf8mb4,两处必须一致。已经建错的库可以用ALTER DATABASE和ALTER TABLE CONVERT TO改,但已损坏的数据改不回来,只能重新导入。
4.3 位次区间算出来是空的
现象是考生位次 15000,冲的区间算出 12000 到 15000,但数据库里这个区间一条记录都没有。原因可能是参考年份选错了,比如用了 2024 年数据但库里只有 2023 年的。也可能是位次字段存的是字符串类型,BETWEEN比较时按字典序而不是数值序,导致"9000"大于"15000"。解决方法是把min_rank字段类型改成INT,查询前用DESCRIBE admission_score确认字段类型。
4.4 推荐结果重复出现同一所学校
现象是冲稳保三档里同一所院校反复出现。原因是分数表里同一院校有多个专业记录,JOIN 之后每个专业一行,没有去重。解决办法是在 SQL 里加GROUP BY c.college_id或者用DISTINCT,如果只想看院校最低线,可以在 WHERE 里加s.major_id IS NULL只取院校级别的记录。
4.5 程序跑起来特别慢
现象是输入位次后要等十几秒才出结果。原因是admission_score表在province、subject_type、min_rank上没有索引,每次查询都全表扫描。解决办法是建联合索引:CREATE INDEX idx_query ON admission_score (province, subject_type, year, min_rank)。索引字段顺序要和 WHERE 条件顺序一致,把区分度高的province放前面。建完索引用EXPLAIN看执行计划,type列从ALL变成range就说明生效了。
5. 让推荐更准:用三年数据加权和一分一段表做修正
单年位次法的局限在于,如果某年某校突然扩招或断档,那一年的位次就是异常值,直接拿来参考会误导。我的习惯是取最近三年数据做加权平均,权重按年份由近到远设为 0.5、0.3、0.2,这样既反映趋势又不会被单年异常带偏。实现上不用改表结构,在查询时把三年结果分别取出,在 Python 里按院校聚合算加权位次即可。
def weighted_rank(cursor, college_id, province, subject, years=(2023, 2022, 2021)): weights = {2023: 0.5, 2022: 0.3, 2021: 0.2} total_w, total_rank = 0, 0 for y in years: cursor.execute( "SELECT min_rank FROM admission_score " "WHERE college_id=%s AND province=%s AND subject_type=%s " "AND year=%s AND major_id IS NULL", (college_id, province, subject, y) ) row = cursor.fetchone() if row and row[0]: total_rank += row[0] * weights[y] total_w += weights[y] return int(total_rank / total_w) if total_w else None这段代码的要点是major_id IS NULL,只取院校级别的最低线,避免专业线混进来拉偏均值。total_w用来处理某年数据缺失的情况,缺一年就只用剩下两年的权重归一化,不会因为除以固定权重导致结果偏小。
另一个提升精度的手段是引入一分一段表。位次法假设考生位次和往年位次直接可比,但各省考生总数每年在变,15000 名在考生 50 万的省份和 80 万的省份含金量不同。一分一段表记录了每个分数对应的累计人数,用它可以把今年的位次换算成等效往年位次:先查今年位次对应的分数,再用这个分数去查往年一分一段表里对应的位次。这个换算能让跨年比较更准,代价是需要额外导入一分一段数据,多一张表的事。
| 修正手段 | 需要的数据 | 提升点 | 代价 |
|---|---|---|---|
| 三年加权 | 历年录取位次 | 平滑单年异常 | 查询次数增加 |
| 一分一段换算 | 历年一分一段表 | 消除考生总数差异 | 多一张表、多一步换算 |
| 招生计划修正 | 当年招生计划 | 反映扩招缩招 | 计划数据获取难 |
最后说个我自己的习惯:每次改完推荐逻辑,不要只看一两个位次的结果,而是拿 5000、15000、30000、50000 四个典型位次各跑一遍,人工检查冲稳保三档的数量是否合理、有没有明显不该出现的学校。这套系统说到底是个辅助工具,推荐结果必须经得起人工复核,如果连你自己看都觉得离谱,那算法参数一定有问题。希望帮到你。
本文还有配套的精品资源,点击获取