☰
基于Python的Django+Flask大学生就业数据分析系统设计与实现
2026/10/10 15:23:24 网站建设 项目流程

这套"基于Python的大学生就业数据分析系统"项目,是我在实际业务中遇到过好几次的典型需求形态。很多高校的就业指导中心、二级学院或者做职业教育数据服务的公司,都需要类似的系统来做数据支撑。我结合自己做过的一个模拟项目和业内的常见做法,把标题背后涉及的思路、技术选型、核心实现和踩坑经历完整拆开来讲,文章里的设计决策和代码方向都可以直接拿去参考。

1. 为什么需要一套大学生就业数据分析系统

1.1 传统管理方式的痛点

很多高校目前用的就业管理系统,还停留在"记录台账"的层面:学生填了就业去向,管理员在后台核对、录入,统计报表基本靠导出Excel后手工处理。这种模式存在三个先天问题:

第一,数据更新滞后。招聘信息和学生签约情况变化很快,但手工维护的数据库很难做到实时同步。往往秋招结束一个月后,学校拿到的统计数据还是两周前的快照。第二,分析维度非常浅。传统系统最多能统计出"就业率是多少",但回答不了"哪些行业的薪资涨幅最快""哪些专业的学生更容易留在一线城市""不同学历层次的就业去向差异有多大"这类深层问题。第三,信息孤岛严重。招聘信息、学生档案、往届就业数据、市场薪资水平,分散在不同部门甚至不同系统里,没有一个统一的视角。

我见过最典型的场景是:某高校就业指导中心的老师想分析"最近三年计算机专业毕业生的薪资中位数趋势",结果需要从三个Excel文件里手工合并数据,再写公式统计。类似的痛点在调研中反复出现,所以开发一套以数据分析为核心的就业信息系统,就成了一线教师和管理者非常迫切的需求。

1.2 系统到底要解决哪些问题

这套系统表面上是给就业中心用的管理后台,但实际使用人群可以拆成三类:

  • 就业中心管理员:需要导入和管理学生信息、招聘信息,生成各类统计报表,掌握全校就业动态。
  • 学生:需要查招聘信息、浏览薪资分析、了解所学专业的行业去向,辅助自己做出择业决策。
  • 院系辅导员:需要跟踪所带学生的就业进度,查看横向对比数据,及时发现就业困难学生。

系统价值不在于把数据录入到数据库里,而是让数据"流动"起来:从多个来源采集数据,统一清洗和存储,再通过可视化的方式展示分析结果。比如"专业-薪资-行业三维交叉分析""近三届毕业生就业地域流向图""企业招聘需求词云",这些功能用传统方式做非常费劲,但用Python的数据处理能力配合Web框架,就顺畅得多。

1.3 这个项目适合谁来参考

如果你是做Python Web开发的工程师、高校相关系统的乙方开发者,或者是在校学生要做毕业设计,这个项目都有很强的参考价值。它串起了爬虫、数据清洗、关系型数据库设计、Web后端开发、可视化展示和部署上线这条完整链路。尤其值得注意的是,标题里的"django-flask",是一种双框架混合架构,这种设计方案在真实企业项目里并不少见,背后的取舍思路比技术本身更值得琢磨。

2. 技术选型:Django + Flask 一套系统两种架构

2.1 为什么不是只用一个框架

很多开发者的第一反应是:既然用了Python,直接用Django或者Flask二选一不就行了,为什么要混着用?这个质疑非常合理,单用框架确实更简单,但实际场景里双框架混合有它存在的理由。

Django是一个"大而全"的重量级框架,自带ORM、Admin后台、用户认证、表单处理、中间件机制,非常适合快速搭建业务管理系统。它的杀手锏是Admin站点——几乎不用写代码就能对数据库做增删改查,这对就业中心的管理员来说非常友好。Django的ORM也相当成熟,模型定义好之后,迁移、查询、聚合统计都很顺手。

