ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Django+Echarts招聘数据可视化实战:从数据清洗到仪表盘交付

Django+Echarts招聘数据可视化实战:从数据清洗到仪表盘交付 简介这是一份以Django为Web框架、Python完成数据清洗与统计、Echarts实现前端图表交互的招聘数据可视化分析项目包适合正在学习Web开发与数据分析结合的初中级开发者可作为从后端接口到前端图表的整体参考。压缩包共165个文件其中14个py文件对应Django视图与数据处理逻辑39个json用于配置与数据交换51个js及12个css支撑Echarts图表渲染与页面样式另有html模板和sqlite3数据库整体仅4.65MB轻量易部署。数据源可替换为任意招聘平台导出的Excel或CSV后端经Pandas清洗后通过Django接口输出JSONEcharts再动态渲染图表是一套可迁移的分析模板。项目覆盖数据获取、清洗、分析、接口返回和图表展示全流程已有2954人学习下载manage.py、requirements.txt及templates、static、data等目录结构清晰适合边读边调试可直接运行后查看按地区、岗位、学历等维度生成的柱状图、折线图与饼图便于理解前后端联动方式和可视化分析思路。1. 招聘数据可视化没你想的那么简单Django 只是中场Echarts 才是交付战场拿到一份“DjangoPythonEcharts对招聘数据进行可视化分析.zip”压缩包很多人的第一反应是“又是爬虫图表的老三样”。但真把这个项目跑起来并用于实际分析你会发现中间隔着三道坎招聘数据本身脏得让人头大、Django 的 ORM 查询设计决定接口好不好写、Echarts 的配置项细节直接决定图表能不能看。这个标题本质上是把一条完整的数据链路串起来——Python 负责数据清洗、Django 负责数据建模与接口、Echarts 负责把分析结果变成可交互图表。适合想用真实业务数据练手 Django 全栈的新手也适合需要给团队做内部招聘趋势报表的开发者。本文按我实际做过的方案拆开讲从项目骨架、数据入库、接口设计到 Echarts 渲染最后是 5 个高发踩坑点。2. 技术选型与项目骨架为什么是 Django 配 Echarts而不是 Flask 配 Highcharts2.1 选型理由Django 的 ORM 和 Admin 后台太适合数据类项目了常见做法是 Flask Echarts轻量、上手快但招聘数据可视化分析这个场景下Django 的优势更明显。首先是 ORM招聘数据涉及职位、公司、城市、薪资、学历多个维度Django 的 ORM 可以用模型类直接表达这些实体关系查询聚合时不需要手写 SQL对从 pandas 转过来的 Python 开发者非常友好。其次是自带 Admin 后台数据入库后你想手动改一条错误的职位记录不用写任何代码注册一下模型就能在后台直接编辑这在数据清洗阶段是“后悔药”级别的功能。第三是 Django 的模板系统和静态文件机制虽然现在前后端分离是大流但这类分析型小项目用 Django 模板直接渲染页面、用 Djagno 静态文件机制管理 echarts.min.js 和自定义脚本是成本最低的路径。Echarts 这边没什么悬念。招聘数据分析的图表类型相对固定柱状图看城市职位量、饼图看学历分布、折线图看薪资随工作经验的变化、地图按省份分布。Echarts 对这些图表的支持最成熟文档是全中文社区里现成的配置案例一搜一大把。相比之下 Highcharts 商用要授权Chart.js 功能偏基础如果要画中国地图还得再找扩展库。2.2 环境准备与 Django 项目初始化实际操作从建一个虚拟环境开始。Python 版本建议 3.10 或 3.11Django 用 4.x 都可以不要用太老的版本否则新特性支持和依赖兼容都是坑。打开终端按下面的顺序执行cd ~/projects mkdir recruit_analysis cd recruit_analysis python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install django4.2 pandas django-admin startproject recruit_project . python manage.py startapp analysis这里我把项目拆成两个层次外层recruit_project是 Django 的全局配置目录内层analysis是专门处理招聘数据的业务 App。数据清洗用 pandas是因为招聘数据基本都是 CSV 或 JSON 格式pandas 处理缺失值和类型转换比纯 Python 快得多。安装完成后先做一个基础验证python manage.py migrate python manage.py runserver 0.0.0.0:8000浏览器打开http://127.0.0.1:8000看到 Django 默认的火箭页面说明骨架没问题。migrate 的作用是初始化 Django 自带的 auth、session 等系统表这步不执行的话后续创建超级用户和访问 Admin 后台都会报错。2.3 定义数据模型职位、公司、城市三张核心表招聘数据的实体关系其实很清晰职位属于某个公司职位在某个城市发布学历和经验要求是职位的属性。我一般会建三张表Company存公司信息、City存城市维度表、Position存职位明细其中 Position 通过外键关联 Company 和 City。这样做的好处是后续做聚合分析时不需要在每一行职位数据里重复存公司地址和城市区域模型层面更干净。from django.db import models class Company(models.Model): name models.CharField(max_length128, uniqueTrue) industry models.CharField(max_length64, blankTrue) size models.CharField(max_length32, blankTrue) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return self.name class City(models.Model): name models.CharField(max_length64, uniqueTrue) province models.CharField(max_length32) def __str__(self): return self.name class Position(models.Model): title models.CharField(max_length128) salary_min models.IntegerField(default0) salary_max models.IntegerField(default0) education models.CharField(max_length16, blankTrue) experience models.CharField(max_length16, blankTrue) company models.ForeignKey(Company, on_deletemodels.CASCADE, related_namepositions) city models.ForeignKey(City, on_deletemodels.SET_NULL, nullTrue, related_namepositions) publish_date models.DateField(nullTrue, blankTrue) def __str__(self): return f{self.title} {self.company.name}几个关键字段的取值逻辑说一下。salary_min和salary_max用整型存单位是 K千元这是因为招聘网站上的薪资通常写成 “15-25K·14薪”入库前先清洗成整数后续算平均值和分段统计都很方便。education和experience不建外键直接存字符串原因是我们只做分析展示不做表单级的关联校验字符串在 Echarts 分类轴上的表现更直接。on_deletemodels.SET_NULL这个参数很关键如果某个城市的数据被删职位记录不会级联删除而是把 city 置为空避免误删。3. 招聘数据的清洗入库从脏数据到 Django ORM 批量写入与聚合查询3.1 数据来源与清洗策略CSV 里全是坑招聘数据的原始形态最常见的是两种一种是从招聘网站采集下来的 CSV 或 JSON另一种是别人打包好的数据集。不管哪种直接往 Django 模型里灌数据都会翻车因为源头数据有四个通病城市字段有“北京”、“北京市”、“北京-海淀区”三种写法薪资有“面议”、“15-25K·14薪”、“3.5-5千/月”各种乱格式学历字段有“本科”、“本科及以上”这种没办法直接分类的情况发布时间有的精确到秒、有的只有年份。我的清洗策略分三步走。第一步用 pandas 读进来统一列名第二步写清洗函数处理城市、薪资、学历三个维度的脏值第三步逐行转换后批量写入 Django ORM。城市字段用str.replace去掉“市”字和“-区域”后缀薪资字段用正则提取数字区间面议直接置成 0-0学历字段做个映射表把“本科及以上”归一成“本科”“硕士及以上”归一成“硕士”。3.2 写清洗脚本并用 ORM 批量写入数据库清洗脚本我放在analysis/management/commands/load_recruit_data.py里用 Django 自带的 Command 框架来做这样可以直接python manage.py load_recruit_data data.csv执行不需要额外写入口文件。import re import pandas as pd from django.core.management.base import BaseCommand from analysis.models import Company, City, Position def clean_city(raw): if pd.isna(raw): return None city str(raw).replace(市, ).strip() if - in city: city city.split(-)[0] return city def clean_salary(raw): if pd.isna(raw) or 面议 in str(raw): return 0, 0 nums re.findall(r[\d.], str(raw)) if len(nums) 2: return 0, 0 lo int(float(nums[0]) * 1000) hi int(float(nums[1]) * 1000) return lo, hi def clean_education(raw): edu_map {本科及以上: 本科, 硕士及以上: 硕士, 大专及以上: 大专} if pd.isna(raw): return return edu_map.get(str(raw).strip(), str(raw).strip()) class Command(BaseCommand): help Load recruit CSV into database def add_arguments(self, parser): parser.add_argument(csv_path, typestr) def handle(self, *args, **options): df pd.read_csv(options[csv_path]) positions [] for _, row in df.iterrows(): city_name clean_city(row.get(city)) city_obj City.objects.get_or_create(namecity_name)[0] if city_name else None company_obj, _ Company.objects.get_or_create( namerow[company], defaults{industry: row.get(industry, )} ) lo, hi clean_salary(row.get(salary)) positions.append(Position( titlerow[title], salary_minlo, salary_maxhi, educationclean_education(row.get(education)), experiencerow.get(experience, ), companycompany_obj, citycity_obj, )) Position.objects.bulk_create(positions, batch_size500) self.stdout.write(fLoaded {len(positions)} positions)参数说明get_or_create是 Django ORM 里一个高效但不建议在大批量场景里过度依赖的方法它会先发一条 SELECT 再决定是否 INSERT在逐行循环里性能损耗明显但优点是不会重复创建城市和公司适合这种维度表数据量小的场景。bulk_create是批量写入的关键一次 INSERT 里带上 500 条记录比逐条save()快将近两个数量级。代码里defaults参数的作用是当公司已存在时不覆盖已有字段只补充缺失的行业信息。3.3 聚合查询按城市和薪资维度把数据变成图表需要的结构数据入库后真正给 Echarts 用的不是原始记录而是聚合结果。比如“各城市职位数量”、“不同学历的平均薪资”、“薪资区间分布”这些 Echarts 的柱状图和饼图都只接受categories和values两个数组所以 Django 视图层的职责就是做聚合计算然后输出 JSON。OR马 的annotate和values组合是实现这种分组统计最顺手的工具。from django.db.models import Count, Avg from analysis.models import Position def city_position_stats(): result Position.objects.filter(city__isnullFalse) \ .values(city__name) \ .annotate(totalCount(id), avg_salaryAvg(salary_max)) \ .order_by(-total) return list(result)这段查询的逻辑拆开看values(city__name)指定按城市分组annotate(totalCount(id))给每个组加一个职位数量字段avg_salary用Avg(salary_max)计算该城市职位薪资上限的平均值order_by(-total)按数量倒序排方便前端直接把前 10 个城市渲染成横向柱状图。这里.filter(city__isnullFalse)很重要因为前面清洗时可能有城市缺失的记录不排除的话聚合会出现一行None分组。这个查询返回的是一个 QuerySet里面每个元素是字典json.dumps时需要注意中文字符串默认不会转成 Unicode 转义要在视图层用JsonResponse并设置json_dumps_params{ensure_ascii: False}。4. Django 接口与 Echarts 渲染把数据库里的数字变成浏览器里的仪表盘4.1 设计 JSON 接口一个视图对应一张图表前端图表需要五种数据结构城市-职位数量柱状图、城市-平均薪资折线图、学历分布饼图、经验要求占比饼图、发布时间趋势折线图。接口不要做成了一个“万能 JSON 接口”那样前端拿数据还要自己二次处理我习惯一个视图专注做一个聚合查询。from django.http import JsonResponse from django.db.models import Count, Avg from analysis.models import Position def city_position_api(request): stats Position.objects.filter(city__isnullFalse) \ .values(city__name) \ .annotate(totalCount(id)) \ .order_by(-total)[:10] data { categories: [item[city__name] for item in stats], values: [item[total] for item in stats], } return JsonResponse(data, json_dumps_params{ensure_ascii: False}) def salary_trend_api(request): stats Position.objects.exclude(salary_max0) \ .values(experience) \ .annotate(avg_salaryAvg(salary_max)) \ .order_by(experience) data { categories: [item[experience] for item in stats], values: [round(item[avg_salary] / 1000, 1) for item in stats], } return JsonResponse(data, json_dumps_params{ensure_ascii: False})接口返回的永远只有categories和values两个键这个约定在前端写起来最省心xAxis.data直接用data.categoriesseries.data直接用data.values。薪资接口里的exclude(salary_max0)是把“面议”的记录排除掉否则平均薪资会被拉低到失真。round(..., 1)把实际薪资从元转成 K 并保留一位小数直接给 Echarts 展示更直观。路由注册要在recruit_project/urls.py里完成路径不要用api/这种模糊写法。我一般把路径设计成和分析对象绑定的形式from django.urls import path from analysis import views urlpatterns [ path(api/city-positions/, views.city_position_api), path(api/salary-trend/, views.salary_trend_api), ]4.2 Django 模板里引入 Echarts静态文件路径是第一个大坑前端页面我放在analysis/templates/analysis/dashboard.htmlEcharts 库文件放进analysis/static/analysis/echarts.min.js然后在模板最开头加载静态文件并初始化图表容器。这里百分之八十的新手会翻车在静态文件的路径和加载顺序上。!DOCTYPE html {% load static %} html head meta charsetUTF-8 title招聘数据分析看板/title script src{% static analysis/echarts.min.js %}/script style .chart-box { width: 45%; height: 400px; display: inline-block; } /style /head body div idchart-city classchart-box/div div idchart-salary classchart-box/div script src{% static analysis/dashboard.js %}/script /body /html注意{% static %}模板标签必须在{% load static %}之后才能用这是 C 语言和 Java 转过来的开发者最容易漏掉的一点。Echarts 的script必须放在所有图表容器之前加载否则dashboard.js里调用echarts.init时会报echarts is not defined。图表容器需要显式设置宽度和高度Echarts 如果发现容器长宽都是 0会直接渲染一个空白区域不报任何错误这个坑我最初排查了整整一个下午。4.3 用 fetch 拉取接口数据并渲染图表setOption 的顺序很关键dashboard.js里我写一个通用函数renderChart接受元素 ID、接口地址、图表类型三个参数用fetch拿数据后直接填充到 option 里。招聘数据可视化最常用到的配置项是折线图的xAxis刻度、柱状图的渐变色、饼图的tooltip和legend。async function renderCityChart() { const resp await fetch(/api/city-positions/); const data await resp.json(); const chart echarts.init(document.getElementById(chart-city)); chart.setOption({ title: { text: 各城市职位数量 TOP10, left: center }, tooltip: { trigger: axis }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: category, data: data.categories, axisLabel: { rotate: 30, interval: 0 } }, yAxis: { type: value }, series: [{ name: 职位数, type: bar, data: data.values, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: #83bff6 }, { offset: 1, color: #2f76d1 } ]) } }] }); }这段代码有三个细节值得解释。axisLabel.rotate: 30解决了城市名称过长导致 X 轴刻度文字重叠的问题招聘数据里“呼和浩特”“乌鲁木齐”这种词几乎必重叠不旋转根本没法看。interval: 0强制显示所有刻度默认情况下 Echarts 会自动跳过部分标签视觉效果是城市数据对不上。柱状图的渐变色用的是echarts.graphic.LinearGradient这是 Echarts 5 官方推荐的渐变方案比旧版本的color: new echarts.graphic.LinearGradient写法更标准渐变色让柱子在深色背景或打印场景下轮廓更清晰。折线图参照同样的结构把series.type改成line并加上areaStyle: { opacity: 0.3 }即可。饼图稍微特殊一点series.data需要转成{ name: 类别, value: 数值 }的对象数组在 fetch 拿到数据后做一个map转换再传入。5. 避坑与常见问题排查静态文件、跨域、图表空白和 ORM 误删的 5 个案例5.1 模板里 img 标签加载不出图片static 目录配置的玄学现象Django 模板里写了img src{% static analysis/logo.png %}页面打开图片裂开浏览器控制台报 404。原因settings.py 里的STATICFILES_DIRS没有配置Django 默认只从每个 App 的static子目录加载静态文件但开发模式下如果DEBUGTrue且django.contrib.staticfiles在INSTALLED_APPS里还需要确保模板顶部的{% load static %}没有被遗漏。更隐蔽的情况是analysis/static/analysis/logo.png这个路径中logo.png和logo.PNG的大小写必须和引用完全一致Linux 系统严格区分大小写Windows 上能跑的代码到生产环境全挂。解决在settings.py里增加STATIC_URL /static/确认项目目录下static文件夹存在然后运行python manage.py collectstatic看是否有文件被复制最后浏览器直接访问http://127.0.0.1:8000/static/analysis/logo.png验证。5.2 Echarts 图表渲染空白但不报错现象页面打开div 区域一片灰白打开浏览器控制台没有 JavaScript 报错接口数据也已经成功返回。原因Echarts 初始化时容器元素的宽度或高度为 0。这通常发生在图表容器用display: none或父元素使用 flex 布局且子项没有设置flex-basis的情况下。另外一个高频场景是图表代码在 DOM 完全加载之前执行了echarts.init此时容器还没有实际尺寸。解决先把容器样式改成width: 45%; height: 400px; display: inline-block;并确认页面能看到一个矩形区域再把脚本放在window.onload或DOMContentLoaded回调里执行。观测是否解决的办法在echarts.init之前用console.log(document.getElementById(chart-city).clientWidth)打印宽度如果输出 0说明样式没生效。5.3 接口返回中文变 \uXXXX 乱码现象浏览器访问/api/city-positions/能看到{categories: [\u5317\u4eac, \u4e0a\u6d77]}前端图表上的城市名变成一串反斜杠 Unicode。原因JsonResponse默认使用json.dumps的默认参数ensure_asciiTrue会把非 ASCII 字符转义成\u序列。解决在JsonResponse构造函数里加上json_dumps_params{ensure_ascii: False}同时确认响应头里有Content-Type: application/json; charsetutf-8。Django 3.0 之后的默认响应头设置会用 UTF-8不会出现编码头问题。5.4 Django ORM 删除对象时把关联数据一起删了现象想删掉一个测试城市结果这个城市下所有职位记录都没了数据库里职位表几乎清空。原因模型里Position.city的外键设置成了on_deletemodels.CASCADE删除 City 对象时 ORM 会级联删除所有关联的 Position。在招聘数据这种分析场景职位数据是核心资产城市只是分组维度级联删除是最危险的操作之一。解决把外键改成on_deletemodels.SET_NULL, nullTrue让城市删除后职位保留但city_id置空。如果不希望置空就用PROTECT有职位关联的城市直接禁止删除并抛ProtectedError。修改模型后必须执行python manage.py makemigrations analysis和python manage.py migrate否则数据库表结构和代码不一致。5.5 CSV 数据里日期字段导入失败现象用load_recruit_data命令导入数据报错invalid literal for int()或者validation error指向publish_date字段。原因CSV 里的日期有的是 “2024/05/11”、有的是 “2024-05-11 23:00:22”、有的是 “2024年5月”Django 的DateField期望的是 ISO 格式YYYY-MM-DD直接塞字符串会校验失败。解决在清洗函数里用pd.to_datetime(row[publish_date], errorscoerce)统一转成 pandas 的时间类型再用value.date()提取日期部分传给模型字段。errorscoerce会在转换失败时把值置成NaT入库前检查一下pd.isna()再决定跳过还是置空。raw_date row.get(publish_date) if pd.isna(raw_date): publish_date None else: publish_date pd.to_datetime(raw_date, errorscoerce).date()6. 让看板真正可用定时刷新数据和首屏加载优化分析看板做完只是第一步真正放到团队内部用还需要解决两个问题数据源更新了怎么办、图表打开太快加载不完怎么办。数据更新我采用定时执行清洗入库命令的方式招聘数据每周拉取一次比较合理。服务器上用 crontab 每个周一凌晨三点跑一次0 3 * * 1 cd /home/user/recruit_analysis venv/bin/python manage.py load_recruit_data /data/recruit_latest.csv这里的venv/bin/python是为了确保 Django 命令在正确的 Python 环境里执行直接写python可能会用到系统 Python 导致缺少 Django 包。如果想在 Windows 或 Docker 环境里做定时任务APScheduler 是个更好的方案可以把调度器直接写进 Django 的 App 配置里不需要依赖系统 crontab。首屏优化有个非常高效的技巧Django 接口加缓存。招聘数据更新频率低聚合结果完全可以缓存一小时用 Django 内置的cache_page装饰器就行。from django.views.decorators.cache import cache_page cache_page(60 * 60) def city_position_api(request): # 原来的查询逻辑不用动cache_page的缓存键默认包含完整 URL 和请求头不同的查询参数会缓存成不同的键招聘接口我们没做参数区分所以没问题。加了缓存之后浏览器第二次打开看板时接口响应时间能从 200ms 左右降到 10ms 以内这对用户体验的提升是肉眼可见的。Echarts 这边还有一个实用技巧图表加载时显示 loading 动画避免白屏等待的观感。在fetch数据之前调用chart.showLoading()数据回来后先chart.hideLoading()再setOption。如果同时渲染 4 张图表用Promise.all并发请求接口数据页面整体加载时间能减少大概三分之一。最后说一个个人习惯每次改完数据模型或接口结构第一时间检查一下浏览器 Network 面板里接口返回的 JSON 结构和前端 JS 里data.categories是否能对上。Echarts 报错时的信息都很直白但大多数情况下它根本不报错只是渲染空图表。遇到这种情况先打印数据再检查容器尺寸这两个方向能解决九成以上的图表事故。希望这篇实战记录帮到你尤其是那些和我一样、被“空图表不报错”折磨过的朋友先查宽度再查数据。本文还有配套的精品资源点击获取
返回列表