
简介这份资源是面向高校学生与Python Web开发初学者的Django招聘数据可视化分析项目源码可直接用于期末大作业、课程设计或毕业设计。项目以Python为后端、Django为Web框架、Echarts为前端图表库围绕招聘数据完成采集、清洗与多维度可视化展示帮助读者理解从数据到图表的完整链路。压缩包共537个文件约32.32MB包含40个py业务代码、109个html页面模板、102个js脚本、24个css样式及75个gif演示素材另有dll、exe等运行依赖与配置说明文件目录结构清晰便于按模块阅读与二次修改。目前已有396人学习下载。资源已获96分高分通过部署简单下载后即可运行读者可据此掌握Django项目组织方式、Echarts图表配置与招聘数据分析思路快速完成一份结构完整、可演示的课程作品。1. 从一份招聘数据到三张图DjangoPythonEcharts 可视化分析到底在做什么招聘网站上的岗位数据单看一条没什么价值但把几千条岗位按城市、薪资、学历、经验维度聚合起来就能看出行业走向。这份期末大作业要干的事就是用 Python 把招聘数据清洗成结构化字段用 Django 搭一个能查询、能筛选的后端服务再用 Echarts 在前端把结果画成柱状图、饼图、折线图。整套链路跑通你手里就有了一份能演示、能答辩、能继续扩展的可视化分析系统。它适合三类人正在找 Django 项目实战练手的新手需要交期末大作业的学生以及想快速搭一个数据看板原型的开发者。核心难点不在 Django 本身而在数据清洗的字段映射、Echarts 的配置项对接、以及前后端数据格式的统一。下面按「数据怎么来 → 后端怎么建 → 图表怎么画 → 坑在哪」的顺序拆开讲。2. 招聘数据从哪来、怎么洗成 Django 能吃的格式2.1 数据来源与字段设计招聘数据的常见来源有三类公开数据集、自己用 requests 抓取的岗位列表、以及老师直接给的 CSV 文件。不管哪种原始数据通常长这样岗位名称、公司名、城市、薪资区间、学历要求、经验要求、岗位描述。薪资区间往往是「15-25K·13薪」这种字符串城市可能是「北京·朝阳区」学历可能是「本科及以上」。这些字段直接入库没法聚合必须先拆。我一般会设计一张 Job 表字段控制在 8 个以内够用就行字段名类型说明titleCharField岗位名称如「Python 开发工程师」companyCharField公司名称cityCharField城市只保留「北京」「上海」这种salary_minIntegerField薪资下限单位 Ksalary_maxIntegerField薪资上限单位 KeducationCharField学历统一成「大专/本科/硕士/不限」experienceCharField经验统一成「应届/1-3年/3-5年/5年以上」publish_dateDateField发布日期字段设计的原则是能聚合的字段单独存不能聚合的原文丢到 TextField 里备查。薪资拆成 min 和 max 两个整数后面算平均值、画区间分布都方便。2.2 清洗脚本把「15-25K·13薪」拆成两个整数清洗这一步是整个项目最脏最累的活。下面这个脚本处理薪资、城市、学历三个字段直接可以抄import re import csv from datetime import datetime def parse_salary(raw): 把 15-25K·13薪 解析成 (15, 25)解析失败返回 (None, None) if not raw: return None, None # 匹配 15-25K 或 15K-25K 或 15-25千 match re.search(r(\d\.?\d*)\s*[-~]\s*(\d\.?\d*), raw) if match: low float(match.group(1)) high float(match.group(2)) # 如果单位是「万」乘以 10 转成 K if 万 in raw: low, high low * 10, high * 10 return int(low), int(high) # 只有一个数字的情况如 20K以上 single re.search(r(\d\.?\d*), raw) if single: val int(float(single.group(1))) return val, val return None, None def normalize_city(raw): 北京·朝阳区 - 北京 if not raw: return 未知 return raw.split(·)[0].split(-)[0].strip() def normalize_education(raw): 把各种写法统一成四档 if not raw: return 不限 raw raw.strip() if 不限 in raw or 无 in raw: return 不限 if 硕士 in raw or 研究生 in raw: return 硕士 if 本科 in raw or 学士 in raw: return 本科 if 大专 in raw or 专科 in raw: return 大专 return 不限 def clean_csv(input_path, output_path): 读原始 CSV输出清洗后的 CSV with open(input_path, r, encodingutf-8) as f_in, \ open(output_path, w, encodingutf-8, newline) as f_out: reader csv.DictReader(f_in) writer csv.writer(f_out) writer.writerow([title, company, city, salary_min, salary_max, education, experience, publish_date]) for row in reader: smin, smax parse_salary(row.get(salary, )) writer.writerow([ row.get(title, ).strip(), row.get(company, ).strip(), normalize_city(row.get(city, )), smin, smax, normalize_education(row.get(education, )), row.get(experience, 不限).strip(), row.get(publish_date, datetime.now().strftime(%Y-%m-%d)) ]) if __name__ __main__: clean_csv(raw_jobs.csv, clean_jobs.csv) print(清洗完成)这段代码的逻辑分三层parse_salary用正则抓数字区间遇到「万」单位做换算抓不到就返回 None 而不是报错保证脚本能跑完normalize_city按分隔符切掉区县normalize_education用关键词匹配归到四档。参数上唯一要注意的是编码CSV 如果是 GBK 就改成encodinggbk否则会报 UnicodeDecodeError。清洗完先别急着入库用 pandas 快速看一眼分布确认没有大量 Noneimport pandas as pd df pd.read_csv(clean_jobs.csv) print(df[city].value_counts().head(10)) print(df[[salary_min, salary_max]].describe()) print(薪资缺失条数:, df[salary_min].isna().sum())如果缺失超过 20%说明原始数据的薪资字段格式太乱得回去补正则规则。这一步是后面所有图表准确性的地基别跳过。3. Django 后端建模型、写接口、把聚合结果吐给前端3.1 建 app 和模型把清洗后的数据灌进去Django 项目实战新手的第一个卡点通常是 app 建在哪、模型怎么写。标准流程是django-admin startproject jobvis cd jobvis python manage.py startapp jobs然后在jobs/models.py里定义模型from django.db import models class Job(models.Model): title models.CharField(max_length200) company models.CharField(max_length200, blankTrue) city models.CharField(max_length50, db_indexTrue) salary_min models.IntegerField(nullTrue, blankTrue) salary_max models.IntegerField(nullTrue, blankTrue) education models.CharField(max_length20, db_indexTrue) experience models.CharField(max_length20, blankTrue) publish_date models.DateField(nullTrue, blankTrue) class Meta: db_table job indexes [ models.Index(fields[city, education]), ]db_indexTrue加在 city 和 education 上因为后面按这两个字段 group by 查询最频繁。数据量到几万条时没索引的聚合查询会明显变慢。灌数据用 Django shell 或者写一个 management command别手动一条条加# jobs/management/commands/import_jobs.py import csv from django.core.management.base import BaseCommand from jobs.models import Job class Command(BaseCommand): help 从清洗后的 CSV 导入岗位数据 def add_arguments(self, parser): parser.add_argument(csv_path, typestr) def handle(self, *args, **options): path options[csv_path] batch [] with open(path, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: batch.append(Job( titlerow[title], companyrow[company], cityrow[city], salary_minint(row[salary_min]) if row[salary_min] else None, salary_maxint(row[salary_max]) if row[salary_max] else None, educationrow[education], experiencerow[experience], publish_daterow[publish_date] or None, )) if len(batch) 500: Job.objects.bulk_create(batch) batch [] if batch: Job.objects.bulk_create(batch) self.stdout.write(f导入完成共 {Job.objects.count()} 条)bulk_create每 500 条一批比逐条save()快一个数量级。跑命令python manage.py import_jobs clean_jobs.csv。3.2 三个聚合接口城市分布、薪资区间、学历占比前端 Echarts 要的数据格式是{name: 北京, value: 320}这种数组。后端用 Django ORM 的annotate和values直接聚合别在 Python 里循环统计# jobs/views.py from django.http import JsonResponse from django.db.models import Count, Avg, Q from .models import Job def city_distribution(request): 各城市岗位数量 Top 15 data (Job.objects .values(city) .annotate(valueCount(id)) .order_by(-value)[:15]) result [{name: item[city], value: item[value]} for item in data] return JsonResponse({data: result}) def salary_by_education(request): 不同学历的平均薪资 data (Job.objects .exclude(salary_min__isnullTrue) .values(education) .annotate(avg_salaryAvg(salary_min)) .order_by(-avg_salary)) result { categories: [item[education] for item in data], values: [round(item[avg_salary], 1) for item in data] } return JsonResponse(result) def experience_pie(request): 经验要求占比 data (Job.objects .values(experience) .annotate(valueCount(id)) .order_by(-value)) result [{name: item[experience] or 不限, value: item[value]} for item in data] return JsonResponse({data: result})三个接口对应三种 Echarts 图表city_distribution喂柱状图salary_by_education喂折线图或柱状图experience_pie喂饼图。注意exclude(salary_min__isnullTrue)这行薪资缺失的记录不能参与平均值计算否则会把均值拉低。路由配置# jobvis/urls.py from django.urls import path from jobs import views urlpatterns [ path(api/city/, views.city_distribution), path(api/salary/, views.salary_by_education), path(api/experience/, views.experience_pie), ]接口返回的都是纯 JSON前端拿到直接塞给 Echarts 的series.data不需要再转换。这是前后端对接最省事的方式。4. Echarts 前端柱状图、饼图、折线图的配置与数据对接4.1 页面骨架与 Echarts 引入前端不需要上 Vue 或 React一个 HTML 文件加 CDN 引入 Echarts 就够了。期末大作业的重点是图表能出来不是前端工程化。!DOCTYPE html html langzh head meta charsetUTF-8 title招聘数据可视化/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script style .chart { width: 100%; height: 400px; margin-bottom: 30px; } /style /head body h2各城市岗位数量/h2 div idcityChart classchart/div h2不同学历平均薪资/h2 div idsalaryChart classchart/div h2经验要求占比/h2 div idexpChart classchart/div script // 下面逐个初始化图表 /script /body /htmlEcharts 5 的 CDN 地址直接用官方的别去抄那些来路不明的镜像。三个 div 分别对应三张图高度给 400px太矮了图例会挤在一起。4.2 柱状图城市分布 渐变色配置Echarts 柱状图设置渐变色是热搜里问得最多的。用new echarts.graphic.LinearGradient实现fetch(/api/city/) .then(res res.json()) .then(res { const chart echarts.init(document.getElementById(cityChart)); chart.setOption({ tooltip: { trigger: axis }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: category, data: res.data.map(item item.name), axisLabel: { rotate: 30 } // 城市名太长时倾斜 }, yAxis: { type: value, name: 岗位数 }, series: [{ type: bar, data: res.data.map(item item.value), barWidth: 50%, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: #83bff6 }, { offset: 1, color: #188df0 } ]) }, label: { show: true, position: top // 数值显示在柱子顶部 } }] }); });LinearGradient四个参数是 x0, y0, x1, y1(0,0,0,1)表示从上到下渐变。axisLabel.rotate: 30解决城市名重叠问题这是 Echarts 折线图 x 轴刻度太密集时的通用解法。label.position: top把数值标在柱子上方对应热搜里「y 轴的标签如何在每个柱子上面显示」。4.3 饼图经验占比 中间文字Echarts 饼图中间的字用title组件定位到圆心实现fetch(/api/experience/) .then(res res.json()) .then(res { const chart echarts.init(document.getElementById(expChart)); const total res.data.reduce((sum, item) sum item.value, 0); chart.setOption({ tooltip: { trigger: item, formatter: {b}: {c} ({d}%) }, title: { text: total \n岗位, left: center, top: center, textStyle: { fontSize: 16, color: #666 } }, legend: { orient: vertical, left: left }, series: [{ type: pie, radius: [40%, 65%], // 环形中间留空放文字 center: [50%, 50%], data: res.data, emphasis: { itemStyle: { shadowBlur: 10, shadowColor: rgba(0,0,0,0.3) } } }] }); });radius: [40%, 65%]做成环形图中间才有位置放总数。如果写成radius: 60%就是实心饼图title会盖在扇区上。formatter: {d}%自动算百分比不用自己除。4.4 折线图学历与薪资的关系fetch(/api/salary/) .then(res res.json()) .then(res { const chart echarts.init(document.getElementById(salaryChart)); chart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: res.categories, boundaryGap: false }, yAxis: { type: value, name: 平均薪资(K) }, series: [{ type: line, data: res.values, smooth: true, areaStyle: { opacity: 0.2 }, markPoint: { data: [{ type: max, name: 最高 }] } }] }); });boundaryGap: false让折线从 y 轴开始不留空隙。smooth: true平滑曲线areaStyle加面积填充。markPoint自动标注最高点答辩时这个细节很加分。三张图的数据都来自后端接口改数据只需要重新导入 CSV前端不用动。这就是前后端分离的好处。5. 避坑排查从数据到图表最容易翻车的 5 个地方5.1 薪资平均值算出来是 0 或者异常低现象折线图上所有学历的平均薪资都是个位数明显不对。原因原始数据里薪资单位混用有的写「15-25K」有的写「1.5-2.5万」清洗时没统一换算1.5 被当成 1.5K 入库了。解决在parse_salary里加单位判断遇到「万」统一乘 10 转成 K。入库前用df[salary_min].describe()检查 min 值正常应该在 3 以上出现 1 或 2 就是单位没转。5.2 饼图中间的文字被扇区盖住现象title设置了但看不见或者被饼图挡住一半。原因用了实心饼图radius: 60%圆心被扇区填满title的 z 层级低于 series。解决改成环形图radius: [40%, 65%]或者给 title 加z: 10提高层级。环形图是标准做法视觉上也更好看。5.3 Django 接口返回中文乱码现象前端拿到的城市名是\u5317\u4eac这种 Unicode 转义。原因JsonResponse默认json_dumps_params{ensure_ascii: True}。解决改成JsonResponse(data, json_dumps_params{ensure_ascii: False})。这个坑不影响功能但调试时看着难受。5.4 数据量大了之后接口超时现象导入 5 万条数据后城市分布接口要 3 秒以上才返回。原因values(city).annotate(Count(id))没走索引全表扫描。解决在Job.Meta.indexes里给 city 加索引然后python manage.py makemigrations migrate。加完索引后同样的查询能降到 200ms 以内。如果还慢考虑用 Redis 缓存聚合结果设置 10 分钟过期。5.5 Echarts 图表在切换 tab 后宽度变成 100px现象图表放在隐藏的 tab 里切换过去后图被压扁。原因Echarts 初始化时容器display: none拿不到实际宽度。解决在 tab 切换的回调里调chart.resize()。如果用的是原生 JS监听 tab 按钮的 click 事件延迟 100ms 再 resize。这个坑在单页应用里特别常见血泪经验。6. 让这份大作业多拿 10 分三个进阶技巧第一个技巧是给图表加数据筛选。在页面上放一个下拉框选城市选完后只请求该城市的数据。后端接口加一个city查询参数def city_distribution(request): city request.GET.get(city) qs Job.objects.all() if city: qs qs.filter(citycity) data qs.values(city).annotate(valueCount(id)).order_by(-value)[:15] return JsonResponse({data: [{name: i[city], value: i[value]} for i in data]})前端 fetch 时拼上?city北京。这个改动不大但答辩时演示「交互式筛选」比静态图有说服力。第二个技巧是用 Echarts 的dataset替代手动 map。现在每个图都要res.data.map(item item.name)数据源换了就得改代码。用dataset可以直接把后端返回的数组喂进去chart.setOption({ dataset: { source: res.data }, xAxis: { type: category }, yAxis: {}, series: [{ type: bar, encode: { x: name, y: value } }] });encode指定哪个字段对应 x 轴、哪个对应 y 轴。后端加字段前端不用改扩展性更好。第三个技巧是导出图片。Echarts 自带getDataURL加一个按钮document.getElementById(exportBtn).onclick function() { const url chart.getDataURL({ type: png, pixelRatio: 2, backgroundColor: #fff }); const a document.createElement(a); a.href url; a.download city_chart.png; a.click(); };pixelRatio: 2导出高清图直接贴进报告里。这个功能实现只要 5 行代码但报告里图表清晰度提升明显。最后说一个我自己的习惯每次改完清洗脚本先跑一遍python manage.py shell手动查几条数据确认字段对得上再批量导入。直接导几万条然后发现字段错位删表重来的时间够你查十遍。数据清洗是整个项目里最容易返工的一环慢就是快。希望帮到你。本文还有配套的精品资源点击获取