Flask则是一个"小而美"的微框架,灵活度极高,启动快,写起接口来非常轻便。它特别适合承载那些"不需要重型业务逻辑、只需要快速响应"的服务,比如给前端提供JSON数据接口、跑爬虫调度任务、做简单的数据转换服务。

所以实际选型的出发点不是"哪个框架更好",而是"什么样的业务形态更适合哪种框架"。就业数据分析系统里,既有传统的管理后台需求(适合Django),又有高频的数据统计API需求(适合Flask),混合架构可以让两部分各自发挥长处。

2.2 Django 和 Flask 的分工边界

我见过不少混合方案,最常见的分工方式是这样的:

  • Django:负责核心业务系统,包括用户登录认证、学生信息管理、招聘信息管理、后台管理界面、基础页面渲染。因为Django自带Admin,管理员的大部分日常工作可以靠在Admin里配置搞定。
  • Flask:负责数据服务层,包括爬虫数据的接收与清洗接口、统计分析API、图表数据接口、推荐接口。这些接口的特点是逻辑独立、返回JSON、不需要Django那种复杂的模板渲染。

这种分工的额外好处是模块解耦。爬虫和数据更新任务非常容易出问题——反爬策略变了、页面结构调整了、数据格式异常了,如果这些逻辑混在Django的主进程里,很可能因为一个爬虫异常就把整个管理系统拖垮。拆到Flask独立进程后,数据服务挂了也只是接口暂时不可用,管理后台不受影响。

2.3 两个框架如何共享数据模型

混合架构最大的难点在于两个框架如何访问同一套数据库。我试过两种方案:

第一种方案是Flask完全独立,自己建模型、自己连数据库。好处是两个框架完全隔离,缺点是需要维护两套数据库模型代码,表结构一变更就要改两处,非常痛苦。

第二种方案是让Flask直接复用Django的ORM。具体做法是在Flask启动文件的开头,先设置环境变量并加载Django配置:

# flask_app.py import os import django os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'career_analysis.settings') django.setup() from flask import Flask, jsonify from django.db.models import Avg, Count from career_app.models import EmploymentRecord app = Flask(__name__) @app.route('/api/statistics/salary') def salary_statistics(): data = (EmploymentRecord.objects .values('industry') .annotate(avg_salary=Avg('salary'), total=Count('id')) .order_by('-avg_salary')) return jsonify(list(data))

这个方案里,数据库表结构完全由Django的模型定义和迁移来管理,Flask只负责查询和返回。模型定义只保留一份,避免了双份代码同步的问题。需要注意的是,django.setup()必须在使用任何Django模块之前调用,否则会报AppRegistryNotReady错误。这个细节我在后面问题排查部分会详细说。

3. 数据模型与分析维度设计

3.1 核心数据表怎么设计

数据模型是整个系统的地基,设计得好不好直接影响后续所有分析功能的开发效率。我参考模拟项目的设计,把核心表拆成五个部分:

数据域数据表关键字段
基础档案StudentInfo学号、姓名、性别、专业、学历层次、毕业年份、学院
就业结果EmploymentRecord学生外键、就业状态、单位名称、行业分类、薪资、就业城市
招聘信息RecruitmentInfo企业名称、岗位名称、行业、薪资区间、学历要求、城市、发布时间
统计结果StatisticCache统计类型、维度名、指标名、数值、更新时间
系统管理UserProfile用户外键、角色类型、所属学院

Django模型代码大致长这样:

# models.py from django.db import models class StudentInfo(models.Model): student_no = models.CharField(max_length=32, unique=True) name = models.CharField(max_length=64) gender = models.CharField(max_length=8) major = models.CharField(max_length=128) degree = models.CharField(max_length=16) graduation_year = models.IntegerField() college = models.CharField(max_length=128) class Meta: db_table = 'student_info' class EmploymentRecord(models.Model): student = models.ForeignKey(StudentInfo, on_delete=models.CASCADE) employment_status = models.CharField(max_length=16) company_name = models.CharField(max_length=128) industry = models.CharField(max_length=64) salary = models.DecimalField(max_digits=10, decimal_places=2, null=True) city = models.CharField(max_length=32) record_time = models.DateField() class Meta: db_table = 'employment_record' class RecruitmentInfo(models.Model): company_name = models.CharField(max_length=128) position_name = models.CharField(max_length=128) industry = models.CharField(max_length=64) salary_min = models.IntegerField(null=True) salary_max = models.IntegerField(null=True) degree_request = models.CharField(max_length=16, null=True) city = models.CharField(max_length=32) publish_time = models.DateField(null=True) class Meta: db_table = 'recruitment_info'

