
简介这份资源是一套基于 Django 框架的智联招聘数据可视化分析系统成品包含完整源代码、SQLite 数据库文件及使用说明适合需要学习 Python Web 开发、数据采集与可视化项目的初中级开发者。系统实现了从智联招聘自动爬取数据、清洗预处理、入库管理再到职位分布、薪资水平、技能要求、公司规模与行业趋势的多维度图表展示覆盖数据全流程处理。资源共 450 个文件大小约 12.21MB主体为 112 个 png 图片、98 个 pyc 编译文件、90 个 js 脚本、40 个 css 样式、32 个 py 后端文件及 15 个 html 页面另有 sqlite3/csv 数据库文件、字体图标和 eot/ttf/woff 等资源目录结构清晰便于按模块对照学习。当前已有 194 人学习浏览。借助源码中的爬虫逻辑、Django 视图配置、ECharts 图表参数读者可快速复现招聘数据分析场景并在此基础上扩展自己的数据可视化项目。1. 智联招聘数据可视化卡点不在图表而在数据管道招聘类数据可视化项目在课程设计和简历项目里出现频率极高但大多数实现都停在“把 CSV 塞进数据库再拿 ECharts 画几个折线图”的层次。真正让这类系统产生价值的是字段设计如何处理薪资区间、发布时间、学历门槛这类脏数据以及后端聚合查询在百万行数据下能不能顶住。本文不假定你拿到的是某一套完整源代码而是讲清楚做“Django 智联招聘数据分析系统”时从数据库建模、导入清洗、接口聚合到可视化落地的一整套可复制方案顺手把 Django 和 mysqlclient 编译、admin 后台建表这类高频坑一并排掉。这套路径对三类人最有用正在做数据库课程设计或毕业设计的学生、想用 Django 快速搭建数据看板的初级工程师、需要给业务方交付招聘趋势报表的后端开发。整个方案以 Django 4.x/5.x 为基础数据层用 MySQL图表层用 ECharts全部代码可以在本地直接跑通。2. Django 数据模型设计招聘数据要拆成两张表还是三张表很多人在第一步就走偏把智联招聘采集结果一股脑塞进一张大宽表字段包括岗位名称、公司名称、薪资、地点、学历、经验、发布时间。单表在演示时没问题但一旦要做“某一城市薪资趋势”或“指定行业岗位增量”这类聚合查询宽表的索引命中率会很难看而且薪资字段如果不拆开会直接毁掉所有可视化维度。2.1 核心表结构岗位事实表与维度表拆分常规做法是拆成岗位信息表job_post和公司维度表company岗位表通过外键关联公司。之所以不把公司字段冗余在岗位表里是因为同一家公司会发布大量岗位公司规模、行业属性、融资阶段这些维度字段重复存储会把数据库体积撑大两三倍同时让 group by 聚合的 IO 成本上升。见下面 Django models.py 的写法from django.db import models class Company(models.Model): name models.CharField(max_length128, db_indexTrue) industry models.CharField(max_length64, blankTrue) scale models.CharField(max_length32, blankTrue) # 规模: 100-499人 financing models.CharField(max_length32, blankTrue) # 融资阶段 class Meta: db_table recruit_company class JobPost(models.Model): title models.CharField(max_length128) company models.ForeignKey(Company, on_deletemodels.CASCADE) city models.CharField(max_length32, db_indexTrue) district models.CharField(max_length32, blankTrue) salary_min models.IntegerField(default0) # 单位: K salary_max models.IntegerField(default0) salary_avg models.FloatField(default0) edu_level models.CharField(max_length16, blankTrue) experience models.CharField(max_length32, blankTrue) publish_date models.DateField(db_indexTrue) crawl_date models.DateTimeField(auto_now_addTrue) class Meta: db_table recruit_job_post indexes [ models.Index(fields[city, publish_date]), ]这段代码里最值得留意的是薪资字段的拆分salary_min、salary_max、salary_avg三个数值字段分别代表最低月薪、最高月薪和折算后的平均月薪。智联招聘页面上薪资写成“15-20K·14薪”这种文本直接存字符串没法算趋势必须解析后拆开存。折算平均薪时把“14薪”也考虑进去注意 15-20K·14薪 的年薪是 17.5K × 14均摊到 12 个月后要再加一点系数这个细节在后面的导入脚本里处理。为什么不用 JSONField 存薪资如果只是展示岗位详情JSONField 可以但可视化系统要按salary_avg做 AVG、PERCENTILE 这类聚合JSON 字段在 MySQL 里聚合性能会掉一个数量级。Django 的 JSONField 在 MySQL 5.7 走 JSON 类型查询时对内部字段做-表达式是可以的但索引失效和隐式转换问题会让 10 万行以上的聚合明显变慢。数值拆列、建普通 B 树索引是最稳妥的路。2.2 导入脚本处理脏数据的三个必写分支数据可视化系统的成败一半取决于导入时的清洗逻辑。智联接口或爬虫拿到的最常见脏场景有三个薪资文本格式不统一、工作地点含“上海-浦东新区”这种带区县的字符串、发布日期有“发布于 2024-10-12”这种前缀文案。我在写导入脚本时一般会这样做import re from datetime import datetime from django.utils.dateparse import parse_date def parse_salary(text): # 输入: 15-20K·14薪 / 8千-1万 / 面议 if not text or text.strip() in (面议, 薪资面议): return 0, 0, 0 text text.replace(万, K).replace(千, K) numbers re.findall(r\d\.?\d*, text) if len(numbers) 2: return 0, 0, 0 min_sal float(numbers[0]) max_sal float(numbers[1]) avg_raw (min_sal max_sal) / 2 # 14薪均摊: 每年多发2个月平摊到12个月 months re.search(r(\d)薪, text) avg avg_raw * (int(months.group(1)) / 12.0) if months else avg_raw return int(min_sal), int(max_sal,), round(avg, 1) def parse_city(raw_location): # 输入: 北京-海淀区 - 北京; 广州 - 广州 parts raw_location.split(-) return parts[0].strip(), parts[1].strip() if len(parts) 1 else def parse_publish_date(raw_date): raw_date raw_date.replace(发布于, ).strip() dt parse_date(raw_date) return dt or datetime.now().date()导入脚本的核心逻辑不是取数而是归一化。salary_min单位统一成 K所有薪资低于 3K 的数据要么是实习生要么是脏数据导入时可以由你决定是否过滤但更稳妥的做法是保留在库中并打上标签不要简单丢弃因为后续“学历-薪资交叉分析”时低薪数据可能代表兼职岗位丢弃会导致结论失真。日期字段建议始终用DateField而不是DateTimeField按天做趋势分析时DateField可以直接 group by如果存了时间戳就得多做一层TruncDate转换Django 查询表达式会多一截数据量大时性能差距明显。3. Django 接口层用 ORM 聚合代替逐行 for 循环模型建好后数据可视化系统的后端核心就是接口层。实际项目中很多代码把聚合逻辑写到 Python 的 for 循环里数据量过万后响应时间会急剧变长正确做法是让 MySQL 完成 group by 和聚合计算Django 只负责在 ORM 层把结果转成序列化字典。这里的核心工具是django.db.models里的Count、Avg、F函数。3.1 按城市和日期统计岗位数量招聘趋势分析最基础的视图是“指定时间范围内各城市岗位发布数量”它回答“哪座城市在招人最多”的问题。API 设计上我建议直接返回 JSON配合前端 ECharts 使用而不是在 Python 里拼 HTML 片段。from django.db.models import Count from django.http import JsonResponse from .models import JobPost def job_trend_by_city(request): city request.GET.get(city, ) days int(request.GET.get(days, 30)) start_date timezone.now().date() - timedelta(daysdays) qs JobPost.objects.filter(publish_date__gtestart_date) if city: qs qs.filter(citycity) result (qs .values(city, publish_date) .annotate(cntCount(id)) .order_by(city, publish_date)) # 把结果转成前端友好的结构 data {} for item in result: key f{item[city]}|{item[publish_date]} data[key] item[cnt] return JsonResponse({ok: True, data: data})这段代码的要点在哪里首先values(city, publish_date)配合annotate(cntCount(id))是在数据库层完成分组计数生成的不再是模型对象列表而是字典列表。其次publish_date__gte过滤条件推动索引命中避免全表扫描。最后前端拿到扁平 map 后自己转换成图表的 data 结构。如果你需要的是“各城市岗位总量 Top10”把 group by 字段换成city并加.order_by(-cnt)[:10]即可不需要额外写 SQL。需要注意的一个细节是ORM 聚合返回的字典里的 key 是你在values()里写的名字如果你在values()里写city结果里就是city别把它当city_id。如果两个字段用到了外键关联如company__industry事后再用 id 转换是常见 bug 来源直接在values()里写company__industry即可。3.2 薪资分布与学历交叉分析第二个高频组合条件是薪资和学历。这个接口响应“某个城市本科学历平均薪资是多少”或“各学历薪资中位数”这类问题。注意 MySQL 本身没有内置 MEDIAN 函数但可以通过PERCENTILE_CONT实现Django ORM 没有封装这个函数常见做法是取AVG或用近似值。from django.db.models import Avg def salary_analysis(request): city request.GET.get(city, 全部) fields request.GET.get(group, edu_level) qs JobPost.objects.all() if city ! 全部: qs qs.filter(citycity) qs qs.exclude(salary_avg0) # 把“面议”数据排除 result (qs .values(fields) .annotate( avg_salaryAvg(salary_avg), cntCount(id) ) .order_by(-cnt)) return JsonResponse({ok: True, data: list(result)})exclude(salary_avg0)是保证分析画图不出现断点悬崖的关键面议岗位实际薪资会偏离市场均值如果在导入时已经过滤掉一部分这里再做一次兜底可以让分析结果更稳健。字段group支持灵活传参前端切换“按学历”“按经验”时后端可以复用同一个 View不做if/else堆逻辑。如果你的数据量到达几十万行聚合查询会让 MySQL 的临时表增大响应时间可能从 50ms 涨到 1s 以上。这时候我会做两件事一是给salary_avg、city、publish_date建联合索引二是引入 Django 的QuerySet.cache()——准确说是from django.core.cache import cache把聚合结果按参数做缓存设 600 秒过期。注意写好缓存 key必须把城市和分组字段拼进去否则不同筛选条件会拿到同一份旧数据。3.3 数据库连接与 mysqlclient 的坑Django 连 MySQL 需要驱动常见选择有两个pymysql和mysqlclient。核心诉求是稳定的话直接上 mysqlclient它的 C 扩展性能好得多。在 Debian/麒麟这类 Linux 系统上部署时会遇到编译报错因为mysql_config路径不在 PATH 里。pip install django mysqlclient # 如果上面安装失败先装系统依赖 # sudo apt install python3-dev default-libmysqlclient-dev pkg-configmysqlclient在 Windows 上通常要下预编译的 whl 包国内镜像站可以解决问题在麒麟或 CentOS 上则用yum install mysql-devel补依赖。真正要注意的是 Django 5.x 对 MySQL 版本有下限要求需要 MySQL 8.0 及以上如果还在用 5.7 要么升级要么退回 Django 4.2 LTS 版本这是项目初始化时就要确认的兼容矩阵。4. 可视化层Django 模板还是 Vue 前后端分离可视化系统的展示层如何选型会显著影响开发工作量。招聘数据可视化项目在课程设计和企业内部工具这两个场景下最常见的落地方式仍然是 Django 模板 原生 HTML ECharts CDN。前后端分离是主流但对一个“看板型”项目来说纯模板方案可以少写一整套跨域配置和 token 鉴权坑少交付快。4.1 Django 模板渲染数据到 ECharts在 Django 视图里把聚合结果传入模板然后在前端 JS 里用模板变量初始化 ECharts 实例。下面以“近 30 天岗位发布趋势折线图”为例这是整个系统最容易出效果也最容易出 bug 的地方因变量未正确序列化导致 JS 语法错误的现象很常见。def dashboard(request): days 30 start_date timezone.now().date() - timedelta(daysdays) trend ( JobPost.objects .filter(publish_date__gtestart_date) .extra(select{day: DATE(publish_date)}) .values(day) .annotate(cntCount(id)) .order_by(day) ) # 补全空日期确保前端点线不会断 date_range [start_date timedelta(daysi) for i in range(days)] date_map {item[day]: item[cnt] for item in trend} full_data [date_map.get(d.strftime(%Y-%m-%d), 0) for d in date_range] return render(request, dashboard.html, { dates: [d.strftime(%m-%d) for d in date_range], counts: full_data, })这个视图和前面的 JSON API 视角不同它直接为模板渲染服务。用到.extra(select{day: DATE(publish_date)})是因为 Django ORM 没有内置精确到天的忽略时间截断借助extra可以在 SQL 层调用 MySQL 的DATE函数。date_map补零逻辑确保了工作日没有数据的天显示为 0 而不是断线这是磁盘图表不连续的常见原因。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title招聘数据可视化 Dashboard/title script srchttps://cdn.jsdelivr.net/npm/echarts5/script /head body div idtrend-chart stylewidth: 100%; height: 400px;/div script const chart echarts.init(document.getElementById(trend-chart)); const dates {{ dates|safe }}; const counts {{ counts|safe }}; chart.setOption({ title: { text: 近30天岗位发布趋势 }, tooltip: { trigger: axis }, xAxis: { type: category, data: dates }, yAxis: { type: value }, series: [{ type: line, data: counts, smooth: true, areaStyle: {} }] }); window.addEventListener(resize, () chart.resize()); /script /body /html{{ dates|safe }}这个过滤器的含义是让 Django 不再对列表字符串做 HTML 转义因为这里要解析成 JS 数组。没有加|safe时引号会被转成quot;浏览器控制台会报 “Unexpected token” 的语法错误第一次踩坑的人往往要排查半小时。如果你的数据里带有/script之类的内容要谨慎使用safe但这套系统里日期和数字都是安全的。4.2 词云与岗位技能热力图折线图只是开胃菜招聘数据可视化中区分完成度和优秀程度的通常是词云和热力图。智联招聘的“职位描述”文本字段可以做关键词频率统计常见做法是用jieba分词后统计词频。import jieba from django.db.models import Count def skill_cloud_data(request): from .models import JobPost # 取最近7天岗位 start_date timezone.now().date() - timedelta(days7) qs JobPost.objects.filter(publish_date__gtestart_date) text .join(qs.values_list(title, flatTrue)[:2000]) words jieba.lcut(text) stopwords set([开发, 工程师, 招聘, 岗位, 要求, 岗位职责]) word_count {} for w in words: if len(w) 2 or w in stopwords: continue word_count[w] word_count.get(w, 0) 1 top sorted(word_count.items(), keylambda x: x[1], reverseTrue)[:50] return JsonResponse({ok: True, data: [{name: w, value: c} for w, c in top]})这段代码的边界要从内存角度审视values_list(title, flatTrue)取出 2000 条文本拼成一个字符串再分词是可控的但如果把全库几十万条都拼进来会爆内存和高耗时。生产级别可以改成切片处理比如每天跑一次把词频结果缓存到 Redis 或数据库表避免用户每次打开词云都触发全量分词。可视化层用 ECharts 的wordCloud系列注意echarts-wordcloud不是核心模块需要单独引一个 JS 文件# 下载 echarts-wordcloud 插件并放到 static 目录 # 或者直接用 CDN: https://cdn.jsdelivr.net/npm/echarts-wordcloud24.3 企业级数据可视化的配色和布局细节“企业级数据可视化”不是靠复杂图表撑起来的而是靠排版和颜色语义招聘数据看板上冷色表示岗位量规模暖色表示薪资高低顶部一排 KPI 卡展示“城市岗位总数”“平均薪资”“最高薪岗位”“企业数”。ECharts 主题可以在初始化时注册// 注册自定义主题: 深色背景, 主色 #4e79a7, 辅助色 #f28e2b echarts.registerTheme(recruit, { backgroundColor: #1e1e1e, color: [#4e79a7, #f28e2b, #e15759, #76b7b2] }); const chart echarts.init(document.getElementById(chart), recruit);这些细节决定了看板被投屏到大屏时的观感也决定了面试官或答辩老师的第一印象。不要在一张页面里堆八个图表四到六个图表合理排布每个图表标题清晰坐标轴单位明确比“功能全”更重要。5. 智联招聘系统部署宝塔面板、collectstatic 和两个隐藏坑可视化系统开发完成后部署是另一个高频事故现场。每天都有开发者卡在静态文件 404 或者 MySQL 版本不兼容的问题上。这里把从项目初始化到上线的完整步骤列出来按这套操作可以少走弯路。5.1 宝塔部署 Django 的配置顺序宝塔面板是当前国内部署 Django 最常见的环境其 Nginx uWSGI 的协议栈配置关键点是静态文件交给 Nginx动态请求转发给 uWSGI。以下是若干关键配置项。server { listen 80; server_name your-domain.com; location /static/ { alias /www/wwwroot/recruit_project/staticfiles/; } location /media/ { alias /www/wwwroot/recruit_project/media/; } location / { include uwsgi_params; uwsgi_pass 127.0.0.1:8001; uwsgi_param UWSGI_CHDIR /www/wwwroot/recruit_project; uwsgi_param UWSGI_SCRIPT recruit_project.wsgi; } }部署前记得在settings.py里配置STATIC_ROOT os.path.join(BASE_DIR, staticfiles)然后执行python manage.py collectstatic把 Django 自带的 admin 静态文件复制到指定目录。很多人的 404 问题就是少跑了这一步。DEBUGFalse时 Django 不再伺服静态文件只靠 Nginx 的 alias 规则解析检查collectstatic是否执行成功是第一排查顺序。如果遇到 uWSGI 无法启动常见原因有两个一是recruit_project.wsgi的模块路径写错二是 Python 环境与 uWSGI 编译时使用的 Python 版本不一致用多 Python 版本的管理器时尤其常见。在宝塔里创建项目的时候注意选好 Python 版本再安装依赖和 uWSGI尽量全程用一个解释器。5.2 可视化看板性能优化Django 查询缓存与 MySQL 索引部署之后接踵而来的问题就是响应变慢。当数据量增长到几十万行后Django 的 ORM 查询如果不优化Dashboard 首屏加载可能超过 3 秒。我会优先做三件事缓存、聚合表、联合索引。from django.core.cache import cache def cached_dashboard_data(key_prefixdash_): cache_key f{key_prefix}_trend result cache.get(cache_key) if result: return result result heavy_query() # 上文的聚合逻辑 cache.set(cache_key, result, 600) return resultMySQL 上要建的最佳联合索引是(city, publish_date, salary_avg)这同时覆盖城市筛选、日期范围过滤和薪资聚合三条查询路径。用EXPLAIN SELECT ...可以确认查询是否走索引如果看到typeALL表示全表扫描必须回数据库层面调整。关于缓存内存缓存适合单机小项目多进程 uWSGI 下可以选 RedisDjango 的django-redis包配置即可。另一个隐藏坑是 MySQL 的sql_modeONLY_FULL_GROUP_BY。新版 MySQL 默认启用这个模式聚合查询时会直接报错。若你写values(city).annotate(cntCount(id)).order_by(cnt)没问题但若 select 的字段和 group by 不一致就会触发错误修改django.db.backends.mysql.options的init_command或直接改 MySQL 的配置文件推荐不改全局设置而是把 SQL 语句调整为符合 ONLY_FULL_GROUP_BY 规范的形式这是最不容易出问题的做法。5.3 验证图表数据正确性的一个脚本部署完成后最后一个很有用的动作是写一个单元测试来验证聚合结果与原始 CSV 一致。手动打开前端看数字太容易漏错误了用下面的测试把导入数据和可视化接口的结果对拍一遍。from django.test import TestCase from .models import JobPost, Company class RecruitDataTest(TestCase): def setUp(self): c Company.objects.create(name测试公司) for i in range(10): JobPost.objects.create( titlefJava开发{i}, companyc, city北京, salary_min15, salary_max25, salary_avg20, publish_date2024-10-01 ) def test_city_count(self): total (JobPost.objects .filter(city北京) .count()) self.assertEqual(total, 10) def test_avg_salary(self): avg (JobPost.objects .filter(city北京) .aggregate(avgAvg(salary_avg))[avg]) self.assertEqual(avg, 20.0)这类测试跑通后再部署能挡掉 80% 的“看起来工正常但数据不对”的发布事故。调试这类问题时直接看一下 SQL 是标准手段在 Django Shell 里对查询集打印.query属性即可看到它翻译出的原生 SQL排查 group by 错误或排序错误几乎一步到位。最后留下一个习惯建议把采集到的智联招聘原始 CSV 文件保留一份备份数据库可以随时从 CSV 重建防止初始化时测试数据污染生产数据。这也算是“源代码 数据库 使用说明”这份交付物里数据库脚本最值得保留的一条工程经验。本文还有配套的精品资源点击获取