ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python爬虫+Flask+ECharts:高校录取分数分析与可视化毕设全流程拆解

Python爬虫+Flask+ECharts:高校录取分数分析与可视化毕设全流程拆解 简介一份面向计算机专业毕业设计/课程设计的完整项目资料聚焦高校历年招生分数数据的采集、清洗、存储与可视化。系统基于Python网络爬虫抓取高校录取分数线清洗后写入文件系统借助Flask提供Web查询服务并用ECharts展示最低/最高/平均录取分数线分布、录取人数与批次分布、历史录取趋势等核心模块同时支持专业模糊搜索与专业分析。压缩包共46个文件包含16个JavaScript、8个CSS、4个HTML组成的前端可视化页面4个Python核心脚本实现爬虫与Web逻辑另附演示视频、SQLite数据库、JSON数据、字体图标及依赖配置整体约78.47MB目录结构清晰便于直接运行和研究。配套演示视频完整展示系统效果与操作流程可帮助理解爬虫调度、数据清洗、Flask路由与ECharts图表联动等关键环节也可作为课程设计或毕业设计的完整参考蓝本。上线以来已有133人学习浏览对希望快速搭建同类型数据可视化课题的学生具有较强借鉴价值。1. 从爬虫到可视化这份高校分数分析项目把毕设的数据链路一次打通做高校录取分数相关的毕设或课设时最卡人的往往不是Web页面怎么写而是数据从哪来、抓下来怎么洗、洗完后怎么用图表讲清楚。这套基于Python的高校历年招生分数研究与可视化系统正好把这条链路完整走了一遍先用爬虫从高考网抓取各大高校历年录取分数线做完数据清洗后落地到文件系统再通过Flask对外提供查询服务最后用ECharts把最低分、最高分、平均分、录取人数和批次分布全部做成可视化的图表。如果你正在找一份能直接跑通、演示效果又不拉胯的数据分析类毕业设计这套资源是很合适的参考。它适合两类人——一类是拿来做毕设或课设的学生另一类是想快速搭一个数据可视化Demo的开发者。项目里不仅有完整代码还带演示视频看完视频再对照代码整个系统的运行逻辑会清楚很多。系统的技术栈覆盖了Python爬虫、Flask Web开发、数据分析清洗和ECharts可视化几个方向恰好是毕业设计答辩时老师最容易追问的几个点。不过拿到资源后直接运行经常会有环境不一致的问题这篇文章会按项目实际的文件结构带你拆一遍把每个模块的职责、参数含义和容易踩的坑都讲清楚。2. 项目结构与数据流设计为什么数据落到 JSON 而不是数据库2.1 压缩包里的每个文件承担什么角色先把压缩包里的内容按职责划分清楚。整套系统的核心入口是app.py这是Flask应用的主程序负责启动Web服务、注册路由、加载数据和渲染模板。spider.py是独立的爬虫脚本它的任务是从高考网抓取原始HTML页面并解析出结构化数据写入高校录取分数.json这类数据文件。config.py集中管理爬虫和Web服务的配置项包括目标URL、请求头、存储路径这类参数。util.py则是工具函数模块主要放数据清洗、格式转换这类与业务无关的公共方法。模板目录templates下有三个关键页面index.html是总览首页search_zhuanye.html负责专业模糊搜索school_analysis.html是单所院校的详细分析页。静态资源目录static里放着ECharts所需的JS、CSS、字体文件还带了一个pick-a-color插件用于图表配色。user_info.db是SQLite数据库文件从命名推测是记录用户操作信息的。requirements.txt是依赖清单演示视频则在压缩包里对应的.mov文件里。从职责边界看这个项目刻意把数据处理和Web展示拆开了。spider.py负责产生数据app.py负责消费数据两者通过JSON文件解耦。这意味着你不跑爬虫也能启动Web服务用仓库里已经有的高校录取分数.json就能完成演示这是很务实的思路。2.2 数据链路全貌抓取、清洗、存储、查询、渲染整条数据链路可以分为五个环节。第一环是爬虫抓取spider.py构造HTTP请求获取高考网的高校录取分数页面拿到HTML文档后用解析库提取表格内容。第二环是数据清洗这一步处理缺失值、去重、格式转换比如把字符串类型的分数转成整数把「理科」「文史」这类字段统一成标准枚举。第三环是存储清洗后的数据写入JSON文件以学校、年份、批次、文理科作为组织维度。第四环是查询Flask在启动时把JSON加载进内存通过内存过滤而不是反复读文件来响应搜索请求。第五环是渲染视图函数把数据传给模板模板再交给ECharts绘制图表。这套「文件即数据库」的模式在数据量不大时很讨巧。高校录取分数通常是几千到几万条记录JSON文件不过几MB到十几MB加载到内存后查询速度极快完全不需要引入MySQL。而且用JSON做数据交换格式时Python端通过json.dumps输出前端JavaScript通过JSON.parse接收类型转换上的问题少很多。如果你的数据集超过几百万条方案才需要调整。2.3 config.py 与 util.py 的职责边界改参数不需要动业务代码config.py在项目里承担的是集中配置职责。爬虫目标URL、请求之间的延时、User-Agent、JSON文件读写路径都放在这里。实际在改的时候你要换数据源就改URL要降低被封风险就把请求间隔调大要换存储路径就改文件路径参数。这样写的好处是爬虫代码和Web代码都不需要跟着改环境变了只动配置文件。util.py是另一块值得关注的模块这里放的是数据清洗逻辑。常见处理包括把表格里「——」或空字符串统一转成空值、去掉省市区名称里的空格、把「一本」「二本」这类批次名映射成标准名称。把这类逻辑抽到工具模块里不是为了漂亮而是为了复用——爬虫清洗和Web端二次校验都会调用它。实际调试时你会发现把清洗逻辑分散在各处是后期最痛苦的事集中到util.py能少踩很多坑。3. 抓取与清洗实现思路spider.py 的解析策略与数据校验3.1 为什么选择解析 HTML 表格而不是调用接口很多做爬虫的人第一反应是找接口。但高考网这类站点接口要么做了签名要么返回的数据结构不固定抓包分析的成本很高。相比之下页面上的录取分数表格结构非常规整每一行就是一个院校的录取记录用解析库定位表格节点再逐行提取反而更稳定。spider.py常见的实现思路是这样import requests from bs4 import BeautifulSoup import json import time from config import HEADERS, TARGET_URL, DELAY, OUTPUT_FILE def fetch_html(url): # 构造带超时和UA的请求避免被服务器拒绝 resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 # 如果返回的不是200或页面结构异常直接抛异常方便排查 if resp.status_code ! 200: raise ValueError(f请求失败: HTTP {resp.status_code}) return resp.text def parse_score_table(html): soup BeautifulSoup(html, lxml) # 定位包含分数表格的节点常见是table标签或特定id的div table soup.find(table) rows table.find_all(tr) records [] for row in rows[1:]: # 跳过表头行 cells row.find_all(td) if len(cells) 6: continue records.append({ school: cells[0].get_text(stripTrue), province: cells[1].get_text(stripTrue), year: int(cells[2].get_text(stripTrue)), batch: cells[3].get_text(stripTrue), min_score: int(cells[4].get_text(stripTrue) or 0), max_score: int(cells[5].get_text(stripTrue) or 0) }) return records这段代码的逻辑是先用requests带自定义请求头去GET目标页面把返回的HTML交给BeautifulSoup解析然后定位到表格节点遍历所有数据行每行按预先确认好的列顺序提取学校名、省份、年份、批次、最低分和最高分。cell.get_text(stripTrue)的作用是去掉单元格首尾空白分数转int时如果遇到空字符串直接默认给0这一步就是清洗。参数上有几个点需要注意。DELAY是两次请求之间的间隔时间控制在1到3秒比较稳妥HEADERS里的User-Agent最好换成浏览器常见的值避免被识别为脚本timeout10是请求超时网络波动时能避免程序卡死。如果目标页面用了分页就需要在外层再加循环遍历每一页然后把解析结果合并。3.2 字段拆分与清洗规则学校、批次、文理的组合粒度抓下来的是原始字段真正落到JSON文件时要按查询需求重新组织。以录取分数数据为例完整记录至少要包含学校名称、所属省份、年份、录取批次、文理科类别、最低分、最高分、平均分、录取人数和招生代码。其中「文理科类别」这一项最容易被忽略——同一所学校同一年的数据在页面上可能按理科、文史分开展示解析时如果不区分可视化结果就会互相覆盖。清洗规则上我建议按这样处理学校名称统一去空格和全角字符分数字段必须能转成整数不能转的置为None并记录到日志年份要转成四位整数过滤掉明显超过当前年份的异常值批次字段做枚举映射统一成「本科一批」「本科二批」「专科」这类标准词。这些规则在util.py里以函数形式实现爬虫和Web端共用。数据存储时推荐按学校为主键的嵌套结构顶层是学校名称内层按年份和科类存放记录。这样设计对模板渲染特别友好前端拿到一所学校的数据后可以直接按年份取出整条记录绘制折线图不需要前端再做复杂的数组变换。3.3 数据落地前的完整性检查空值、数值边界与重复去重抓取完成并不意味着数据能用落地前的校验要跟上。我一般会在spider.py的末尾加一个校验函数跑完抓取后自动执行。校验逻辑主要查三件事第一必填字段是否有空值比如学校名和年份不能为空第二分数是否在合理区间录取分数低于100或高于750大概率是解析出错第三是否存在重复记录同一学校、同一年、同一科类出现两条数据要合并或去重。def validate_data(records): cleaned [] seen set() for item in records: key (item[school], item[year], item.get(subject, )) # 重复记录直接跳过 if key in seen: continue seen.add(key) # 数值边界检查超出范围的数据丢弃并记录 if item.get(min_score) and item[min_score] 100: print(f异常最低分: {item}) continue if item.get(max_score) and item[max_score] 750: print(f异常最高分: {item}) continue cleaned.append(item) return cleaned这段代码在内存里去重和过滤异常值返回的数据再写入JSON文件。去重用了元组作为唯一键把学校、年份、科类组合成一个不可变对象塞进集合配合continue跳过重复项。运行时你会在控制台看到被过滤的异常记录这些提示能帮你反查解析逻辑哪里有偏差。数据量大的时候校验过程会比较耗时所以建议把校验结果写进日志而不是直接打印。另外json.dump写入文件时务必设置ensure_asciiFalse否则中文字段会变成\uXXXX的转义序列后续可视化时前端处理会增加额外的解析负担。4. Flask 查询与 ECharts 可视化从路由设计到前端渲染4.1 三个页面的职责分配总览、专业搜索、院校分析Flask端的核心是三个页面对应模板目录里的三个HTML文件。index.html是系统总览展示所有高校录取分数的宏观分布通常包括最低分、最高分、平均分的整体分布情况以及录取人数和批次分布。search_zhuanye.html是专业方向搜索页用户在搜索框输入专业关键词后端做模糊匹配后返回对应的院校列表。school_analysis.html是单校分析页进入某所学校的详情后展示这所学校历年的录取分数线变化趋势。路由设计上常见做法是这样/对应总览/search接收POST请求处理专业搜索/school/school_name动态路由接收学校名称并返回该校的详细分析页面。动态路由是Flask很关键的功能school_name是动态参数视图函数里可以用它拼接查询条件这样做自然、对SEO也友好。4.2 专业模糊搜索的实现内存过滤与前端联动专业搜索这个功能点看起来很普通但在答辩里很加分。实现方式有两种一种是把专业表存进SQLite数据库用LIKE查询另一种是把专业列表加载到内存里用Python过滤。数据量不大时内存过滤响应更快也不需要额外的数据库连接。from flask import Flask, render_template, request, jsonify import json app Flask(__name__) def load_data(): # Web服务启动时一次性加载JSON数据避免每次请求都读磁盘 with open(高校录取分数.json, r, encodingutf-8) as f: return json.load(f) app.route(/search, methods[POST]) def search(): # 从JSON结构里提取专业关键词 keyword request.form.get(keyword, ).strip() if not keyword: return render_template(search_zhuanye.html, results[], keyword) results [] # 遍历每所学校的数据用in操作符做子串匹配 for school in school_data: majors school.get(majors, []) matched [m for m in majors if keyword.lower() in m.lower()] if matched: results.append({ school: school[name], matched_majors: matched, avg_score: school.get(avg_score, 0) }) return render_template(search_zhuanye.html, resultsresults, keywordkeyword)这里有两个参数细节request.form.get取了POST表单里的keyword字段strip()去掉首尾空格避免用户误输入匹配时对专业名和关键词都做了lower()转换这样「计算机」和「计算机科学与技术」这类大小写不一致的情况也能匹配上。如果专业表里包含同义词可以考虑扩展到difflib库的模糊匹配但常规演示场景子串匹配够用了。4.3 图表数据的传递方式JSON 字符串直接注入 ECharts optionFlask和ECharts之间传递数据最直接的方式是后端把数据转成JSON字符串传进模板前端用JSON.parse解析后填充进ECharts的option对象。不推荐在模板里写一堆Jinja2循环去拼JavaScript数组那样调试效率太低。app.route(/school/school_name) def school_analysis(school_name): school find_school(school_name) if not school: return render_template(school_analysis.html, error未找到该院校) # 按年份排序返回近N年的录取分数趋势 trend school.get(trend, []) trend.sort(keylambda x: x[year]) chart_data { years: [t[year] for t in trend], min_scores: [t[min_score] for t in trend], max_scores: [t[max_score] for t in trend], avg_scores: [t[avg_score] for t in trend] } return render_template( school_analysis.html, school_nameschool_name, chart_data_jsonjson.dumps(chart_data, ensure_asciiFalse) )视图函数把年份、最低分、最高分、平均分四组数据封装成一个字典再用json.dumps序列化成字符串传给模板。前端JavaScript里这样接收// templates/school_analysis.html 的 script 标签内 const chartData JSON.parse({{ chart_data_json | safe }}); option { title: { text: 历年录取分数线, left: center }, tooltip: { trigger: axis }, legend: { data: [最低分, 最高分, 平均分], bottom: 0 }, xAxis: { type: category, data: chartData.years }, yAxis: { type: value, name: 分数 }, series: [ { name: 最低分, type: line, data: chartData.min_scores }, { name: 最高分, type: line, data: chartData.max_scores }, { name: 平均分, type: line, data: chartData.avg_scores } ] }; chart.setOption(option);加| safe过滤器是因为Jinja2默认会转义HTMLJSON字符串里的引号会被转成实体导致JSON.parse报错。这个细节是很多人图表空白却找不到原因的地方。前端拿到数据后直接填入折线图的三个series年份数组作为x轴刻度数据结构和ECharts的option完全对应。总览页面用到的是分布直方图和饼图。分布直方图展示各分数段的高校数量饼图展示录取批次的占比这两个图表的传值方式和折线图完全一样只是option的类型参数不同。ECharts的图表类型由type字段控制柱状图是bar饼图是pie折线图是line后端只需要保证传过去的数组长度一致。5. 环境搭建与实战避坑从零启动到跑通全流程5.1 环境准备与依赖安装Python 版本、pip 与依赖清单动手之前先把运行环境准备好。项目基于Python开发压缩包里有Python 3.6和3.7的缓存文件说明开发时用的Python版本在3.6、3.7附近。稳妥起见建议直接安装Python 3.8以上版本兼容性更好。requirements.txt里应包含flask、requests、beautifulsoup4、lxml这几个核心依赖。装依赖用pip或国内镜像站都可以实际操作时我会习惯把pip源换成国内镜像不然下载速度会让人怀疑人生。创建虚拟环境是尽量避免的操作尤其是机器上同时有多个Python项目时。在项目根目录下执行# 创建虚拟环境并激活 python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate # 安装依赖 pip install -r requirements.txt虚拟环境的逻辑是给当前项目单独开一个Python解释器和第三方库目录避免和系统全局环境互相污染。激活后终端提示符前面会出现(venv)标记这时候安装的包都进了venv目录删掉整个目录就能完全卸载干净不留下任何后悔药问题。5.2 启动步骤先生成数据再启动 Web 服务整个系统的启动顺序有讲究。第一步跑爬虫生成最新数据第二步启动Flask服务。如果你的目标是快速看效果也可以跳过爬虫直接用现成的JSON文件。先执行爬虫脚本抓取最新数据cd 项目目录 python spider.pyspider.py跑起来后会在终端输出抓取进度。如果只是做演示建议先不做全量抓取因为高考站的页面数量大、抓取耗时较长跑完可能要几十分钟。你可以临时把config.py里的分页参数改小比如只抓前5页确认流程没问题再放开。数据就绪后启动Flask服务python app.py默认情况下Flask会在127.0.0.1:5000启动开发服务器浏览器访问http://127.0.0.1:5000就能看到总览页面。需要注意的是默认的app.run()不带调试参数时修改代码后需要手动重启服务才能生效。5.3 避坑清单五个真实遇到过的翻车现场坑一图表区域一片空白浏览器控制台报错Cannot read property getContext of null。原因是ECharts初始化的DOM元素没有拿到最常见的是echarts.init写在DOM加载之前。解决方法是把初始化代码放在window.onload或页面底部确保div已经渲染完成。另一个原因是div高度为0ECharts容器必须有明确的高度样式。给图表容器设height: 400px是常见修法。坑二JSON文件里的中文在页面显示为乱码。原因是写入JSON时没有指定ensure_asciiFalse。默认情况下json.dump会把非ASCII字符转成\uXXXX前端解析后重新编码时如果页面charset不是utf-8就会乱码。解决方法是写入和读取都明确指定encodingutf-8写出代码例子里那个json.dumps(chart_data, ensure_asciiFalse)。坑三爬虫请求被服务器拒绝返回403。常见原因是没有请求头或UA被识别为脚本。解决方法是把User-Agent改成浏览器的完整字符串再加上Referer字段模拟从页面跳转进入。另外把DELAY调到2秒以上避免短时间高频请求触发反爬。坑四跳转学校页面时传中文参数Flask路由匹配失败。出现这个问题的原因是URL里的中文需要编码Flask的动态路由参数默认不会自动编码。解决方法是前端跳转时用encodeURIComponent处理学校名称Flask视图函数里拿到的是解码后的原始字符串。坑五模板继承没生效子页面加载不到公共头部和CSS。原因是index.html和layout.html之间的{% extends %}路径写错了。检查子模板的extends语法块写在第一行并且路径相对于模板根目录。另外注意static目录里的CSS文件路径要能通过url_for(static, filenamecss/style.css)正确访问。6. 进阶技巧让数据源动起来把静态项目变成可持续运行的系统6.1 定时触发爬虫的两种常见做法当前项目的模式是手动跑spider.py抓数据隔一段时间手动更新一次。真正部署时这种模式不现实。第一种常见的自动化方案是用系统的crontab或计划任务每天凌晨执行一次爬虫脚本自动更新JSON数据。第二种是改用APScheduler库在Python进程内加定时任务Flask服务跑着同时后台定时抓取数据。APScheduler的基本用法很清晰初始化调度器后注册任务然后指定触发器类型。IntervalTrigger按固定间隔触发CronTrigger按标准cron表达式触发每天两点半更新一次就用后者。6.2 替换成数据库查询层如何保持不动项目用JSON文件做存储数据量上来后读写效率会变差。一个务实的升级路径是把存储层换成SQLite或MySQL但查询接口保持不变。做法是保留Flask路由层的函数签名把load_data函数内部改成查询数据库返回的字段结构和原来JSON里的保持一致。这样前端模板不需要做任何修改后端只是换了一个数据来源。实际改的过程中有个关键点要注意JSON存储时是嵌套结构数据库里要拆成schools表、scores表、majors表查询时要多次join。把原来的内存过滤改成SQL查询时字段名要保持一致。利用「查询层不变」这个约束让数据库替换过程中始终有可用的中间状态。6.3 把历年趋势做成真正的选校参考打通机器学习的入口这个项目现在做的是「已发生数据」的可视化。如果想往机器学习方向拓展最顺手的切入点是分数线预测——用历年录取分数训练一个线性回归或时间序列模型预测下一年的录取分数线区间把预测结果叠加到ECharts折线图上作为虚线延伸。这部分不需要改动现有架构只是在school_analysis.html里多画一条预测线数据由后端模型计算后传到前端。这也是我觉得这个项目最值得扩展的地方。它的核心价值在于把「爬虫、清洗、存储、可视化」这套链路完整跑通了一遍让你真正理解数据是怎么从网页变成图表的。比起把每一个知识点都啃一遍再动手先跑通一套真实项目再逐步替换模块更快也更有效。从那以后我每次拿到这类毕业设计源码都强制自己走一遍「先跑通、再改数据、最后换模块」的流程只有亲手把所有环节跑通一遍才知道坑在哪里。希望帮到你。本文还有配套的精品资源点击获取
返回列表