之所以把StatisticCache单独拆一张表,是因为统计分析接口如果每次都实时跑聚合SQL,数据量一大就会出现明显的性能瓶颈。实际项目里更常见的做法是:用定时任务把热门指标提前计算好,写进缓存表,接口只查缓存结果。

3.2 就业分析维度的底层逻辑

数据分析维度决定了系统能回答哪些业务问题。我梳理了最核心的几个维度:

  • 就业率分析:按专业、学历、学院、毕业年份统计就业率。公式就是已就业人数除以总人数,但这个"就业"的口径需要提前定清楚——是签了三方协议算,还是灵活就业也算。口径不一致,统计结果就会五花八门。
  • 薪资分析:平均薪资、薪资中位数、薪资区间分布、不同行业/岗位/学历的薪资对比。这里需要注意薪资的字段类型,尽量统一规范化,比如爬虫采集来的工资既有"8k-12k"这种区间格式,也有"年薪20万"这种写法,必须统一转成"月薪多少元"才能做数值计算。
  • 就业去向分析:按单位性质分布(国企、民企、外企、事业单位)、行业分布、岗位类型分布。这部分可以做成饼图和柱状图。
  • 地域流向分析:不同城市/省份的就业人数占比,结合毕业生源地做交叉分析。这部分用可视化地图呈现效果最好。
  • 专业对口度:通过对比学生专业和岗位方向的匹配度来估算,虽然做不到完全精确,但用关键词匹配能算出一个近似值。

这里我想提醒一点:就业分析的指标设计不能照抄通用BI工具的指标,必须和就业业务场景对齐。比如"就业质量"这个概念,系统里可以拆解为薪资水平、专业对口度、单位性质稳定性、就业地域分布等多个可量化的二级指标。指标定义得越清楚,开发阶段越少来回返工。

4. 实操过程:从零搭建到可视化上线

4.1 项目骨架与基础配置

我按实际项目的步骤来演示一遍。先创建虚拟环境并安装依赖:

python -m venv venv source venv/bin/activate pip install django flask djangorestframework celery pandas requests beautifulsoup4 lxml redis

创建Django项目和主应用:

django-admin startproject career_analysis cd career_analysis python manage.py startapp career_app

在Django的settings.py里注册应用并配置数据库,我用的是MySQL,注意字符集要设置成utf8mb4,否则存中文表情符号或者生僻字会报错。

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'career_db', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', }, } } LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_TZ = True

配置完成后执行迁移生成数据表:

python manage.py makemigrations career_app python manage.py migrate python manage.py createsuperuser

4.2 数据采集与清洗流程

数据采集是这个系统最见功夫的部分。我从三个渠道拿数据:高校就业信息网发布的公开招聘信息、主流招聘平台的公开页面、学校内部就业系统中的学生去向记录(需要授权和脱敏处理)。

爬虫部分以招聘平台页面为例,使用requests加BeautifulSoup处理:

import requests from bs4 import BeautifulSoup def fetch_jobs(page_url): headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } resp = requests.get(page_url, headers=headers, timeout=10) resp.encoding = 'utf-8' soup = BeautifulSoup(resp.text, 'html.parser') jobs = [] for item in soup.select('.job-list-item'): title = item.select_one('.job-title').get_text().strip() company = item.select_one('.company-name').get_text().strip() salary_text = item.select_one('.salary').get_text().strip() city = item.select_one('.job-area').get_text().strip() jobs.append({ 'position': title, 'company': company, 'salary_text': salary_text, 'city': city }) return jobs

