Django+Echarts招聘数据可视化实战:可运行、可扩展、可交付 简介这是一套基于Django框架与Python后端、结合ECharts前端图表库实现的招聘数据可视化分析完整项目源码面向Web开发初学者及数据分析入门者解决招聘行业数据采集、清洗、存储与多维度动态展示的实际问题。资源包共165个文件含14个核心Python脚本涵盖Django视图、模型、爬虫逻辑与数据处理模块、51个JavaScript文件ECharts配置与交互逻辑、39个JSON格式的模拟/示例招聘数据集以及12个CSS样式文件含Font Awesome图标库与响应式布局支持整体压缩包仅4.68MB轻量易部署。目前已有751人学习下载代码经实测可直接运行无需额外调试附带SQLite3本地数据库与完整目录结构便于理解前后端分离设计思路、Django ORM数据流转机制及ECharts动态渲染流程是掌握Web数据可视化全栈实践的优质入门范例。1. 这不是又一个“静态图表Demo”DjangoPythonEcharts招聘数据可视化源码真能跑通、真有业务逻辑、真能改出你自己的看板你肯定见过太多标着“DjangoEcharts可视化”的项目——点开一看views.py里硬编码了5条假数据templates/index.html里Echarts配置写死在script标签里连manage.py runserver都得手动改ALLOWED_HOSTS才能不报错。这次不一样。这个DjangoPythonEcharts对招聘数据进行可视化分析源码.zip我亲手解压、装依赖、建库、跑迁移、导入示例数据、调接口、改图表、换字段全程没动过requirements.txt以外的任何第三方包也没补过一行“兼容性补丁”。它不是一个教学玩具而是一个可直接嵌入招聘中台、HR数据分析后台或校招系统前端的轻量级可视化模块后端用Django REST Framework暴露结构化API中间用Python做真实的数据清洗去重、薪资归一化、城市分级、岗位聚类前端用Echarts 5.x原生渲染响应式图表支持按时间范围筛选、多维度联动下钻、导出PNG。适合正在做校园招聘系统升级的后端工程师、需要快速交付HR数据看板的全栈开发者以及想拿真实项目练手、避开“Hello World级Django”的Python初学者——只要你愿意花30分钟配好环境就能看到带搜索框、带筛选器、带动态刷新的真实招聘数据仪表盘。2. 从零启动Django项目结构拆解与核心模块运行逻辑这个源码包不是“扔给你一个manage.py就完事”的黑匣子。它采用清晰分层设计data_processor/负责原始招聘数据ETLapi/提供RESTful接口dashboard/是主应用static/里Echarts资源组织规范templates/模板完全解耦图表逻辑。下面带你一层层剥开看清它为什么能“确保可以运行”。2.1 项目目录结构与职责划分每个文件夹都在解决一个具体问题解压后你会看到标准Django项目骨架但关键在于各模块的分工是否合理├── manage.py ├── requirements.txt # 明确限定版本Django4.2.7, django-filter23.3, pandas1.5.3, pyecharts2.0.4注意不是最新版是经过实测兼容的 ├── recruitment/ # 项目配置目录settings.py在此 │ ├── __init__.py │ ├── settings.py # 关键DEBUGTrue默认开启DATABASES使用SQLite3开箱即用STATICFILES_DIRS已预设指向static/ │ ├── urls.py # 路由总入口已包含api/和dashboard/的include │ └── wsgi.py ├── data_processor/ # 真正干活的模块不只读CSV还做字段标准化 │ ├── __init__.py │ ├── management/ # Django自定义命令python manage.py load_jobs │ │ └── commands/ │ │ └── load_jobs.py # 核心从sample_data/jobs.csv导入并清洗 │ └── utils.py # 清洗函数normalize_salary()、extract_city()、classify_job_title() ├── api/ # REST API层不是简单序列化而是带聚合逻辑 │ ├── __init__.py │ ├── apps.py │ ├── models.py # JobPost模型含salary_min/salary_max、city、job_type、publish_date等真实字段 │ ├── serializers.py # 序列化器对salary字段做区间转数字、对city做映射如“北京”→“华北” │ ├── views.py # 视图集JobStatsViewSet提供按月统计、按城市分布、按岗位类型TOP10等6个聚合接口 │ └── urls.py # /api/stats/city/, /api/stats/monthly/ 等路径已注册 ├── dashboard/ # 前端展示层纯HTMLJS无Vue/React干扰 │ ├── __init__.py │ ├── apps.py │ ├── views.py # render主页面不处理业务逻辑只传context{chart_config: ...} │ └── urls.py # /dashboard/ 路径 ├── static/ # Echarts资源集中地你看到的.css文件全在这里 │ ├── css/ │ │ ├── styles.css # 自定义布局侧边筛选栏宽度、图表容器高度、响应式断点 │ │ └── fontawesome-all.min.css # 图标字体筛选按钮、导出图标用 │ ├── js/ │ │ ├── echarts.min.js # Echarts 5.4.2压缩版非CDN离线可用 │ │ ├── dashboard.js # 核心初始化Echarts实例、绑定筛选事件、调用/api/stats/接口、更新图表 │ │ └── utils.js # 封装fetch请求、错误提示、loading状态控制 │ └── fonts/ # FontAwesome字体文件 ├── templates/ │ └── dashboard/ │ └── index.html # 主模板引入static/js/下的JS预留div idchart-city等容器 ├── sample_data/ # 示例数据jobs.csv1200条真实爬取脱敏数据含岗位、城市、薪资、经验要求 │ └── jobs.csv └── db.sqlite3 # 初始化空数据库首次运行migrate后生成提示sample_data/jobs.csv是整个项目的生命线。它不是5行测试数据而是1287条覆盖一线至四线城市的招聘记录字段包括job_title、company_name、city、salary_range如“15K-25K”、experience_required如“3-5年”、publish_date格式为2023-08-15。所有清洗、聚合、图表渲染都基于此数据驱动——这意味着你替换自己的CSV改两行代码就能复用整套流程。2.2 数据清洗逻辑详解Python如何把“15K-25K”变成可统计的数字很多类似项目卡在第一步薪资字段是字符串无法求平均值。这个源码包的data_processor/utils.py给出了生产级处理方案# data_processor/utils.py import re import pandas as pd def normalize_salary(salary_str): 将15K-25K、面议、8000-12000元/月等字符串统一转为月薪中位数单位元 返回 float 或 None无法解析时 if not isinstance(salary_str, str) or 面议 in salary_str: return None # 匹配数字K/k 或 数字支持中文元、英文/month pattern r(\d\.?\d*)[kK]?(?:-|~|—|至)?(\d\.?\d*)[kK]?(?:元|\/month|\/月)? match re.search(pattern, salary_str.strip()) if match: low, high match.groups() if high: # 有区间取中位数 low_val float(low) * (1000 if k in salary_str.lower() else 1) high_val float(high) * (1000 if k in salary_str.lower() else 1) return round((low_val high_val) / 2) else: # 单值如15K return round(float(low) * (1000 if k in salary_str.lower() else 1)) # 尝试匹配纯数字如8000-12000 pure_num_pattern r(\d)-(\d) pure_match re.search(pure_num_pattern, salary_str) if pure_match: return round((int(pure_match.group(1)) int(pure_match.group(2))) / 2) return None这段代码的关键在于容错性它不假设输入格式统一而是用正则覆盖常见变体K/小写k、中文破折号、英文短横、面议字样并自动识别单位K1000。load_jobs.py命令调用此函数时会将清洗后的数值存入JobPost.salary_median字段后续所有图表如“各城市平均薪资柱状图”都基于此字段聚合。这不是炫技而是避免你在真实数据中因一条“年薪30W”记录导致整张图Y轴爆炸。2.3 Echarts图表配置与Django数据桥接如何让Python字典变成JS可渲染对象前端dashboard.js不直接写死Echarts option而是通过Django模板引擎注入JSON配置!-- templates/dashboard/index.html -- script // 从Django context传入的初始配置已序列化为JSON const initialChartConfig {{ chart_config|safe }}; // 初始化Echarts实例 const chartCity echarts.init(document.getElementById(chart-city)); chartCity.setOption(initialChartConfig); /script而dashboard/views.py中生成chart_config的逻辑是# dashboard/views.py from django.shortcuts import render from django.http import JsonResponse from api.views import JobStatsViewSet # 复用API层的聚合逻辑 import json def dashboard_view(request): # 调用API层的聚合方法获取城市分布数据 stats_view JobStatsViewSet() city_data stats_view.get_city_distribution() # 返回 [{city: 北京, count: 245}, ...] # 构建Echarts option字典非JSON字符串 chart_config { tooltip: {trigger: item}, legend: {top: 5%, left: center}, series: [{ name: 岗位数量, type: pie, radius: [40%, 70%], avoidLabelOverlap: False, itemStyle: {borderRadius: 10}, label: {show: False}, emphasis: {label: {show: True, fontSize: 16}}, data: [{value: item[count], name: item[city]} for item in city_data] }] } return render(request, dashboard/index.html, {chart_config: json.dumps(chart_config)})这里的关键是数据聚合在Python层完成保证逻辑一致性图表配置在Python层组装避免JS里拼接字符串最终以JSON字符串安全注入模板。你不需要在JS里写fetch(/api/stats/city/)再手动解析——所有首屏图表都是服务端直出加载快、SEO友好、调试直观。如果要加新图表只需在views.py里新增一个聚合方法在index.html里加一个div idchart-new再在chart_config字典里追加对应series即可。3. 接口设计与Echarts联动机制6个REST API如何支撑完整看板这个项目的API层不是摆设而是真正被前端dashboard.js调用的“数据中枢”。所有图表交互筛选、刷新、下钻都通过AJAX请求这些接口实现。理解它们的输入输出是你二次开发的基础。3.1 六大核心API端点与业务语义URLHTTP方法输入参数输出示例精简业务用途前端调用时机/api/stats/city/GET无[{city:北京,count:245},{city:上海,count:198}]各城市岗位数量分布页面加载时初始化“城市分布饼图”/api/stats/monthly/GETstart2023-01end2023-12[{month:2023-01,count:120},{month:2023-02,count:135}]按月岗位发布趋势“时间范围筛选器”变更后重新请求/api/stats/salary/GETcity北京{avg_salary:18500,median_salary:17200,min_salary:8000,max_salary:45000}指定城市薪资统计点击饼图某城市区块后弹出详情/api/stats/job_type/GETlimit10[{job_type:后端开发,count:320},{job_type:产品经理,count:187}]岗位类型TOP N初始化“岗位类型柱状图”/api/stats/experience/GET无[{exp:应届生,count:89},{exp:1-3年,count:421}]经验要求分布加载“经验要求环形图”/api/jobs/search/POST{keywords:[python,django],city:深圳,salary_min:10000}[{job_title:Django开发工程师,company:XX科技,salary:15K-25K}]多条件组合搜索“搜索框”提交后填充表格注意所有API均返回标准JSON无HTML包装。/api/jobs/search/是唯一POST接口其余均为GET符合RESTful规范。search/接口使用Django Filter后端支持关键词模糊匹配、城市精确匹配、薪资区间查询——这意味着你不用改一行JS就能在前端增加“最低月薪”滑块。3.2 前端dashboard.js如何用原生Fetch驱动图表联动static/js/dashboard.js是整个交互的灵魂。它不依赖jQuery用现代Fetch API与API层通信并严格管理Echarts实例状态// static/js/dashboard.js class DashboardController { constructor() { this.charts {}; this.initCharts(); this.bindEvents(); } initCharts() { // 初始化所有图表容器 this.charts.city echarts.init(document.getElementById(chart-city)); this.charts.monthly echarts.init(document.getElementById(chart-monthly)); this.charts.salary echarts.init(document.getElementById(chart-salary)); // ...其他图表 } bindEvents() { // 时间筛选器变更事件 document.getElementById(date-range).addEventListener(change, (e) { const { start, end } this.parseDateRange(e.target.value); this.loadMonthlyChart(start, end); // 重新请求并渲染 }); // 城市饼图点击事件下钻 this.charts.city.on(click, (params) { const city params.name; this.loadSalaryDetail(city); // 请求该城市薪资详情 }); } async loadMonthlyChart(start, end) { try { const response await fetch(/api/stats/monthly/?start${start}end${end}); const data await response.json(); // 构建Echarts option const option { xAxis: { type: category, data: data.map(d d.month) }, yAxis: { type: value }, series: [{ data: data.map(d d.count), type: line, smooth: true }] }; this.charts.monthly.setOption(option); } catch (err) { console.error(加载月度趋势失败:, err); this.showError(网络错误请检查后端是否运行); } } parseDateRange(value) { // 将2023-Q1转为2023-01和2023-03支持季度快捷选择 if (value.includes(-Q)) { const [year, q] value.split(-Q); const monthStart (parseInt(q) - 1) * 3 1; const monthEnd parseInt(q) * 3; return { start: ${year}-${monthStart.toString().padStart(2, 0)}, end: ${year}-${monthEnd.toString().padStart(2, 0)} }; } return { start: value, end: value }; // 默认单月 } } // 页面加载完成后初始化 document.addEventListener(DOMContentLoaded, () { new DashboardController(); });这段JS的关键设计点状态隔离每个图表有独立Echarts实例this.charts.city避免setOption互相污染错误兜底catch块统一处理网络错误前端显示友好提示而非白屏参数转换parseDateRange()支持“2023-Q1”这种业务常用快捷输入无需后端额外适配无框架依赖纯原生JS你可直接复制到任何项目中不引入额外打包负担。3.3 如何扩展新API以“学历要求分布”为例假设你要增加“学历要求分布”图表本科/硕士/不限只需三步无需动前端JS在api/models.py的JobPost模型中添加字段如果原始CSV有该列# api/models.py class JobPost(models.Model): # ...原有字段 education_required models.CharField(max_length20, blankTrue, nullTrue) # 新增在data_processor/management/commands/load_jobs.py中清洗并保存该字段# load_jobs.py 内 def handle(self, *args, **options): df pd.read_csv(sample_data/jobs.csv) # ...其他清洗 df[education_required] df[education].apply(lambda x: self.normalize_education(x)) # ...保存到JobPost在api/views.py中新增聚合方法并注册URL# api/views.py from rest_framework.response import Response from rest_framework.views import APIView class EducationStatsView(APIView): def get(self, request): # 聚合学历分布 data JobPost.objects.values(education_required).annotate( countCount(id) ).order_by(-count) return Response(list(data)) # api/urls.py 中添加 urlpatterns [ # ...原有路由 path(stats/education/, EducationStatsView.as_view(), nameeducation-stats), ]前端只需在dashboard.js里加一行this.loadEducationChart()调用再在index.html里加一个div idchart-education——整个新图表就活了。这就是设计良好的分层带来的扩展性。4. 避坑指南部署与运行中踩过的5个真实血泪坑别信“确保可以运行”的宣传语。我在三台不同配置的机器Mac M1、Windows 10 WSL2、Ubuntu 22.04上实测时至少遇到5个让新手卡住半小时以上的坑。以下全是真实现象、根本原因和一招解决法按出现频率排序4.1 现象python manage.py runserver启动成功但访问http://127.0.0.1:8000/dashboard/报404原因Django默认DEBUGFalse时STATICFILES_DIRS不生效且collectstatic未执行导致/static/js/dashboard.js404进而整个页面空白。但错误日志里只显示“GET /static/js/dashboard.js HTTP/1.1” 404极易被忽略。解决确保recruitment/settings.py中DEBUG True开发阶段必须若坚持DEBUGFalse则必须先运行python manage.py collectstatic --noinput并将STATIC_ROOT指向可写目录如/var/www/static再配置Nginx反向代理静态文件。4.2 现象python manage.py load_jobs执行后数据库里jobpost表为空命令无报错原因sample_data/jobs.csv文件编码不是UTF-8常见于Windows Excel另存为CSVPandas读取时中文字段乱码导致city、job_title等字段为NaN清洗函数normalize_salary()返回NoneDjango ORM因nullFalse字段为空而静默跳过该行。解决用VS Code或Notepad打开jobs.csv确认编码为UTF-8 with BOM或UTF-8在load_jobs.py中强制指定编码df pd.read_csv(csv_path, encodingutf-8)添加日志self.stdout.write(f成功加载 {len(df)} 行原始数据)确认读取量。4.3 现象Echarts图表显示“数据为空”但API接口如/api/stats/city/返回正常JSON原因dashboard.js中fetch请求跨域被浏览器拦截。Django默认不带CORS头而前端JS在http://127.0.0.1:8000运行API请求发往同域名本不该跨域——但如果你用file://方式双击打开index.html绕过Django服务器或用Live Server插件在http://127.0.0.1:5500启动则构成跨域。解决绝对禁止双击HTML文件运行必须通过python manage.py runserver启动Django服务确认浏览器地址栏是http://127.0.0.1:8000/dashboard/而非file:///...或http://127.0.0.1:5500/...若需前后端分离开发安装django-cors-headers并配置CORS_ALLOWED_ORIGINS [http://localhost:3000]。4.4 现象薪资图表Y轴数值异常如显示“15000000”明显是“15K”被误算为15000000原因normalize_salary()函数中单位识别逻辑缺陷。当salary_str为“15K-25K”时正则匹配到low15,high25但k in salary_str.lower()判断为True于是low_val 15 * 1000 15000high_val 25 * 1000 25000中位数19500正确但如果字符串是“15K/月”正则可能只匹配到low15highNone则进入单值分支15 * 1000 15000也正确。但若字符串是“15000-25000元/月”正则匹配low15000,high25000此时k in ...为False结果就是(1500025000)/2 20000正确。真正翻车的是“年薪30W”——正则没覆盖“W”单位return None该记录被丢弃。但如果你的CSV里有“30W”而函数没处理就会返回NoneDjango ORM存入NULL后续聚合时AVG()忽略NULL看似正常实则数据丢失。解决修改normalize_salary()增加对“W/w”的支持# 在pattern后添加 elif w in salary_str.lower(): # 匹配30W、50w/年 w_match re.search(r(\d\.?\d*)[wW](?:\/年|\/year)?, salary_str) if w_match: return round(float(w_match.group(1)) * 10000)在load_jobs.py中添加数据质量报告统计salary_median IS NULL的记录数self.stdout.write(f薪资无法解析{null_count} 条)。4.5 现象Linux服务器上python manage.py runserver 0.0.0.0:8000启动但外网无法访问原因云服务器如阿里云、腾讯云安全组默认只开放22、80、443端口8000端口被拦截或本地防火墙如ufw阻止。解决云服务器登录控制台找到“安全组”添加入方向规则端口8000协议TCP授权对象0.0.0.0/0或限制为你的IP本地Linuxsudo ufw allow 8000终极验证在服务器上执行curl http://127.0.0.1:8000/api/stats/city/若返回JSON则Django正常问题必在网络层。5. 进阶技巧如何把这套源码变成你自己的招聘数据中台模块现在你已经能跑通、看懂、改基础图表。但真正的价值在于复用——把它从“演示项目”变成你现有系统的“数据看板模块”。下面分享三个经实战验证的落地技巧每一步都附可抄作业的代码。5.1 技巧一无缝集成到现有Django项目非替换而是嵌入你不必推翻重来。假设你已有Django项目my_hr_system想把招聘看板作为其子应用。操作步骤如下复制源码模块到你的项目cd my_hr_system/ # 创建新app保持命名一致避免路径冲突 python manage.py startapp dashboard_embedded # 将本源码包中的 dashboard/, api/, data_processor/ 目录整体复制到 my_hr_system/dashboard_embedded/ # 注意不要复制 manage.py, requirements.txt, recruitment/ 等顶层文件修改dashboard_embedded/apps.py声明应用配置# dashboard_embedded/apps.py from django.apps import AppConfig class DashboardEmbeddedConfig(AppConfig): default_auto_field django.db.models.BigAutoField name dashboard_embedded label dashboard_embedded # 防止与原dashboard冲突在my_hr_system/settings.py中注册应用和静态文件# settings.py INSTALLED_APPS [ # ...你的原有app dashboard_embedded, # 新增 ] # 静态文件路径指向源码包中的static STATICFILES_DIRS [ BASE_DIR / dashboard_embedded/static, # 指向你复制的static目录 # ...其他路径 ]在my_hr_system/urls.py中挂载子路由# urls.py from django.urls import path, include urlpatterns [ # ...你的原有路由 path(recruitment-dashboard/, include(dashboard_embedded.urls)), # 新增 ]关键修改dashboard_embedded/urls.py移除api/前缀冲突# dashboard_embedded/urls.py from django.urls import path, include from . import views urlpatterns [ path(, views.dashboard_view, namedashboard-home), # 主页 # 注意这里不include(api.urls)而是用你主项目的api路由 # 因为你主项目可能已有/api/v1/需统一管理 ]此时访问http://yoursite.com/recruitment-dashboard/即可看到看板且所有静态文件、模板均从你主项目加载。API接口则需你主项目自行实现/api/v1/recruitment-stats/等路由复用本源码的JobStatsViewSet逻辑——这才是企业级集成。5.2 技巧二用Celery异步加载大数据集避免页面卡死当你的招聘数据超10万条load_jobs命令同步执行会阻塞Django主线程用户点击“刷新”后浏览器转圈10秒。解决方案Celery异步任务。安装Celery与Redis推荐Redis作Brokerpip install celery redis # 启动Redisredis-server 默认端口6379在dashboard_embedded/tasks.py中定义异步任务# dashboard_embedded/tasks.py from celery import shared_task from data_processor.management.commands.load_jobs import Command as LoadJobsCommand shared_task def async_load_jobs(csv_path): 异步执行数据导入 cmd LoadJobsCommand() try: cmd.handle(csv_pathcsv_path) return {status: success, message: 数据导入完成} except Exception as e: return {status: error, message: str(e)}在dashboard_embedded/views.py中触发异步任务# views.py from django.http import JsonResponse from .tasks import async_load_jobs def trigger_import(request): if request.method POST: # 从POST获取CSV路径或上传文件后存临时路径 csv_path /tmp/uploaded_jobs.csv task async_load_jobs.delay(csv_path) # 异步执行 return JsonResponse({task_id: task.id, status: started})前端用轮询检查任务状态dashboard.js中// 轮询Celery任务状态 async checkTaskStatus(taskId) { const response await fetch(/api/task-status/${taskId}/); const data await response.json(); if (data.status success) { this.showSuccess(数据导入成功); this.refreshAllCharts(); // 重新加载所有图表 } else if (data.status started) { setTimeout(() this.checkTaskStatus(taskId), 2000); // 2秒后重试 } }这样用户点击“导入”后立即得到响应后台默默处理处理完自动刷新图表——体验丝滑。5.3 技巧三定制化图表主题与导出功能增强源码包中的Echarts是默认主题但企业VI要求蓝白主色、公司Logo水印。static/js/dashboard.js已预留扩展点// static/js/dashboard.js class DashboardController { constructor() { // 加载自定义主题从static/css/theme.json fetch(/static/css/theme.json) .then(r r.json()) .then(theme echarts.registerTheme(custom, theme)); this.initCharts(); } initCharts() { // 使用自定义主题初始化 this.charts.city echarts.init(document.getElementById(chart-city), custom); // ...其他图表同理 } // 增强导出功能PNGPDF需后端配合 exportChartAsPDF(chartId) { // 调用后端API生成PDF使用weasyprint或pdfkit fetch(/api/export/pdf/?chart${chartId}, { method: POST, headers: {X-CSRFToken: getCookie(csrftoken)}, }).then(r r.blob()).then(blob { const url window.URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download 招聘看板-${chartId}.pdf; a.click(); }); } }对应的后端api/views.py中添加导出视图# api/views.py from django.http import HttpResponse from weasyprint import HTML import json def export_pdf_view(request): chart_id request.GET.get(chart) # 根据chart_id获取对应图表数据复用get_city_distribution等方法 data get_chart_data(chart_id) # 你自己的函数 # 渲染PDF模板用weasyprint html_string f htmlbody h1招聘数据看板 - {chart_id}/h1 img srcdata:image/png;base64,{generate_chart_png(chart_id, data)} / p生成时间{timezone.now()}/p /body/html html HTML(stringhtml_string) result html.write_pdf() response HttpResponse(result, content_typeapplication/pdf) response[Content-Disposition] fattachment; filenamerecruitment-{chart_id}.pdf return response从那以后我每次接手新项目只要涉及数据看板第一件事就是把这套DjangoEcharts结构拷贝过来删掉sample_data/替换data_processor/utils.py里的清洗逻辑30分钟内就能跑通客户的真实数据。它不追求技术炫酷但胜在稳定、可读、易改——在交付压力下这比任何“高大上”的架构都珍贵。希望帮到你。本文还有配套的精品资源点击获取