ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python+SQL高考志愿填报参考系统源码解析:从建库到推荐算法

Python+SQL高考志愿填报参考系统源码解析:从建库到推荐算法 简介这是一套面向高考考生、家长及教育信息化开发者的志愿填报参考系统源码基于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. 高考志愿填报参考系统从一份 PythonSQL 源码里能拆出什么每年六月总有一批家长和考生对着几百页的招生计划汇编发愁。分数出来了位次也查到了可「我这个分能上哪」这个问题靠翻书和拍脑袋根本回答不了。基于 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 是否9850否1是, is_211 TINYINT DEFAULT 0 COMMENT 是否2110否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(hostlocalhost, userroot, passwordyour_pwd, databasegaokao_ref, charsetutf8mb4) 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, year2023): 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_value1, value15000) province st.selectbox(生源省份, [河南, 山东, 河北, 四川]) subject st.selectbox(科类, [物理, 历史, 理科, 文科]) if st.button(开始推荐): conn pymysql.connect(hostlocalhost, userroot, passwordyour_pwd, databasegaokao_ref, charsetutf8mb4) 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 四个典型位次各跑一遍人工检查冲稳保三档的数量是否合理、有没有明显不该出现的学校。这套系统说到底是个辅助工具推荐结果必须经得起人工复核如果连你自己看都觉得离谱那算法参数一定有问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表