Flask+SQL招聘数据可视化系统:毕业设计实战指南

发布时间:2026/10/8 20:51:06
Flask+SQL招聘数据可视化系统:毕业设计实战指南
简介这是一套面向计算机专业毕业生与Python Web初学者的可视化求职分析系统基于Flask框架与SQL数据库搭建可用于毕业设计选题、课程实践或求职数据研究。系统围绕职位、公司、行业、地区等维度组织数据通过SQL查询与聚合完成岗位需求、薪资水平与就业趋势的可视化呈现帮助使用者理解招聘市场动态并辅助职业规划。压缩包共57个文件约1.26MB包含11个py主程序与爬虫脚本、11个js与2个css前端资源、10个html页面模板、2个sql建表与数据文件以及图片、日志和说明文档结构完整、模块划分清晰。目前已有111人学习下载。项目整合了数据采集、数据库建模、Web路由与图表展示等环节下载后可直接运行便于读者快速理解Flask与SQL的协作方式并在此基础上二次开发或撰写论文。1. 从招聘网站到可视化大屏这套 FlaskSQL 求职分析系统到底能跑出什么打开招聘网站搜索“Python 开发”翻二十页你大概能记住几个薪资数字但很难说清“北京和成都的 Python 岗位薪资差多少”“三年经验是不是真的比应届翻倍”“哪些技能词出现频率最高”。这套基于 Python Flask SQL 的可视化求职分析系统解决的就是这件事把招聘数据抓下来、存进数据库、用 Web 界面把薪资分布、城市对比、技能热度画成图表。它适合两类人——正在做毕业设计、需要一个能直接运行、结构完整、答辩讲得清楚的项目以及想练手 Flask 全栈开发、SQL 查询和 ECharts 可视化的入门开发者。整套系统不依赖复杂的大数据组件一台普通笔记本就能跑起来数据量在几千到几万条之间完全够用。下面从环境搭建、数据入库、接口设计到可视化落地把每个环节拆开讲。2. 环境搭建与项目骨架Flask 最小可运行结构怎么搭2.1 为什么选 Flask 而不是 FastAPI 或 Django做毕业设计选型的第一原则是“能讲清楚、能跑起来、能改得动”。Flask 在这三点上最稳。FastAPI 性能好、异步支持强但毕业设计场景下数据量不大异步带来的复杂度反而容易让答辩时被问住。Django 自带 Admin 和 ORM 全套但目录结构重改一个页面要动好几个文件对新手不友好。Flask 的核心就一个app.py路由、模板、数据库操作都能在一个文件里看到全貌后面再按 Blueprint 拆分。SQL 方面直接用sqlite3或SQLAlchemy都行我一般建议毕业设计用SQLAlchemy因为 ORM 写起来快答辩时也能体现“分层设计”的思路。2.2 创建虚拟环境与安装依赖先确认 Python 版本建议 3.9 以上。打开终端按顺序执行# 创建项目目录 mkdir job_analysis cd job_analysis # 创建虚拟环境Windows 用 python -m venv venv python3 -m venv venv # 激活虚拟环境 # macOS/Linux: source venv/bin/activate # Windows: # venv\Scripts\activate # 安装核心依赖 pip install flask flask-sqlalchemy pandas requests beautifulsoup4这里解释一下每个包的作用flask是 Web 框架本体flask-sqlalchemy把 SQLAlchemy 集成进 Flask省去手动管理 sessionpandas用来做数据清洗和统计聚合requests和beautifulsoup4负责采集端的数据获取和解析。如果采集端用 API 返回 JSONbeautifulsoup4可以省掉但毕业设计里两种都常见先装上不亏。2.3 项目目录结构与配置一个能直接运行的 Flask 项目目录不用太复杂但要有基本分层job_analysis/ ├── app.py # 应用入口 ├── models.py # 数据库模型 ├── routes.py # 路由与接口 ├── static/ │ ├── js/echarts.min.js │ └── css/style.css ├── templates/ │ ├── index.html │ └── dashboard.html └── data/ └── jobs.db # SQLite 数据库文件app.py里做三件事创建 Flask 实例、配置数据库连接、注册路由。代码大致如下from flask import Flask from flask_sqlalchemy import SQLAlchemy app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///data/jobs.db app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False db SQLAlchemy(app) # 必须在 app 上下文中创建表 with app.app_context(): from models import Job db.create_all() if __name__ __main__: app.run(debugTrue, host0.0.0.0, port5000)SQLALCHEMY_DATABASE_URI指向 SQLite 文件好处是零配置、单文件、方便拷贝。如果学校要求用 MySQL把 URI 改成mysqlpymysql://user:passwordlocalhost/job_db再pip install pymysql即可。debugTrue只在开发时开部署时要关掉否则有安全风险。host0.0.0.0让同一局域网的其他设备也能访问答辩演示时用手机或另一台电脑打开很方便。3. 数据采集与 SQL 入库从招聘页面到结构化表3.1 采集端的最小可用写法采集招聘数据常见做法是优先找 JSON 接口其次才是解析 HTML。以某招聘网站的搜索接口为例返回的是 JSON用requests直接拿import requests import time import random def fetch_jobs(keyword, city, page): url https://example.com/api/search params { keyword: keyword, city: city, page: page, pageSize: 30 } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } resp requests.get(url, paramsparams, headersheaders, timeout10) if resp.status_code 200: return resp.json().get(data, {}).get(list, []) return [] # 采集多页加随机延时 all_jobs [] for p in range(1, 6): jobs fetch_jobs(Python, 北京, p) all_jobs.extend(jobs) time.sleep(random.uniform(1, 3))timeout10防止请求卡死time.sleep加随机延时是基本礼貌也降低被封的概率。headers里的User-Agent必须带否则很多接口直接返回 403。采集到的每条记录通常包含职位名称、公司、薪资、城市、经验要求、学历要求、技能标签等字段。3.2 数据清洗与字段映射原始数据不能直接入库薪资字段经常是“15-25K·13薪”这种字符串需要拆成最低薪资、最高薪资、薪资月数三个数值字段import re def parse_salary(salary_str): if not salary_str or salary_str 面议: return None, None, None # 匹配 15-25K 或 15-25K·13薪 match re.search(r(\d)-(\d)K(?:·(\d)薪)?, salary_str) if match: low int(match.group(1)) high int(match.group(2)) months int(match.group(3)) if match.group(3) else 12 return low, high, months return None, None, None这个正则只处理“数字-数字K”格式实际数据里还有“15K-25K”“1.5-2万”等变体需要按实际采集结果补充规则。清洗后的数据用pandas做一次去重按“公司职位城市”组合去重避免同一岗位重复入库。3.3 SQL 建表与批量插入用 SQLAlchemy 定义模型对应 SQL 建表语句from app import db class Job(db.Model): __tablename__ jobs id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(200), nullableFalse) company db.Column(db.String(200)) city db.Column(db.String(50), indexTrue) salary_low db.Column(db.Integer) salary_high db.Column(db.Integer) salary_months db.Column(db.Integer, default12) experience db.Column(db.String(50)) education db.Column(db.String(50)) skills db.Column(db.Text) source db.Column(db.String(50)) created_at db.Column(db.DateTime, defaultdb.func.now())对应的原生 SQL 建表语句是CREATE TABLE jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR(200) NOT NULL, company VARCHAR(200), city VARCHAR(50), salary_low INTEGER, salary_high INTEGER, salary_months INTEGER DEFAULT 12, experience VARCHAR(50), education VARCHAR(50), skills TEXT, source VARCHAR(50), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_city ON jobs(city);city字段加索引因为后面按城市聚合查询频率最高。skills用 TEXT 存 JSON 字符串或逗号分隔字符串查询时用LIKE或JSON函数处理。批量插入用db.session.bulk_save_objects()比逐条add快很多几千条数据几秒就能写完。4. 可视化接口与 ECharts 对接让 SQL 查询变成图表4.1 三个核心统计接口的设计可视化大屏通常需要三类数据薪资分布、城市对比、技能热度。每个接口对应一条 SQL 聚合查询返回 JSON 给前端 ECharts。薪资分布接口按薪资区间分组统计岗位数量from flask import jsonify from app import app, db from models import Job from sqlalchemy import func, case app.route(/api/salary_distribution) def salary_distribution(): # 按薪资中位数划分区间 buckets [ (0, 10, 10K以下), (10, 15, 10-15K), (15, 20, 15-20K), (20, 30, 20-30K), (30, 999, 30K以上) ] result [] for low, high, label in buckets: count Job.query.filter( Job.salary_low low, Job.salary_low high ).count() result.append({name: label, value: count}) return jsonify(result)这里用salary_low做区间划分比用中位数更稳定因为部分记录只有最低薪资。case语句也可以一次性写完但循环写法更直观毕业设计答辩时容易讲清楚。城市对比接口取岗位数量前 10 的城市app.route(/api/city_ranking) def city_ranking(): rows db.session.query( Job.city, func.count(Job.id).label(count), func.avg((Job.salary_low Job.salary_high) / 2).label(avg_salary) ).group_by(Job.city).order_by(func.count(Job.id).desc()).limit(10).all() return jsonify([{ city: r.city, count: r.count, avg_salary: round(float(r.avg_salary or 0), 1) } for r in rows])func.avg里用(salary_low salary_high) / 2估算平均薪资比单独用salary_low更接近真实值。limit(10)控制返回条数前端图表不会太挤。技能热度接口需要把skills字段拆开统计app.route(/api/skill_hot) def skill_hot(): jobs Job.query.filter(Job.skills.isnot(None)).all() counter {} for job in jobs: for skill in job.skills.split(,): skill skill.strip() if skill: counter[skill] counter.get(skill, 0) 1 # 取前 15 个 sorted_skills sorted(counter.items(), keylambda x: x[1], reverseTrue)[:15] return jsonify([{name: k, value: v} for k, v in sorted_skills])这个接口在 Python 层做拆分统计数据量几千条时完全没问题。如果数据到十万级建议在 SQL 层用JSON_TABLE或单独建技能关联表。4.2 ECharts 前端对接与参数配置前端页面引入 ECharts 后用fetch调接口拿数据// 薪资分布饼图 fetch(/api/salary_distribution) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(salaryChart)); chart.setOption({ title: { text: 薪资分布 }, tooltip: { trigger: item }, series: [{ type: pie, radius: [40%, 70%], data: data, emphasis: { itemStyle: { shadowBlur: 10, shadowColor: rgba(0,0,0,0.3) } } }] }); });radius: [40%, 70%]做成环形图比实心饼图更清爽。城市对比用柱状图技能热度用词云或横向柱状图。ECharts 的setOption可以多次调用做增量更新但毕业设计里一次渲染就够。4.3 页面路由与模板渲染Flask 返回 HTML 页面用render_templatefrom flask import render_template app.route(/) def index(): return render_template(index.html) app.route(/dashboard) def dashboard(): return render_template(dashboard.html)index.html放搜索框和概览数字dashboard.html放三个图表容器。模板里用{{ url_for(static, filenamejs/echarts.min.js) }}引用静态资源这样改路径时不用动 HTML。5. 避坑与排查这套系统最容易翻车的五个地方5.1 数据库文件路径写错导致表建不出来现象运行app.py后没有报错但查询时报no such table: jobs。原因SQLALCHEMY_DATABASE_URI里的相对路径是相对于 Flask 实例所在目录不是项目根目录。解决用绝对路径或者确保data/目录已存在。sqlite:///data/jobs.db前面三个斜杠表示相对路径如果data目录不存在SQLite 不会自动创建目录只会创建文件失败。5.2 采集频率过高被临时限制现象前几页正常返回后面全部返回空或 403。原因请求间隔太短触发了对方的频率限制。解决把time.sleep调到 2-5 秒加随机抖动同时检查headers里是否带了User-Agent和Referer。如果还是不行降低采集页数毕业设计用几百条数据足够画出有意义的图表。5.3 薪资字段解析遗漏导致统计偏差现象薪资分布图里“10K以下”柱子特别高。原因大量“面议”或格式不规范的薪资被解析成None在区间统计时被归入最低区间。解决在解析函数里把无法识别的记录单独标记统计时排除或归入“其他”类别。不要为了图表好看强行填默认值答辩时被问到数据来源会很难解释。5.4 ECharts 图表不显示或显示空白现象页面打开了但图表区域一片空白。原因通常是容器没有设置高度或者echarts.init在 DOM 渲染完成前执行。解决给图表容器加styleheight: 400px;并把初始化代码放在DOMContentLoaded事件里或者把script放在/body之前。5.5 部署到服务器后接口 500 错误现象本地跑得好好的放到服务器上访问接口报 500。原因服务器上没装依赖或者 SQLite 文件权限不对。解决用pip freeze requirements.txt导出依赖服务器上pip install -r requirements.txt检查data/目录和jobs.db文件的读写权限。如果用的是 MySQL确认数据库用户有远程连接权限。6. 进阶技巧让这套系统在答辩时多拿十分6.1 用 SQL 窗口函数做薪资排名如果数据库是 MySQL 8.0 或 PostgreSQL可以用窗口函数直接算出每个城市的薪资排名比在 Python 里排序更高效SELECT city, title, salary_low, RANK() OVER (PARTITION BY city ORDER BY salary_low DESC) AS rank_in_city FROM jobs WHERE salary_low IS NOT NULL LIMIT 50;这条查询在每个城市内部按最低薪资降序排名答辩时展示“北京薪资最高的十个 Python 岗位”这类榜单比单纯画柱状图更有说服力。SQLite 3.25 以上也支持窗口函数但老版本不行需要确认环境。6.2 加一个“技能共现”分析技能热度只统计了单个技能出现次数但“Python Flask SQL”这种组合出现的频率更能说明岗位要求。做法是在 Python 层用itertools.combinations生成技能对from itertools import combinations from collections import Counter pair_counter Counter() for job in jobs: skills [s.strip() for s in job.skills.split(,) if s.strip()] for pair in combinations(sorted(skills), 2): pair_counter[pair] 1 top_pairs pair_counter.most_common(10)combinations(sorted(skills), 2)保证“Python,Flask”和“Flask,Python”被算作同一对。结果可以用关系图或热力图展示是答辩时的一个亮点。6.3 用 Flask 蓝图拆分路由当routes.py超过 300 行建议拆成 Blueprintfrom flask import Blueprint api_bp Blueprint(api, __name__, url_prefix/api) api_bp.route(/salary_distribution) def salary_distribution(): ... # app.py 中注册 from routes import api_bp app.register_blueprint(api_bp)这样每个模块独立改一个接口不会影响其他部分。答辩时老师问“项目结构怎么组织的”把 Blueprint 的拆分逻辑讲清楚比说“全写在一个文件里”加分不少。6.4 数据缓存与查询优化如果每次刷新页面都重新查数据库数据量大时会明显变慢。加一个简单的内存缓存from functools import lru_cache lru_cache(maxsize32) def get_city_ranking(): # 原来的查询逻辑 ...lru_cache把函数结果缓存起来相同参数再次调用时直接返回。注意缓存的数据不能实时更新适合毕业设计这种数据变化不频繁的场景。如果要求实时性改用Flask-Caching加 Redis但那就超出毕业设计的复杂度了。6.5 答辩演示前的检查清单演示前按这个顺序过一遍确认虚拟环境已激活、依赖已安装、数据库文件存在且有数据、debugTrue已关闭或保持开启但知道风险、三个接口在浏览器里能直接返回 JSON、图表页面在 1920x1080 分辨率下布局正常。我自己的习惯是提前录一段两分钟的屏幕操作视频万一现场网络或环境出问题直接放视频也能把功能讲完。这套系统从搭建到跑通认真做大概三到五天核心代码不到一千行但覆盖了采集、清洗、入库、接口、可视化完整链路作为毕业设计体量刚好。希望帮到你。本文还有配套的精品资源点击获取