这套基于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_salaryAvg(salary), totalCount(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_length32, uniqueTrue) name models.CharField(max_length64) gender models.CharField(max_length8) major models.CharField(max_length128) degree models.CharField(max_length16) graduation_year models.IntegerField() college models.CharField(max_length128) class Meta: db_table student_info class EmploymentRecord(models.Model): student models.ForeignKey(StudentInfo, on_deletemodels.CASCADE) employment_status models.CharField(max_length16) company_name models.CharField(max_length128) industry models.CharField(max_length64) salary models.DecimalField(max_digits10, decimal_places2, nullTrue) city models.CharField(max_length32) record_time models.DateField() class Meta: db_table employment_record class RecruitmentInfo(models.Model): company_name models.CharField(max_length128) position_name models.CharField(max_length128) industry models.CharField(max_length64) salary_min models.IntegerField(nullTrue) salary_max models.IntegerField(nullTrue) degree_request models.CharField(max_length16, nullTrue) city models.CharField(max_length32) publish_time models.DateField(nullTrue) 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 createsuperuser4.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, headersheaders, timeout10) 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_size500)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__isnullTrue) .values(industry) .annotate(avg_salaryAvg(salary), totalCount(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__gte2022) .annotate(monthTruncMonth(record_time)) .values(month, student__major) .annotate(avg_salaryAvg(salary))) return jsonify({code: 0, data: list(data)}) if __name__ __main__: app.run(host0.0.0.0, port8001, debugFalse)前端可视化我用的是ECharts免费开源、文档齐全、图表类型多国内用得很多。在Django的模板里写一个页面展示行业薪资柱状图!-- templates/salary_dashboard.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 title行业薪资分析/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body div idchart stylewidth: 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, triggercron, hour2, minute0) scheduler.start() atexit.register(lambda: scheduler.shutdown())定时任务设计时需要留个心眼采集任务运行时间长且中间可能失败应该在函数内部做异常捕获、任务状态日志和失败重试。我踩过的坑是爬虫连续失败三天接口还在返回旧的缓存数据但看起来一切正常。后来给爬虫任务加了企业微信机器人和邮件告警失败超过三次就通知管理员。5. 部署与常见问题排查5.1 生产环境部署方案生产环境里Django和Flask不能直接用开发服务器跑需要分开启动并放到Nginx后面。比较稳妥的部署结构是Django使用gunicorn启动监听127.0.0.1:8000Flask使用gunicorn启动监听127.0.0.1:8001Nginx做统一入口按路径转发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_TZTrue之后DateTimeField字段底层存储的是UTC时间。Flask直接读取这些时间时如果服务器本地时区没配好页面显示的时间和实际入库时间会差8小时。处理方式是把所有服务器时区统一设置为Asia/Shanghai同时在展示层做一个时间格式化工具函数统一用本地时区输出。5.3 数据准确性验证经验数据分析系统的价值全在于数据准确性但这个点恰恰最容易被忽视。我在上线前做了一轮数据对账从原始Excel里随机抽取十个专业手动统计就业率和薪资均值再和系统计算结果对比。结果发现有一个专业的就业率在系统里算出来是112%显然是分母统计错了——有七名学生重复出现在两届的毕业生名单里。修正去重逻辑后数据才恢复正常。这类问题提醒我一个重要经验统计分析功能一定要把指标口径做成可配置的配置项并且在页面明显位置展示口径说明。就业率怎么定义、薪资中位数怎么算、对口度怎么判断这些都不能拍脑袋要能和业务方对齐。6. 关于这个项目的一些后续扩展想法系统上线之后我还在持续思考后续的优化方向。标题里的大学生就业数据分析其实是个可以做很深的方向当前版本侧重的是统计和展示下一步完全可以往预测和推荐方向走。比如利用往年就业数据和当前招聘市场的供需状况做下一年各专业就业趋势的预测再比如根据学生的专业、技能标签和求职偏好做个性化岗位推荐。这些是数据分析系统天然的延伸方向技术上可以引入更精细的算法但前提还是底层数据足够干净、维度足够完整。如果你准备动手做类似系统我的建议是不要一上来就追求功能大而全。先把最核心的就业率统计、薪资分析、招聘信息展示这三个模块做扎实跑通数据采集-清洗-入库-统计-可视化这条链路再加扩展功能。这样做的好处是核心链路跑通后你对数据质量和接口性能会建立起直观手感后面新增分析模块时效率会高很多。我个人在实际操作中的体会是这个系统真正难的不是Django和Flask的技术细节而是理解就业数据这件事本身。指标口径没有对齐、数据源不干净、分析结果脱离业务场景这些问题远比框架选择更影响项目成败。先把业务想明白再让技术落地整套系统才不会变成一堆代码堆积的数据废纸篓。