ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Django+Python+Echarts招聘数据可视化:从CSV清洗到交互大屏

Django+Python+Echarts招聘数据可视化:从CSV清洗到交互大屏 简介这是一套面向Python Web开发初学者与数据分析入门者的招聘数据可视化实战源码基于Django搭建后端服务配合Python完成数据清洗与统计再通过Echarts在前端呈现图表帮助读者理解从数据到页面的完整链路。压缩包共165个文件约4.68MB其中51个js与39个json承担前端交互与数据配置14个py与12个pyc构成Django业务逻辑另有12个css、5个html及字体图标资源负责页面样式并附带sqlite3数据库与Pipfile依赖清单目录结构清晰便于按模块阅读。资源已有751人学习下载代码完整、下载即可运行适合作为课程设计、毕业设计或自学练手项目读者可据此掌握Django视图与模板渲染、Echarts图表配置、招聘数据字段处理与统计维度设计等关键技能并在此基础上替换数据集、扩展分析指标快速搭建属于自己的可视化分析系统。1. 招聘数据可视化分析系统从原始 CSV 到可交互大屏的完整落地路径招聘网站每天产生海量岗位信息薪资、学历、城市、经验要求散落在不同字段里。想快速看清“哪个城市薪资最高”“什么学历要求占比最大”“哪些技能被反复提及”靠 Excel 手工透视表效率太低而且每次数据更新都要重来一遍。Django Python Echarts 这套组合解决的正是这个问题Python 负责数据清洗和统计计算Django 提供 Web 框架和接口Echarts 在前端把结果渲染成折线图、柱状图、饼图、词云图和中国地图。这套方案适合有 Python 基础、想做一个完整数据可视化项目的开发者也适合需要快速搭建内部招聘数据看板的团队。源码结构清晰照着本文的步骤走本地跑通不是难事。2. 技术选型与数据准备为什么是 Django 而不是 Flask2.1 Django 的 MTV 模式在可视化项目里的实际作用Django 的 MTV 模式Model-Template-View在这个项目里分工很明确。Model 层定义招聘数据的表结构比如岗位名称、公司名称、薪资范围、学历要求、城市、经验年限等字段。Template 层负责页面骨架View 层处理业务逻辑——从数据库读取数据、做聚合统计、返回 JSON 给前端。相比 FlaskDjango 自带 ORM、Admin 后台和模板系统不需要额外拼装组件。对于招聘数据这种字段多、需要频繁查询和统计的场景Django ORM 的聚合函数annotate、aggregate能省掉大量手写 SQL 的工作。常见做法是先用 Django 的inspectdb从现有数据库反向生成 Model或者直接根据 CSV 字段手动定义。我一般会先跑一遍python manage.py startapp jobs创建应用然后在models.py里把字段类型定清楚。薪资字段建议用CharField存原始文本如“15-25K”另加两个IntegerField存解析后的最低和最高薪资方便后续做数值统计。2.2 用 Pandas 清洗招聘 CSV 的四个关键步骤原始招聘数据通常来自爬虫或公开数据集字段里夹杂着“薪资面议”“经验不限”“本科及以上”这类非结构化文本。直接入库会导致统计结果失真。下面这段清洗脚本是我常用的模板import pandas as pd import re # 读取原始 CSV注意编码问题gbk 和 utf-8 都试一下 df pd.read_csv(jobs_raw.csv, encodingutf-8) # 步骤1解析薪资范围提取最低和最高薪资单位K def parse_salary(s): if pd.isna(s) or 面议 in str(s): return pd.Series([None, None]) nums re.findall(r(\d\.?\d*), str(s)) if len(nums) 2: return pd.Series([float(nums[0]), float(nums[1])]) elif len(nums) 1: return pd.Series([float(nums[0]), float(nums[0])]) return pd.Series([None, None]) df[[salary_min, salary_max]] df[salary].apply(parse_salary) # 步骤2统一学历要求把“本科及以上”“统招本科”归为“本科” def normalize_education(s): s str(s) if 不限 in s or 无 in s: return 不限 for level in [博士, 硕士, 本科, 大专, 中专, 高中]: if level in s: return level return 其他 df[education_clean] df[education].apply(normalize_education) # 步骤3提取城市去掉“·”后面的区域信息 df[city_clean] df[city].astype(str).str.split(·).str[0] # 步骤4丢弃薪资和城市缺失的记录避免统计偏差 df df.dropna(subset[salary_min, salary_max, city_clean]) df.to_csv(jobs_clean.csv, indexFalse, encodingutf-8-sig) print(f清洗完成剩余 {len(df)} 条记录)这段代码的逻辑说明parse_salary用正则提取数字处理“15-25K”“20K以上”“15-25K·13薪”等格式normalize_education把各种变体归到统一层级城市字段按“·”分割取第一部分。参数方面encoding建议先用chardet检测原始文件编码避免乱码。清洗后的 CSV 用utf-8-sig保存Excel 打开不会乱码。2.3 把清洗后的数据导入 Django 数据库清洗完的数据需要进 Django 的数据库。两种方式一是用python manage.py loaddata加载 fixtures二是写一个自定义 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 handle(self, *args, **options): with open(jobs_clean.csv, r, encodingutf-8-sig) as f: reader csv.DictReader(f) batch [] for i, row in enumerate(reader): batch.append(Job( titlerow[title], companyrow[company], salary_minrow[salary_min] or None, salary_maxrow[salary_max] or None, cityrow[city_clean], educationrow[education_clean], experiencerow[experience], )) if len(batch) 500: Job.objects.bulk_create(batch) batch [] self.stdout.write(f已导入 {i1} 条) if batch: Job.objects.bulk_create(batch) self.stdout.write(self.style.SUCCESS(导入完成))bulk_create比逐条save()快一个数量级500 条一批是经验值太大容易撑爆内存。执行python manage.py import_jobs即可。注意 Model 里字段的nullTrue和blankTrue要设对否则空值会报错。3. Django 接口层把统计结果喂给 Echarts3.1 用 ORM 聚合函数算出 Echarts 需要的四种数据格式Echarts 折线图需要 x 轴和 y 轴数组饼图需要{name, value}列表地图需要城市名和对应数值词云需要{name, value}且 value 决定字号。这些数据都在 Django 的 View 里用 ORM 算出来返回JsonResponse。以“各城市平均薪资”为例from django.db.models import Avg, Count from django.http import JsonResponse from jobs.models import Job def city_salary(request): # 按城市分组计算平均薪资按薪资降序排列 data (Job.objects .values(city) .annotate(avg_salaryAvg(salary_min)) .order_by(-avg_salary)[:20]) # 取前20个城市 result { cities: [item[city] for item in data], salaries: [round(item[avg_salary], 1) for item in data] } return JsonResponse(result)values(city)按城市分组annotate计算每组的平均薪资order_by排序后取前 20。返回的 JSON 结构直接对应 Echarts 的xAxis.data和series.data。学历分布用Count聚合词云用values(skill).annotate(countCount(id))逻辑类似。3.2 配置 URL 路由和跨域让前端能拿到数据Django 的 URL 配置在urls.py里from django.urls import path from jobs import views urlpatterns [ path(api/city_salary/, views.city_salary), path(api/education_dist/, views.education_dist), path(api/skill_wordcloud/, views.skill_wordcloud), path(api/salary_range/, views.salary_range), ]如果前端页面和 Django 不在同一个端口需要处理跨域。开发阶段用django-cors-headerspip install django-cors-headers在settings.py的INSTALLED_APPS加corsheadersMIDDLEWARE加corsheaders.middleware.CorsMiddleware然后设CORS_ALLOW_ALL_ORIGINS True仅开发环境。生产环境要指定具体域名别偷懒。3.3 用 Echarts 折线图展示薪资趋势的完整前端代码前端页面用 Echarts 的 CDN 引入一个折线图的完整配置如下// 初始化图表实例 var chart echarts.init(document.getElementById(salaryChart)); // 从 Django 接口拿数据 fetch(/api/city_salary/) .then(response response.json()) .then(data { var option { title: { text: 各城市平均薪资排名 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.cities, axisLabel: { rotate: 45 } // 城市名太长时倾斜显示 }, yAxis: { type: value, name: 薪资(K) }, series: [{ data: data.salaries, type: bar, itemStyle: { color: #5470c6 } }] }; chart.setOption(option); }); // 窗口大小变化时自适应 window.addEventListener(resize, function() { chart.resize(); });axisLabel.rotate解决 x 轴城市名重叠问题tooltip.trigger: axis让鼠标悬停显示数值。Echarts 折线图 x 轴刻度用axisLabel.interval: 0强制显示所有标签数据多时配合rotate使用。饼图把series.type改成pie数据格式换成[{name: 本科, value: 120}, ...]。4. 可视化图表进阶地图、词云和动态交互4.1 Echarts 中国地图的引入与数据绑定中国地图需要额外引入地图 JSON 文件。Echarts 5 之后地图数据不再内置需要从官方或第三方获取china.json用echarts.registerMap注册fetch(/static/map/china.json) .then(res res.json()) .then(mapData { echarts.registerMap(china, mapData); var chart echarts.init(document.getElementById(mapChart)); fetch(/api/city_salary/) .then(res res.json()) .then(data { var mapOption { title: { text: 招聘薪资地域分布 }, tooltip: { trigger: item }, visualMap: { min: 0, max: 30, text: [高, 低], calculable: true, inRange: { color: [#e0f3f8, #4575b4] } }, series: [{ type: map, map: china, data: data.cities.map((city, i) ({ name: city, value: data.salaries[i] })) }] }; chart.setOption(mapOption); }); });visualMap控制颜色渐变calculable: true允许拖动筛选。注意城市名要和地图 JSON 里的名称完全一致比如“北京”不能写成“北京市”否则数据绑不上。这是最常见的翻车点。4.2 词云图从技能关键词到字号映射词云图需要echarts-wordcloud扩展库在 Echarts 之后引入script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script script srchttps://cdn.jsdelivr.net/npm/echarts-wordcloud2/dist/echarts-wordcloud.min.js/script后端接口返回[{name: Python, value: 320}, {name: Java, value: 280}, ...]前端配置var wordcloudOption { series: [{ type: wordCloud, shape: circle, sizeRange: [12, 60], // 字号范围 rotationRange: [-45, 45], textStyle: { color: function() { return rgb( [ Math.round(Math.random() * 160), Math.round(Math.random() * 160), Math.round(Math.random() * 160) ].join(,) ); } }, data: wordcloudData }] };sizeRange决定最小和最大字号rotationRange控制旋转角度。数据里 value 最大的词字号最大视觉上最突出。如果词云不显示先检查echarts-wordcloud是否在echarts之后加载。4.3 tooltip 自动换行和柱状图自定义图片的配置技巧Echarts tooltip 内容太长会溢出屏幕用extraCssText限制宽度并自动换行tooltip: { trigger: axis, extraCssText: max-width: 300px; white-space: normal; word-break: break-all; }柱状图柱子用自定义图片显示通过itemStyle.decal或symbol实现。更常见的做法是用pictorialBarseries: [{ type: pictorialBar, symbol: image:///static/img/bar-bg.png, symbolSize: [30, 100], data: data.salaries }]symbol的image://后跟图片路径。注意图片尺寸要和symbolSize匹配否则拉伸变形。这个配置在 Echarts 社区里有大量示例搜“echarts 柱状图自定义图片”能找到现成模板。5. 避坑与排查部署和调试中的五个血泪教训5.1 静态文件 404Django 的 static 配置为什么总是不生效现象Echarts 的 JS 文件、地图 JSON、CSS 全部 404页面空白。原因Django 开发模式下DEBUGTrue时静态文件由django.contrib.staticfiles自动处理但STATIC_URL和STATICFILES_DIRS没配对。解决在settings.py里确认STATIC_URL /static/STATICFILES_DIRS [BASE_DIR / static]模板里用{% load static %}加{% static js/echarts.min.js %}。生产环境用python manage.py collectstatic收集到STATIC_ROOT交给 Nginx 托管。5.2 数据库查询太慢N1 问题和缺失索引现象页面加载超过 10 秒接口响应慢。原因循环里逐条查询关联表或者城市、学历字段没建索引。解决用select_related和prefetch_related减少查询次数在 Model 的city、education、salary_min字段上加db_indexTrue聚合查询用values().annotate()而不是 Python 循环。数据量超过 10 万条时考虑加缓存层把聚合结果存 Redis设 5 分钟过期。5.3 Echarts 图表不显示容器高度为 0 的经典问题现象接口返回数据正常但图表区域一片空白。原因Echarts 初始化时容器div的高度是 0或者echarts.init在 DOM 渲染完成前执行。解决给容器设固定高度styleheight: 400px;JS 放在DOMContentLoaded事件里或页面底部。如果容器在 Tab 切换里切换后要调chart.resize()。5.4 中文乱码CSV 编码和数据库字符集的双重坑现象导入的数据里中文变成问号或乱码。原因CSV 文件编码是 GBKPython 用 UTF-8 读或者 MySQL 数据库字符集不是utf8mb4。解决读 CSV 时用chardet.detect()检测编码或直接指定encodinggbkMySQL 建库时用CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ciDjango 的DATABASES配置里加OPTIONS: {charset: utf8mb4}。5.5 部署到 Windows 10 后接口 500Waitress 和 Nginx 的配置顺序现象开发模式正常用 Waitress Nginx 部署后接口报 500。原因Nginx 反向代理没转发Host头或者 Waitress 的--url-prefix和 Django 的FORCE_SCRIPT_NAME不匹配。解决Nginx 配置里加proxy_set_header Host $host;和proxy_set_header X-Real-IP $remote_addr;Waitress 启动命令用waitress-serve --listen0.0.0.0:8000 project.wsgi:applicationDjango 的ALLOWED_HOSTS加上服务器 IP 或域名。静态文件交给 Nginx 的location /static/处理别让 Waitress 扛。6. 进阶技巧用 Django Admin 做数据校验和定时刷新6.1 在 Admin 里加自定义过滤器快速定位异常数据Django Admin 默认能增删改查但招聘数据里薪资为 0、城市为空这类异常记录靠肉眼翻页找太慢。加一个自定义过滤器from django.contrib import admin from django.utils.translation import gettext_lazy as _ from jobs.models import Job class SalaryRangeFilter(admin.SimpleListFilter): title _(薪资范围) parameter_name salary_range def lookups(self, request, model_admin): return ( (zero, _(薪资为0)), (high, _(薪资大于50K)), (normal, _(正常范围)), ) def queryset(self, request, queryset): if self.value() zero: return queryset.filter(salary_min0) if self.value() high: return queryset.filter(salary_min__gt50) if self.value() normal: return queryset.filter(salary_min__gt0, salary_min__lte50) admin.register(Job) class JobAdmin(admin.ModelAdmin): list_display (title, company, city, salary_min, salary_max, education) list_filter (SalaryRangeFilter, city, education) search_fields (title, company)list_filter里挂上自定义过滤器后Admin 右侧会出现筛选面板点一下就能筛出异常记录。search_fields支持按岗位和公司名搜索。这个技巧在数据清洗阶段特别省时间。6.2 用 management command 加定时任务刷新统计数据Echarts 的数据如果每次请求都实时聚合数据量大时接口会变慢。我一般会写一个定时任务每 10 分钟把聚合结果算好存到一张统计表里接口直接读统计表# jobs/management/commands/refresh_stats.py from django.core.management.base import BaseCommand from django.db.models import Avg, Count from jobs.models import Job, CitySalaryStat class Command(BaseCommand): help 刷新城市薪资统计表 def handle(self, *args, **options): CitySalaryStat.objects.all().delete() stats (Job.objects .values(city) .annotate(avg_salaryAvg(salary_min), job_countCount(id)) .order_by(-avg_salary)) CitySalaryStat.objects.bulk_create([ CitySalaryStat(citys[city], avg_salarys[avg_salary], job_counts[job_count]) for s in stats ]) self.stdout.write(self.style.SUCCESS(f刷新了 {len(stats)} 个城市的统计))Windows 下用“任务计划程序”定时执行python manage.py refresh_statsLinux 下用 crontab。接口层改成从CitySalaryStat读数据响应时间能从秒级降到毫秒级。6.3 验证数据准确性的三个交叉检查方法做完可视化怎么确认图表没骗人我习惯做三个检查第一用 Pandas 直接对清洗后的 CSV 做describe()看薪资的均值、中位数、最大最小值和 Django 接口返回的聚合结果对比偏差超过 5% 就要查原因。第二随机抽 10 条记录手工核对原始 CSV 和数据库里的字段是否一致重点看薪资解析和城市提取有没有漏。第三把 Echarts 图表的数值和 Admin 后台的筛选结果对照比如地图上“北京平均薪资 25K”就在 Admin 里筛城市为北京看平均值是不是 25K。这三个检查做完基本能排除数据层面的低级错误。这套 Django Python Echarts 的招聘数据可视化方案从清洗到入库到接口到前端每个环节都有现成的代码可以抄。我踩过的坑主要集中在静态文件配置、Echarts 容器高度和中文编码上希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进