尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Django招聘数据看板实战:从CSV清洗到ECharts可视化的完整链路

发布时间:2026/9/29 15:50:12

资讯中心
01
ARTICLE

Django招聘数据看板实战:从CSV清洗到ECharts可视化的完整链路

Django招聘数据看板实战:从CSV清洗到ECharts可视化的完整链路
先交代一下背景我一直用 Django 给中小型团队写内部系统去年接了个招聘 SaaS 的外包项目核心需求是把一堆 Boss直聘岗位数据变成可视化的分析后台。项目代号 31467源码我已经整理好放在工程里的source_31467/目录下里面除了完整 Django 工程还有脱敏后的模拟 CSV 数据、清洗脚本和 requirements.txt拿到基本能直接跑。这篇贴不打算从头讲 Django 的概念我只说实际跑通“Boss直聘数据分析”这条链路里最容易被卡住、也最值得复用的几个环节数据清洗、模型设计、聚合查询、图表渲染、部署排错全部按我的真实做法来。如果你也是做求职、招聘、人力资源数据分析相关的东西或者想拿 Django 练手做一个有完整业务闭环的项目这篇文章应该能帮你省下不少试错时间。如果只是想要个花架子大屏那你可能失望因为这套东西的核心是“能落地的分析逻辑”不是只为了好看。1. 一眼看懂项目Django 招聘数据看板拆成了哪三块很多人一听“数据分析项目”第一反应就是拉个 Jupyter Notebook 跑一通 pandas然后画几张图发朋友圈。但放到 Django 里做节奏完全不一样你需要把数据处理、业务模型、前端展示串成一条长期能用的链路。我把它拆成三层每一层单独搞懂整个项目就顺了。1.1 数据清洗层从凌乱的 CSV 到规范表原始数据不是 Excel 导出来就能直接用的。我拿到的第一份 Boss直聘岗位数据是 CSV 文本里面字段混乱、重复项多、薪资写的是“20K-40K”这种文本。如果直接硬灌进数据库后面所有聚合查询都会变成灾难。我的习惯是先用 pandas 做一轮清洗生成一份干净的boss_jobs_clean.csv再导入 Django 的表里。重点就三步import pandas as pd df pd.read_csv(data/raw/boss_jobs.csv, encodingutf-8-sig) df df.drop_duplicates(subset[job_id]).copy() df df.dropna(subset[job_title, company_name, city]) # 把薪资文本解析成数值后续分析才能算平均值和中位数 df[salary_low], df[salary_high] zip(*df[salary_text].map(parse_salary)) df[salary_avg] (df[salary_low] df[salary_high]) / 2有人会问为什么不直接在 Django 的loaddata或管理后台里清洗原因是 pandas 处理重复值、缺失值、正则提取这类操作效率远高于手写 SQL而且不适合把清洗逻辑塞进业务代码里。清洗是分析项目的“上游工厂”上游干净了下游查询才靠谱。1.2 数据模型层用 Django ORM 固化分析主题清洗完之后就不该再用 DataFrame 到处传了我建议直接建 Django 模型。对这类分析项目模型设计别学教科书搞太多范式。公司单独建一张表但岗位表里冗余存公司名和城市文本因为查询时省去大量 JOIN对中小数据量完全值得。# apps/jobs/models.py from django.db import models class Company(models.Model): name models.CharField(max_length128, uniqueTrue) size models.CharField(max_length32, blankTrue) industry models.CharField(max_length64, blankTrue) financing models.CharField(max_length32, blankTrue) def __str__(self): return self.name class JobPosting(models.Model): job_id models.CharField(max_length64, uniqueTrue) title models.CharField(max_length128) company models.ForeignKey(Company, on_deletemodels.CASCADE) city models.CharField(max_length32, db_indexTrue) salary_low models.IntegerField(default0) salary_high models.IntegerField(default0) salary_avg models.FloatField(default0) experience models.CharField(max_length32, blankTrue) education models.CharField(max_length32, blankTrue) description models.TextField(blankTrue) skills models.CharField(max_length255, blankTrue) publish_date models.DateField(nullTrue, blankTrue) class Meta: indexes [ models.Index(fields[city, salary_avg]), models.Index(fields[publish_date]), ]这里有个关键决策薪资存IntegerField而不是CharField。我见过不少人的表里直接把“20K-40K”塞进一个字段后面要算城市平均薪资只能写正则硬解析性能差还容易错。宁可清洗阶段多花十分钟也别让数据库字段偷懒。1.3 可视化层为什么我用 ECharts 而放弃 Matplotlib如果你只做本地探索Matplotlib 和 pandas 自带的绘图完全够用。但只要是“Django 后台看板”这种场景我推荐 ECharts。原因不复杂Django 渲染的是网页Matplotlib 生成的是静态图片每次参数变化都要重新跑 Python 进程生成图片慢且不灵活。ECharts 是纯前端图表库数据通过接口返回 JSON用户可以自己切换维度、缩放、看 tooltip体验完全不是一个级别。项目里我用的 ECharts 5通过 CDN 引入不需要 Node 环境。2. 数据从哪来我的做法和踩过的合规边界这个项目最容易被问到的就是Boss直聘的数据你是怎么拿到的我先把话放前面我不建议任何人一上来就对线上页面硬爬。平台有反爬、有风控、有人机验证更重要的是有用户协议和合规风险。真要做批量采集请先确认对方服务条款、robots 规则和当地法律法规别把技术搞成麻烦。2.1 不推荐硬爬线上页面推荐这三条数据路径我项目里用的数据来源基本是三条自己的账号内导出的投递记录和收藏岗位这类数据最真实但必须脱敏。朋友手动保存的公开岗位信息量不大适合做字段结构测试。我自己造的一份模拟数据字段结构和真实情况一致专门用来跑通分析链路。源码包data/raw/boss_jobs.csv里放的就是脱敏模拟数据岗位公司名都做了替换不会涉及真实隐私。如果你做企业内部项目最好直接让业务方提供数据文件或数据库导出把重点放在分析能力上而不是爬虫。2.2 一份可用的岗位数据至少要有这些字段我整理数据的时候习惯先做字段清单不然清洗很容易漏字段。下面这张表可以作为你的参考起点字段说明示例job_id岗位唯一标识去重用BOSSJOB100234job_title岗位名称Python 后端开发工程师company_name公司名称某科技有限公司city工作城市北京salary_text原文薪资20K-40Kexperience经验要求3-5年education学历要求本科description职位描述负责核心业务系统设计开发...skills技能标签Python, Django, MySQLpublish_date发布日期2025-01-10钱可以后面再加但职业名称、城市、薪资、技能这四个维度是分析的基石缺一个整个看板都会瘸腿。2.3 清洗时最容易被忽视的脏数据细节清洗工作的价值体现在细节上。比如城市字段原始数据里写的是“北京·海淀区”、“上海·浦东新区”直接拿来做聚合会变成几十个桶图根本没法看。我的做法是做一份城市映射表CITY_MAP { 北京·海淀区: 北京, 北京·朝阳区: 北京, 上海·浦东新区: 上海, 上海·徐汇区: 上海, 深圳·南山区: 深圳, # 其他同理 } df[city] df[city_raw].map(CITY_MAP)还有一类脏数据是“面议”。薪资文本里带“面议”的整条记录建议不要直接设为 0否则算平均薪资的时候会把整体拉低。我在清洗脚本里统一把这类薪资字段填成空值后续分析里用filter(salary_low__gt0)排除掉。3. 核心实现拆解models、views、templates 三层怎么配合Django 项目的核心就是 MTV 架构但真正写起来很多人会在模型字段、查询方式、模板数据传递这几个地方纠结。我这里按实际代码一步步拆。3.1 models.py 的字段取舍再聊几句上面的模型示例已经包含常用字段我再补充一个实用点技能字段skills看起来像多对多关系为什么不用单独表因为分析项目的核心是“看趋势”不是“维护数据”。技能单独建表当然规范但查询时要 JOIN 两张表而且清洗阶段提取技能本来就不稳定直接用逗号分隔存一个CharField最简单。等到你确定要做技能反查公司列表这类功能再拆表也不迟。3.2 views.py 里聚合查询的三种写法聚合是这类项目最有价值的地方。比如“哪个城市岗位最多、平均薪资最高”这种问题就是按城市分组算数量、算均值。我常用三种写法按场景选第一种最推荐的 ORM 聚合写法# apps/jobs/views.py import json from django.db.models import Count, Avg from django.http import JsonResponse from apps.jobs.models import JobPosting def api_city_stats(request): rows ( JobPosting.objects .filter(salary_avg__gt0) .values(city) .annotate( totalCount(id), avg_salaryAvg(salary_avg), ) .order_by(-total)[:10] ) return JsonResponse({data: list(rows)}, safeFalse)第二种如果要做技能关键词热度不要只按岗位名分组直接用 Django 的values配合正则函数会比较绕我建议把岗位描述拉到 Python 里用 jieba 统计后面第 4 节再写。第三种复杂 SQL 才用 raw。比如要算薪资中位数、百分位ORM 的表达能力不够我会直接写SELECT原生 SQL。注意raw()返回的RawQuerySet不支持.count()之类的常用 ORM 方法格式化和分页要自己处理能用 ORM 就别轻易上 raw。3.3 模板里的 Ajax 请求和 ECharts 初始化的标准套路写完接口之后前端就很简单了。模板里放一个div然后通过 fetch 请求数据初始化 ECharts。我直接给一份能跑的模板片段!-- templates/dashboard/index.html -- div idcityChart styleheight:400px;/div script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script script fetch(/api/city-stats/) .then(function (resp) { return resp.json(); }) .then(function (res) { var chart echarts.init(document.getElementById(cityChart)); var rows res.data; chart.setOption({ tooltip: {}, xAxis: { type: category, data: rows.map(function (r) { return r.city; }) }, yAxis: { type: value, name: 平均月薪(K) }, series: [{ type: bar, data: rows.map(function (r) { return r.avg_salary.toFixed(1); }) }] }); }); /script这里最容易踩的坑是avg_salary是浮点数前端展示时如果不处理可能出现一长串小数所以我在映射时调用了toFixed(1)。另外 ECharts 初始化一定要等div渲染完成放在页面底部或DOMContentLoaded里都行。4. 三个不能糊弄的分析算法薪资、城市、技能关键词看板不能只是堆图表核心分析逻辑才是项目灵魂。我挑了三个最容易做错、也最有分析价值的点单独展开。4.1 薪资文本解析K、万、面议的归一化处理Boss直聘上的薪资文本常见格式是“20K-40K”但也会有“2-3万”、“15K以上”、“面议”这些变体。解析逻辑必须统一到“千元/月”这个单位否则后面算平均薪资就是灾难。我写的解析函数大概长这样import re def parse_salary(text): if not text or 面议 in text: return None, None text text.strip() # 处理“2-3万” if 万 in text: nums re.findall(r([\d.]), text) if len(nums) 2: return int(float(nums[0]) * 10), int(float(nums[1]) * 10) # 处理“20K-40K” nums re.findall(r(\d), text.replace(K, ).replace(k, )) if len(nums) 2: return int(nums[0]), int(nums[1]) if len(nums) 1: return int(nums[0]), int(nums[0]) return None, None“2-3万”乘 10 变成“20K-30K”是为了和“20K-40K”保持同一个口径。注意正则里我没用K就去掉只是去掉字母之后提取数字如果后面遇到“12.5K-18K”这种带小数点的还需要把re.findall的\d改成[\d.]否则会解析成125和18差距很大。4.2 城市维度下的岗位热度与平均薪资交叉分析城市分析是最直观的分析模块。我在模拟数据上跑出来的结果类似下面这张表城市岗位数量平均月薪(K)北京32831.2上海29630.8深圳21229.5杭州17630.1广州15826.4看这个表光看岗位数量北京第一这符合直觉。但有意思的是杭州岗位量没进前三平均薪资却和上海非常接近。这时候图表用柱状图把“数量”和“平均薪资”两个指标放在同一张图上用双 Y 轴展示就能一眼看出哪些城市“机会多但薪资平”哪些“机会少但薪资高”。这类交叉结论比单纯看岗位数量价值大得多。4.3 从职位描述里提取技能关键词岗位描述文本里藏着真正的技能需求。我用 jieba 分词加停用词过滤再统计词频就可以得到“技能关键词 TopN”。核心代码不复杂import jieba from collections import Counter stopwords {岗位, 职责, 要求, 负责, 以及, 相关, 经验, 我们, 工作, 熟悉, 优先} def extract_skills(description): words [w for w in jieba.cut(description) if w not in stopwords and len(w) 1] return [word for word in words if word.isalnum() or word.isalpha()]跑完全部岗位描述后我拿词频排了序Top 技能基本集中在Python、Django、MySQL、数据分析、Hadoop、Spark这些词上。这里有个经验停用词表要自己维护像“熟悉”这种高频但没业务含义的词不去掉排行榜前几名全是废词。你可以把分词结果存到单独的SkillRank表避免每次刷新页面都重新分词。5. 跑起来和部署时容易踩的六个坑这个项目整体不算复杂但我在本地跑和部署到云服务器的过程中还是踩了几个坑有些是版本问题有些是 Django 项目特有的配置问题写出来给你避避雷。5.1 Python 和 Django 版本错位我开发环境用的是 Python 3.10 Django 4.2。如果你用 Python 3.6 跑 Django 4.x会直接报语法错误反过来用新 Python 跑老 Django也容易出现AttributeError。项目根目录的 requirements.txt 是经过验证的版本组合Django4.2.7 pandas2.0.3 openpyxl3.1.2 jieba0.42.1建议先用python -m venv venv建虚拟环境再装依赖别图省事直接装全局。装完用python manage.py runserver起服务遇到任何ImproperlyConfigured的提示基本都是版本问题。5.2 SQLite 换成 MySQL 后的中文排序问题项目开发时我用 SQLite部署到服务器后切成 MySQL。第一次切完发现中文排序和查询结果不对问题出在字符集。Django 的数据库配置里必须显式指定 utf8mb4DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: boss_analysis, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }另外如果 CSV 清洗阶段已经导过一次数据切换数据库后要重新执行makemigrations和migrate再跑导入脚本别直接复制旧的 SQLite 文件。5.3 Django 静态文件 404看板页面用了 ECharts CDN本机没问题但部署到服务器后内网环境可能屏蔽外网 CDN这时候需要把 ECharts 的 JS 文件下载下来放进static/目录。重点是DEBUGFalse后Django 默认不托管静态文件必须执行python manage.py collectstatic再把whitenoise中间件加进MIDDLEWARE配置里不然静态文件永远 404。这个坑几乎每个 Django 新手都会遇到。5.4 CSV 读取时 BOM 和编码问题Windows 上用 Excel 另存的 CSV默认可能是 GBK 编码也可能是带 BOM 的 UTF-8。用 pandas 读取时不能硬写encodingutf-8否则第一列名会带上\ufeff直接把你坑哭。我的读取逻辑是优先尝试utf-8-sig失败后回退到gbktry: df pd.read_csv(path, encodingutf-8-sig) except UnicodeDecodeError: df pd.read_csv(path, encodinggbk)utf-8-sig会自动处理开头的 BOM 字符这也是我在项目里一直坚持用它的原因。5.5 数据量大以后的分页和查询优化模拟数据只有几千行跑啥都快。但真实场景下岗位数据可能几十万行这时候有两个优化必须做。一是列表页用 DjangoPaginator做分页别一次性把数据全渲染到模板二是查询对象时加select_related(company)避免每次访问公司信息都发一条额外 SQL。如果你按城市过滤模型里已经给city和salary_avg加了联合索引按publish_date做时间筛选也有索引兜底这些都是从模型设计阶段就该考虑好的。5.6 源码目录结构说明与启动命令最后说一下源码包source_31467的目录方便你拿到手后快速定位source_31467/ ├── manage.py ├── requirements.txt ├── config/ │ └── settings.py ├── apps/ │ ├── jobs/ │ │ ├── models.py │ │ ├── views.py │ │ ├── urls.py │ │ └── management/ │ │ └── commands/ │ │ └── import_data.py │ └── dashboard/ │ └── views.py ├── data/ │ ├── raw/ │ │ └── boss_jobs.csv │ └── clean/ │ └── boss_jobs_clean.csv ├── scripts/ │ └── clean_data.py ├── static/ │ └── echarts.min.js └── templates/ └── dashboard/ └── index.html启动顺序是先pip install -r requirements.txt然后执行python manage.py import_data导入数据再执行python manage.py runserver浏览器打开http://127.0.0.1:8000/dashboard/就能看到看板。我自己在实际操作中还有个习惯就是每次改动模型字段后先跑一遍清洗脚本重新生成 CSV再执行导入命令这样可以避免脏数据残留在库里导致图表异常。项目本身不复杂但这条“CSV 清洗 → Django ORM → 接口统计 → ECharts 可视化”的链路换成任何招聘数据都能复用希望这套东西能帮你省点时间。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。