采集到的数据不能直接入库,必须经过清洗。用pandas处理比较快:

import pandas as pd def clean_salary(df): # 将 "8k-12k" 和 "10k以上" 统一转换为数值范围 def parse_salary(text): if text is None or not isinstance(text, str): return None, None text = text.lower().replace('k', '').replace('千', '').replace('元', '') text = text.replace('月薪', '').replace('年薪', '') if '-' in text: parts = text.split('-') return float(parts[0]), float(parts[1]) if text.endswith('以上'): return float(text[:-2]), None try: return float(text), float(text) except ValueError: return None, None df['salary_min'] = df['salary_text'].apply(lambda x: parse_salary(x)[0]) df['salary_max'] = df['salary_text'].apply(lambda x: parse_salary(x)[1]) df['salary_avg'] = (df['salary_min'].fillna(0) + df['salary_max'].fillna(0)) / 2 return df

清洗规则我总结了几条,都比较实用:

  • 去重:同一岗位同一公司同一天发布,视为重复数据,按主键或MD5指纹去重。
  • 缺失值处理:薪资字段缺失,可以用同岗位同城市的平均值填充,或者直接标记为"面议"。
  • 中文文本编码问题:有些老网站响应头不写charset,用requests的text属性会乱码,需要主动设置resp.encoding = 'gbk'。
  • 城市名归一化:北京/北京市/北京城区,统一映射成"北京";"上海浦东新区"归一成"上海"。这关系到后面地域统计的准确性。

清洗完成后,批量写入数据库时,我踩过一个性能坑:逐条insert非常慢,几千条数据能跑好几分钟。后来换成Django ORM的bulk_create,几十倍提速:

from career_app.models import RecruitmentInfo def batch_save_jobs(job_list): objs = [RecruitmentInfo(**item) for item in job_list] RecruitmentInfo.objects.bulk_create(objs, batch_size=500)

4.3 统计接口与可视化实现

数据清洗入库之后,核心工作转向统计接口。这一层用Flask来做。启动Flask应用的时候先加载Django的配置和ORM:

# flask_service.py import os import django os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'career_analysis.settings') django.setup() from flask import Flask, jsonify, request from django.db.models import Avg, Count, Q from django.db.models.functions import TruncMonth from career_app.models import EmploymentRecord, RecruitmentInfo app = Flask(__name__) @app.route('/api/industry/salary') def industry_salary(): data = (EmploymentRecord.objects .exclude(salary__isnull=True) .values('industry') .annotate(avg_salary=Avg('salary'), total=Count('id')) .order_by('-avg_salary')) result = [] for item in data: result.append({ 'industry': item['industry'], 'avg_salary': round(float(item['avg_salary']), 2), 'total': item['total'] }) return jsonify({'code': 0, 'data': result}) @app.route('/api/salary/trend') def salary_trend(): # 统计近三年各专业毕业生的薪资中位数走势 data = (EmploymentRecord.objects .filter(record_time__year__gte=2022) .annotate(month=TruncMonth('record_time')) .values('month', 'student__major') .annotate(avg_salary=Avg('salary'))) return jsonify({'code': 0, 'data': list(data)}) if __name__ == '__main__': app.run(host='0.0.0.0', port=8001, debug=False)

前端可视化我用的是ECharts,免费开源、文档齐全、图表类型多,国内用得很多。在Django的模板里写一个页面展示行业薪资柱状图:

<!-- templates/salary_dashboard.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>行业薪资分析</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> </head> <body> <div id="chart" style="width: 900px; height: 500px;"></div> <script> fetch('/api/industry/salary') .then(response => response.json()) .then(data => { const industries = data.data.map(item => item.industry); const salaries = data.data.map(item => item.avg_salary); const chart = echarts.init(document.getElementById('chart')); chart.setOption({ tooltip: { trigger: 'axis' }, grid: { left: '10%', right: '10%', bottom: '15%' }, xAxis: { type: 'category', data: industries, axisLabel: { rotate: 30 } }, yAxis: { type: 'value', name: '平均月薪(元)' }, series: [{ type: 'bar', data: salaries, itemStyle: { color: '#2f7afe', borderRadius: [4, 4, 0, 0] } }] }); }); </script> </body> </html>

