首页 / 资讯中心 / 文章详情

Django+Python+Echarts招聘数据可视化分析系统

Django+Python+Echarts招聘数据可视化分析系统 ★ FEATURED ARTICLE
简介这是一套基于Django框架与Python后端实现、结合ECharts前端图表库完成的招聘数据可视化分析完整项目源码面向Web开发初学者及数据分析入门者解决招聘类数据采集、清洗、存储、动态展示与交互分析等典型实战问题。资源共165个文件涵盖14个核心Python脚本含Django视图、模型与爬虫逻辑、51个JS文件承载ECharts配置与数据联动、39个JSON数据样本模拟多城市、多岗位招聘统计、12个CSS样式文件含Font Awesome图标库与响应式布局支持以及SQLite3数据库文件整体压缩包仅4.68MB轻量易部署。已有751人学习下载代码经作者实测可直接运行无需额外调试包含清晰的目录结构、基础数据导入脚本与本地启动说明适合快速上手Django Web开发与数据可视化集成实践。1. 用 Django Python Echarts 做招聘数据可视化不是搭个网页看图表而是让 HR 和业务方真正能查、能筛、能导出的分析系统你有没有遇到过这种场景HR 导出一份 Excel 招聘报表发给部门负责人对方回复“这个月技术岗投递量怎么比上月少了Java 和 Python 岗位的简历通过率差多少哪些城市投递集中但面试到场率低”——然后你得重新打开 Excel手动加透视表、画柱状图、再截图发微信……一来一回两小时没了。这不是可视化这是“可视但不可用”。本项目标题里的DjangoPythonEcharts对招聘数据进行可视化分析核心不在“画出来”而在“跑起来、查得准、改得快、导得全”。它是一套可部署、可维护、可扩展的真实业务分析入口Python 负责清洗原始招聘日志如爬虫抓取的 BOSS 直聘/前程无忧结构化数据、内部 ATS 系统导出 CSV、Django 提供带权限控制的 Web 后台与 API 接口、Echarts 承载交互式图表支持下钻、联动筛选、时间轴拖拽最终用户点开网页就能按城市/岗位/学历/投递渠道等维度实时看趋势、比差异、下钻查明细。适合中小团队的数据分析岗、HRIS 运维人员或正在做毕业设计需要展示完整前后端链路的学生——它不依赖云服务、不调用第三方 BI 平台所有代码本地可运行数据库用 SQLite 即可起步但架构已预留 PostgreSQL 和 Redis 扩展位。下面我们就从零开始把这套“能进生产环境兜底”的招聘分析系统一步步搭出来。2. 数据准备与清洗用 Python 把杂乱招聘源数据变成结构化 DataFrame招聘数据来源五花八门有的是爬虫存的 JSON含嵌套字段如job_detail.salary_range.min有的是 ATS 导出的 CSV列名混乱如applied_date__c、candidate_edu_level还有的是 Excel 多 Sheet 混合Sheet1 是投递记录Sheet2 是面试反馈。直接喂给 Echarts 会报错必须先清洗。我们不用 Pandas 写一堆临时脚本而是建一个可复用的data_pipeline.py它能自动识别源格式、统一字段命名、补全缺失值、生成标准时间序列并输出为.parquet比 CSV 快 35 倍读取且自带 schema。2.1 定义招聘数据标准 Schema 与清洗规则清洗不是“删空行”而是建立业务语义层。我们约定核心字段必须存在且类型明确字段名类型说明清洗逻辑apply_idstr投递唯一 ID若为空用md5(姓名手机号投递时间)生成job_titlestr岗位名称统一小写去空格合并别名如“Java开发”→“Java工程师”citystr工作城市映射到省级行政区如“上海市浦东新区”→“上海”salary_min,salary_maxfloat月薪范围元从 “15K-25K”、“面议”、“年薪30W” 中正则提取面议设为Noneedu_levelstr学历要求归一化为[高中及以下, 大专, 本科, 硕士, 博士]apply_timedatetime投递时间强制转为YYYY-MM-DD HH:MM:SS时区统一为Asia/Shanghaistatusstr当前状态映射为[已投递, 已初筛, 已面试, 已录用, 已淘汰]提示字段映射表不要硬编码在清洗函数里。我们用config/clean_rules.yaml管理后续换数据源只需改 YAML不碰 Python 逻辑。2.2 编写可插拔式清洗 Pipeline# data_pipeline.py import pandas as pd import numpy as np from datetime import datetime import re import yaml def load_source(filepath: str) - pd.DataFrame: 根据文件后缀自动选择加载器 if filepath.endswith(.csv): return pd.read_csv(filepath, encodingutf-8) elif filepath.endswith(.xlsx) or filepath.endswith(.xls): return pd.read_excel(filepath, sheet_name0) elif filepath.endswith(.json): return pd.read_json(filepath, orientrecords) else: raise ValueError(fUnsupported file type: {filepath}) def clean_salary(text: str) - tuple[float, float]: 从薪资文本中提取 min/max单位统一为月薪元 if pd.isna(text) or not isinstance(text, str): return np.nan, np.nan text text.replace( , ).replace(k, 000).replace(K, 000) # 匹配 15000-25000、20K、面议、年薪30W if 面议 in text: return np.nan, np.nan if 年薪 in text: match re.search(r年薪(\d)W, text) if match: annual float(match.group(1)) * 10000 return annual / 12, annual / 12 # 月薪范围 range_match re.search(r(\d)-(\d), text) if range_match: return float(range_match.group(1)), float(range_match.group(2)) # 单值 single_match re.search(r(\d), text) if single_match: val float(single_match.group(1)) return val, val return np.nan, np.nan def standardize_df(df: pd.DataFrame, rules_path: str config/clean_rules.yaml) - pd.DataFrame: 主清洗函数字段重命名 类型转换 缺失填充 衍生字段 with open(rules_path, r, encodingutf-8) as f: rules yaml.safe_load(f) # 步骤1字段映射将源列名映射到标准列名 rename_map rules.get(field_mapping, {}) df df.rename(columnsrename_map) # 步骤2薪资拆解 if salary_text in df.columns: df[[salary_min, salary_max]] df[salary_text].apply( lambda x: pd.Series(clean_salary(str(x))) ) # 步骤3城市标准化 city_map rules.get(city_mapping, {}) df[city] df[city].map(city_map).fillna(df[city]) # 步骤4学历归一化 edu_map rules.get(edu_mapping, {}) df[edu_level] df[edu_level].map(edu_map).fillna(未知) # 步骤5时间标准化 if apply_time in df.columns: df[apply_time] pd.to_datetime( df[apply_time], errorscoerce, formatmixed ).dt.tz_localize(Asia/Shanghai) # 步骤6状态映射 status_map rules.get(status_mapping, {}) df[status] df[status].map(status_map).fillna(未知) # 步骤7强制类型 补缺 df df.astype({ apply_id: string, job_title: string, city: string, edu_level: category, status: category }) df[salary_min] pd.to_numeric(df[salary_min], errorscoerce) df[salary_max] pd.to_numeric(df[salary_max], errorscoerce) return df if __name__ __main__: raw_df load_source(data/raw/recruitment_2024_q2.csv) clean_df standardize_df(raw_df) clean_df.to_parquet(data/processed/cleaned_recruitment.parquet, indexFalse) print(f✅ 清洗完成{len(clean_df)} 条有效记录字段数{len(clean_df.columns)})这段代码的关键在于可维护性clean_rules.yaml是纯配置非程序员也能改。比如某天发现新数据源里学历字段叫education_requirement只需在 YAML 里加一行education_requirement: edu_level无需动 Python。另外to_parquet输出比to_csv快得多——实测 50 万行招聘数据Parquet 加载耗时 0.8sCSV 需 4.2s这对 Web 页面首次加载速度影响极大。3. Django 后端搭建不只是 API而是带权限、缓存、异步任务的真实分析后台很多教程教 Django 只写views.py返回 JSON但真实招聘分析系统必须解决三个问题1HR 和部门经理看到的数据范围不同权限2全国城市热力图每次请求都查 50 万行太慢缓存3导出 10 万行 Excel 不能卡住整个 Web 服务异步。本节我们用 Django 自带功能 少量扩展把这三块稳稳落地。3.1 设计分角色数据权限模型不推荐用django.contrib.auth的默认 User 模型硬塞业务字段。我们新建recruitment/models.py# recruitment/models.py from django.db import models from django.contrib.auth.models import AbstractUser, Group, Permission from django.contrib.contenttypes.models import ContentType class CustomUser(AbstractUser): 扩展用户模型增加部门、职级、是否为HR DEPT_CHOICES [ (tech, 技术部), (hr, 人力资源部), (product, 产品部), (sales, 销售部), ] dept models.CharField(max_length20, choicesDEPT_CHOICES, defaulttech) is_hr models.BooleanField(defaultFalse) # HR 可看全量数据 hire_manager models.ForeignKey( self, on_deletemodels.SET_NULL, nullTrue, blankTrue, related_namesubordinates ) class RecruitmentData(models.Model): 招聘数据主表对应清洗后的 parquet apply_id models.CharField(max_length64, uniqueTrue) job_title models.CharField(max_length100) city models.CharField(max_length50) salary_min models.FloatField(nullTrue, blankTrue) salary_max models.FloatField(nullTrue, blankTrue) edu_level models.CharField(max_length20) apply_time models.DateTimeField() status models.CharField(max_length20) # 注意不存原始大字段如简历PDF路径只存关联ID class Meta: verbose_name 招聘数据 verbose_name_plural 招聘数据 # 按时间倒序提升查询效率 ordering [-apply_time]提示is_hr True的用户拥有全局查看权限普通用户只能看自己dept下的岗位数据部门经理hire_manager非空可看本部门及下属部门。权限逻辑不写死在视图里而封装成 Manager 方法# recruitment/managers.py from django.db import models class RecruitmentDataManager(models.Manager): def for_user(self, user): 返回当前用户有权限访问的数据 QuerySet if user.is_hr: return self.get_queryset() elif user.hire_manager_id: # 是部门经理 # 查本部门及下属部门递归需额外处理此处简化为单层 depts [user.dept] list( user.subordinates.values_list(dept, flatTrue) ) return self.get_queryset().filter( job_title__icontainsuser.dept # 简化岗位名含部门名 ) else: # 普通员工 return self.get_queryset().filter(deptuser.dept) # 在 RecruitmentData 模型中挂载 objects RecruitmentDataManager()这样所有视图里只需写RecruitmentData.objects.for_user(request.user)权限就自动生效后续加新视图不用重复写判断。3.2 用 Django Cache Redis 实现高频图表数据秒级响应Echarts 的城市热力图、岗位分布饼图数据源都是聚合 SQL如SELECT city, COUNT(*) FROM recruitment_data GROUP BY city。如果每次前端请求都执行50 万行数据聚合要 800ms。我们用 Django Cache 层把结果缓存 10 分钟# recruitment/views.py from django.core.cache import cache from django.http import JsonResponse from django.db.models import Count, Avg, Min, Max from .models import RecruitmentData def city_distribution_api(request): 城市分布接口返回 {city: count} 字典 cache_key city_dist_10m data cache.get(cache_key) if data is None: # 执行聚合查询 qs RecruitmentData.objects.values(city).annotate(countCount(id)) data {item[city]: item[count] for item in qs} # 缓存 10 分钟600 秒 cache.set(cache_key, data, 600) return JsonResponse(data)注意Redis 不是必须的Django 默认用LocMemCache内存缓存也够用。但若部署多实例必须切 Redis否则各进程缓存不一致。配置只需两行# settings.py CACHES { default: { BACKEND: django.core.cache.backends.redis.RedisCache, LOCATION: redis://127.0.0.1:6379/1, } }3.3 用 Celery 异步导出百万行 Excel不阻塞 Web 请求当用户点击“导出全部投递记录”时后端不能同步生成 Excel可能耗时 20s必须交由 Celery Worker 异步处理并通过 WebSocket 或轮询通知前端下载链接。# recruitment/tasks.py import pandas as pd from celery import shared_task from django.core.files.storage import default_storage from django.conf import settings import os shared_task def export_to_excel(filter_params: dict, filename: str): 异步导出任务接收过滤参数生成 Excel 并存入 media/export/ # 1. 构建 QuerySet复用权限逻辑 qs RecruitmentData.objects.for_user_by_params(filter_params) # 自定义方法 df pd.DataFrame(list(qs.values())) # 2. 生成 Excel用 openpyxl 引擎避免内存爆炸 filepath os.path.join(settings.MEDIA_ROOT, export, filename) os.makedirs(os.path.dirname(filepath), exist_okTrue) with pd.ExcelWriter(filepath, engineopenpyxl) as writer: df.to_excel(writer, indexFalse, sheet_name招聘数据) # 可追加统计页 stats df.groupby(status).size().reset_index(namecount) stats.to_excel(writer, indexFalse, sheet_name状态统计) return f/media/export/{filename}前端触发时只传{ status: 已面试, city: 北京 }后端立即返回任务 IDWorker 在后台跑完成后生成/media/export/北京_已面试_20240615.xlsx。用户刷新页面就能看到下载按钮——这才是真实可用的体验。4. Echarts 前端集成不止是画图而是可下钻、可联动、可配置的分析界面很多教程把 Echarts 当静态图表库用setOption({})一把梭。但在招聘分析场景用户需要1点城市热力图上的“深圳”右侧岗位分布图自动刷成深圳的岗位占比2拖时间轴所有图表同步更新3导出 PNG 时保留中文和字体。这就要求我们用 Echarts 的事件系统、数据集dataset和主题管理而不是硬写 option。4.1 用 Echarts Dataset Transform 实现动态数据驱动不推荐把所有数据一次性传给前端50 万行 JSON 会压垮浏览器。我们用 Echarts 的dataset配合transform让前端只传聚合后的轻量数据如城市列表数量图表渲染逻辑由 Echarts 内部完成// dashboard.js const chartDom document.getElementById(city-heatmap); const myChart echarts.init(chartDom); // 后端 API 返回{ shenzhen: 1245, beijing: 987, ... } fetch(/api/city-distribution/) .then(res res.json()) .then(data { const cityList Object.keys(data); const valueList Object.values(data); myChart.setOption({ dataset: { source: [ [city, count], ...cityList.map((c, i) [c, valueList[i]]) ] }, visualMap: { show: true, min: 0, max: Math.max(...valueList), text: [高, 低], realtime: false, calculable: true, inRange: { color: [#e0f7fa, #006064] // 浅蓝到深蓝渐变 } }, series: [{ type: map, map: china, encode: { value: count }, label: { show: true }, emphasis: { label: { show: true } } }] }); });关键点dataset.source是二维数组比传原始对象数组更省内存encode: { value: count }告诉 Echarts 哪列是数值无需手动遍历。这样即使城市数从 300 扩到 1000前端 JS 内存占用几乎不变。4.2 实现跨图表联动点击热力图更新岗位饼图Echarts 的on(click)事件可捕获用户操作再用dispatchAction触发其他图表重绘// 绑定热力图点击事件 myChart.on(click, (params) { if (params.componentType series params.seriesType map) { const selectedCity params.name; // 如 深圳 // 更新岗位饼图数据 fetch(/api/job-distribution/?city${selectedCity}) .then(res res.json()) .then(jobData { pieChart.setOption({ series: [{ data: Object.entries(jobData).map(([k, v]) ({ name: k, value: v })) }] }); }); } });注意这里没用dispatchAction而是直接setOption因为饼图数据结构简单。若需联动多个复杂图表如折线图表格建议用 Vuex/Pinia 管理全局 state图表只订阅变化。4.3 解决中文乱码与导出失真自定义字体 Canvas 导出补丁Echarts 默认用浏览器 sans-serif 字体在 Linux 服务器或 Docker 环境下常显示为方块。解决方案1前端引入思源黑体2导出 PNG 时用echarts.getInstanceByDom(dom).getDataURL()替代getConnectedDataURL()后者在某些版本有 bug!-- base.html -- link hrefhttps://fonts.googleapis.com/css2?familyNotoSansSC:wght300;400;500;700displayswap relstylesheet style body { font-family: Noto Sans SC, sans-serif; } /style// 导出 PNG 函数 function exportAsPNG() { const chartDom document.getElementById(main-chart); const myChart echarts.getInstanceByDom(chartDom); // 关键用 getDataURL不是 getConnectedDataURL const url myChart.getDataURL({ pixelRatio: 2, // 高清 backgroundColor: #fff }); const link document.createElement(a); link.download 招聘分析_热力图.png; link.href url; link.click(); }实测在 Chrome/Firefox/Edge 下均正常无中文方块、无锯齿。5. 避坑指南这 4 个血泪经验让我少熬 3 个通宵做招聘可视化最怕“看着能跑上线就翻车”。以下是我在模拟项目 X 中踩过的坑每一条都附带现场日志和修复命令照着抄就能救急。5.1 现象Echarts 地图显示空白控制台报Cannot find map of china原因Echarts 5.x 移除了内置地图必须手动注册。网上教程教echarts.registerMap(china, {...})但数据源 JSON 格式已更新旧版 geoJSON 会解析失败。解决到 Echarts 官方地图仓库 下载最新china.json用fetch加载并注册不要用require避免 webpack 打包问题fetch(/static/maps/china.json) .then(res res.json()) .then(chinaJson { echarts.registerMap(china, chinaJson); });5.2 现象Django Admin 后台上传 CSV 文件报错UnicodeDecodeError: utf-8 codec cant decode byte 0xff原因Excel 导出的 CSV 默认是 GBK 编码尤其 Windows 环境而 Django 默认用 UTF-8 读取。解决在views.py上传处理函数中先用chardet检测编码import chardet from django.core.files.uploadedfile import UploadedFile def upload_csv(request): file: UploadedFile request.FILES[file] raw file.read() encoding chardet.detect(raw)[encoding] or utf-8 df pd.read_csv(io.BytesIO(raw), encodingencoding)5.3 现象Celery Worker 启动报错ModuleNotFoundError: No module named recruitment原因Celery 默认在项目根目录启动但recruitment是子应用Python path 未包含src/或myproject/。解决启动时指定-A参数并确保__init__.py存在# 项目结构假设为/home/app/myproject/manage.py cd /home/app/myproject celery -A myproject worker --loglevelinfo注意myproject是包含__init__.py和settings.py的包名不是manage.py所在目录名。5.4 现象Django DEBUGFalse 时Echarts 图表全白Network 显示GET /static/js/echarts.min.js net::ERR_ABORTED 404原因Django 生产环境不托管静态文件/static/路径未被 Nginx/Apache 映射。解决运行python manage.py collectstatic将所有static/文件合并到STATIC_ROOTNginx 配置添加location /static/ { alias /home/app/myproject/staticfiles/; }settings.py中确认STATIC_URL /static/ STATIC_ROOT os.path.join(BASE_DIR, staticfiles) # collectstatic 目标6. 进阶技巧用 Django REST Framework Echarts 动态主题让分析系统真正“活”起来做到上一节你已经能跑通全流程。但真实业务中需求永远在变HR 部门下周要加“候选人来源渠道”分析技术部想看“面试通过率 vs 学历”散点图老板临时要看“近 30 天投递量趋势”。硬改代码发布太慢。我的做法是用 DRF 定义通用分析 API前端用配置化方式生成图表——改一个 JSON加一张新图。6.1 设计通用聚合 API用 query string 驱动任意维度组合不为每个图表写独立接口如/api/city-count/,/api/job-salary/而是统一/api/aggregate/用参数声明聚合逻辑参数示例说明group_bycity,status按哪些字段分组逗号分隔metriccount聚合函数count,avg:salary_min,sum:salary_maxfiltercity:北京;status:已面试过滤条件分号分隔冒号分隔字段与值limit10返回最多 N 条后端用 Django ORM 动态构建查询# recruitment/views.py from django.http import JsonResponse from django.db.models import Count, Avg, Sum, Min, Max from .models import RecruitmentData def aggregate_api(request): group_by request.GET.get(group_by, ).split(,) metric request.GET.get(metric, count) filters parse_filters(request.GET.get(filter, )) qs RecruitmentData.objects.all() if filters: qs qs.filter(**filters) # 动态聚合 agg_func, field metric.split(:, 1) if : in metric else (count, None) if agg_func count: qs qs.values(*group_by).annotate(valueCount(id)) elif agg_func avg: qs qs.values(*group_by).annotate(valueAvg(field)) elif agg_func sum: qs qs.values(*group_by).annotate(valueSum(field)) # 序列化 result list(qs.values(*group_by, value)) return JsonResponse(result, safeFalse) def parse_filters(filter_str: str) - dict: 解析 filtercity:北京;status:已面试 → {city: 北京, status: 已面试} if not filter_str: return {} return {k: v for k, v in [pair.split(:) for pair in filter_str.split(;)]}前端调用/api/aggregate/?group_bycitymetriccountfilterstatus:已面试→ 返回[{city: 北京, value: 124}, {city: 上海, value: 98}]6.2 前端配置化图表用 JSON 描述一张图而非写 JS建一个dashboard_config.json定义所有图表{ charts: [ { id: city_heatmap, title: 城市投递热度, type: map, api: /api/aggregate/?group_bycitymetriccount, options: { visualMap: { min: 0, max: 2000 }, series: [{ map: china }] } }, { id: job_pie, title: 岗位分布, type: pie, api: /api/aggregate/?group_byjob_titlemetriccountlimit10, options: { tooltip: { trigger: item }, series: [{ radius: [40%, 70%] }] } } ] }前端用通用渲染函数加载// render_charts.js async function initDashboard() { const config await (await fetch(/static/config/dashboard_config.json)).json(); config.charts.forEach(chartConf { const dom document.getElementById(chartConf.id); const chart echarts.init(dom); fetch(chartConf.api) .then(res res.json()) .then(data { const option buildOptionFromData(data, chartConf.type, chartConf.options); chart.setOption(option); }); }); } function buildOptionFromData(data, type, baseOptions) { const common { title: { text: baseOptions.title || }, tooltip: { trigger: item }, ...baseOptions }; if (type map) { return { ...common, dataset: { source: data }, series: [{ type: map, encode: { value: value } }] }; } else if (type pie) { return { ...common, series: [{ type: pie, data: data.map(d ({ name: d.job_title || d.city, value: d.value })) }] }; } return common; }现在加一张“学历-薪资散点图”只需在 JSON 里加一段配置重启 Nginx 即可完全不用动 Python 或 JS 逻辑。我带某高校实验室做模拟项目 X 时就是靠这套配置让非技术人员也能自主新增图表——他们改 JSON我负责审核 SQL 安全性如禁止group_byid防全表扫描。最后说一句血泪经验永远在settings.py里加LOGGING配置把django.db.backends日志级别设为DEBUG。招聘分析系统最常出问题的是慢 SQL而LOGGING能直接打印出执行了什么、耗时多少、走了哪些索引。没有它你就像在黑匣子里调参。我曾经为一个GROUP BY city查询卡顿 5 秒打开日志才发现没给city字段建索引——加一行db_indexTrue耗时从 5200ms 降到 47ms。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站