这里有个跨域问题必须说明:Django页面运行在8000端口,Flask接口跑在8001端口,浏览器直接把请求从8000指向8001会被同源策略拦截。实际项目里有两种常见解法:一是借用Nginx反向代理,把/api/路径统一转发到8001端口,让浏览器看起来是同源的;二是在Flask上配置CORS。生产环境推荐用Nginx方案,开发阶段用flask-cors更快:

from flask_cors import CORS CORS(app, resources={r"/api/*": {"origins": "*"}})

4.4 数据更新与定时任务

数据分析系统最怕数据过期。招聘信息每周都会大量更新,如果完全靠人工触发爬虫,很难保证时效性。我的做法是在Flask服务里集成APScheduler,定时抓取数据并更新统计缓存:

from apscheduler.schedulers.background import BackgroundScheduler import atexit def scheduled_update(): # 每日凌晨2点执行数据采集 with app.app_context(): fetch_and_save_jobs() refresh_statistic_cache() scheduler = BackgroundScheduler() scheduler.add_job(scheduled_update, trigger='cron', hour=2, minute=0) scheduler.start() atexit.register(lambda: scheduler.shutdown())

定时任务设计时需要留个心眼:采集任务运行时间长,且中间可能失败,应该在函数内部做异常捕获、任务状态日志和失败重试。我踩过的坑是爬虫连续失败三天,接口还在返回旧的缓存数据,但看起来一切正常。后来给爬虫任务加了企业微信机器人和邮件告警,失败超过三次就通知管理员。

5. 部署与常见问题排查

5.1 生产环境部署方案

生产环境里Django和Flask不能直接用开发服务器跑,需要分开启动并放到Nginx后面。比较稳妥的部署结构是:

  • Django使用gunicorn启动,监听127.0.0.1:8000
  • Flask使用gunicorn启动,监听127.0.0.1:8001
  • Nginx做统一入口,按路径转发

Nginx的配置片段如下:

server { listen 80; server_name career.example.com; client_max_body_size 20M; location /api/ { proxy_pass http://127.0.0.1:8001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 60s; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

启动命令:

gunicorn career_analysis.wsgi:application -w 4 -b 127.0.0.1:8000 gunicorn flask_service:app -w 2 -b 127.0.0.1:8001

静态文件我建议交给Nginx直接托管,不要让Django在线上处理静态资源。Django的collectstatic命令把静态文件收集到指定目录,Nginx加一个location配置指向该目录,这样页面加载速度会有明显提升。

5.2 高频踩坑与解决实录

在这类"双框架混合+数据分析+爬虫"的项目里,有几类问题几乎每做必遇,我整理一下排查思路和解决方法。

问题一:Flask加载Django模型报AppRegistryNotReady错误

这个错误特别典型,原因就是代码里没有在调用Django模型前执行django.setup()。只要在Flask应用创建前设置DJANGO_SETTINGS_MODULE并调用django.setup(),问题就解决了。另外注意一个细节:django.setup()只能调用一次,如果代码里因为调试重复调用,需要用try-except包装或者保证只在入口执行一次。

问题二:统计数据时薪资出现巨额异常值

某次跑行业平均薪资,发现金融行业平均月薪三十多万。排查后发现是某条数据的salary字段单位是"万元/年",没有正确换算成"元/月",导致整列数据被拉偏。这类问题没有捷径,只能从源头清洗时规范单位,并在SQL查询时加一层合理的最大最小值过滤,比如限定月薪在1500到150000之间,超出则标记为异常数据待人工复核。

问题三:图表中文乱码

前端ECharts出现方框和中文字体错位,一般不是ECharts本身的问题,而是浏览器渲染时CSS字体没有正确设置。解决办法是引入通用中文字体栈,同时确保HTML文档头部有 。如果是后端生成的图片报表出现中文乱码,多半是服务端系统缺少中文字体,需要在服务器上安装fonts-noto-cjk之类的字体库。

问题四:跨域请求被拦

开发环境Django和Flask端口不同,前端fetch接口时被浏览器拦截,控制台报CORS错误。最快的解决方法是给Flask装flask-cors并全局开放。生产环境用Nginx反代解决,路径统一成/api/,浏览器端感知不到跨域。

问题五:接口响应太慢,首屏加载要好几秒

统计分析接口很容易因为COUNT和GROUP BY操作太慢,尤其是没有给外键、行业、城市等字段建索引。排查方法:打开Django的connection.queries日志看实际SQL执行时间,然后用EXPLAIN分析SQL是否走索引。常见的优化手段有联合索引、统计缓存、接口分页。我这里用了定时任务把热门指标写进StatisticCache,响应时间从几百毫秒稳定降到二三十毫秒。

问题六:爬虫被封IP导致数据缺口

爬虫高频请求招聘网站,很容易被对方WAF识别。我在实际项目中遇到过一个情况:连续采集四个小时之后,目标网站把所有请求都返回了验证码页面,爬虫任务看似跑完了,实际上拿到的全是垃圾数据。解决办法是放慢频率、随机延迟、随机User-Agent轮换、多次失敗后自动熔断并告警。更重要的是,爬虫必须遵守网站的robots协议和访问频率要求,数据合规这个底线不能破。

问题七:Django时区导致的日期偏差

Django启用USE_TZ=True之后,DateTimeField字段底层存储的是UTC时间。Flask直接读取这些时间时,如果服务器本地时区没配好,页面显示的时间和实际入库时间会差8小时。处理方式是把所有服务器时区统一设置为Asia/Shanghai,同时在展示层做一个时间格式化工具函数,统一用本地时区输出。

5.3 数据准确性验证经验

数据分析系统的价值全在于数据准确性,但这个点恰恰最容易被忽视。我在上线前做了一轮"数据对账":从原始Excel里随机抽取十个专业,手动统计就业率和薪资均值,再和系统计算结果对比。结果发现有一个专业的就业率在系统里算出来是112%,显然是分母统计错了——有七名学生重复出现在两届的毕业生名单里。修正去重逻辑后,数据才恢复正常。

这类问题提醒我一个重要经验:统计分析功能一定要把"指标口径"做成可配置的配置项,并且在页面明显位置展示口径说明。就业率怎么定义、薪资中位数怎么算、对口度怎么判断,这些都不能拍脑袋,要能和业务方对齐。

6. 关于这个项目的一些后续扩展想法

系统上线之后,我还在持续思考后续的优化方向。标题里的"大学生就业数据分析"其实是个可以做很深的方向,当前版本侧重的是统计和展示,下一步完全可以往预测和推荐方向走。比如利用往年就业数据和当前招聘市场的供需状况,做下一年各专业就业趋势的预测;再比如根据学生的专业、技能标签和求职偏好,做个性化岗位推荐。这些是数据分析系统天然的延伸方向,技术上可以引入更精细的算法,但前提还是底层数据足够干净、维度足够完整。

如果你准备动手做类似系统,我的建议是不要一上来就追求功能大而全。先把最核心的就业率统计、薪资分析、招聘信息展示这三个模块做扎实,跑通"数据采集-清洗-入库-统计-可视化"这条链路,再加扩展功能。这样做的好处是,核心链路跑通后,你对数据质量和接口性能会建立起直观手感,后面新增分析模块时效率会高很多。

我个人在实际操作中的体会是:这个系统真正难的不是Django和Flask的技术细节,而是"理解就业数据"这件事本身。指标口径没有对齐、数据源不干净、分析结果脱离业务场景,这些问题远比框架选择更影响项目成败。先把业务想明白,再让技术落地,整套系统才不会变成一堆代码堆积的"数据废纸篓"。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询