☰
基于Python招聘数据分析可视化系统:Django+ECharts全链路实战
2026/10/7 16:38:48 网站建设 项目流程

简介:这是一套面向计算机相关专业学生与项目实战学习者的毕业设计完整方案,主题为基于Python与Django的招聘数据分析可视化系统,适合用作毕设、课程设计或期末大作业,也可作为数据分析与Web开发方向的练手项目。压缩包共59个文件,约10.33MB,以py源码、pyc编译文件、xml配置、png与jpg图表截图、js脚本、sql数据库脚本及xls数据表为主,另含pptx演示文稿、md说明文档与html页面,覆盖前后端与数据库整套内容。资源中提供两份招聘数据SQL脚本,其中一份数据量更充足,便于直接导入分析;同时包含爬虫与数据分析模块,可支撑从数据采集、清洗到可视化展示的完整流程。目前已有552人学习下载,评审分98分,参考价值较高,能帮助读者快速理解项目结构、复用代码并完成答辩准备。

1. 从一份招聘数据到能跑起来的 Django 可视化系统

招聘网站每天产生海量岗位信息,把这些数据抓下来、清洗干净、存进数据库,再用图表把薪资分布、技能需求、城市热度呈现出来,就是「基于 Python 招聘数据分析可视化系统」要干的事。它适合正在做计算机毕业设计、想找一个数据 + Web 全链路练手项目的同学,也适合已经会写 Python 脚本、但没完整搭过一个 Django 应用的人。整套东西的技术栈很清晰:Python 负责采集与清洗,Django 负责 Web 层和 ORM,ECharts 或 Pyecharts 负责前端渲染,MySQL 或 SQLite 负责存储。下面按「数据怎么来 → 后端怎么建 → 图表怎么出 → 坑在哪」的顺序,把每一步落到可复现的命令和代码上。

2. 招聘数据从哪来:采集、清洗与入库的完整链路

2.1 采集方案选型:静态页面、接口还是模拟数据

做招聘数据分析,第一步永远是数据源。常见做法有三种,选哪种直接决定后面工作量。

第一种是直接请求招聘网站的搜索接口。很多招聘站点的列表页是前端异步渲染的,翻页时背后有一个返回 JSON 的接口,用浏览器开发者工具的 Network 面板就能看到请求地址和参数。这种方式拿到的数据字段规整,省去大量解析 HTML 的功夫。但要注意请求频率,加time.sleep控制节奏,否则容易被限流。

第二种是解析静态 HTML。适合那些服务端渲染的页面,用requests拿到 HTML 后用BeautifulSoup或lxml提取。缺点是页面结构一变,选择器就失效,维护成本高。

第三种是准备一份离线数据集。毕业设计场景下,如果采集不稳定,完全可以用一份已经整理好的 CSV 作为数据源,把精力放在分析和可视化上。这不是偷懒,而是把风险控制在可交付范围内。

我一般会先写采集脚本跑一批真实数据,同时保留一份 CSV 兜底。这样即使演示当天网络出问题,系统照样能跑。

2.2 用 requests + BeautifulSoup 抓取岗位列表

下面是一个最小可用的采集脚本,抓取岗位名称、公司、城市、薪资、学历要求五个字段。

import requests from bs4 import BeautifulSoup import csv import time import random HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" } def fetch_page(keyword, page): """抓取单页岗位列表,返回解析后的字典列表""" url = "https://example-job-site.com/search" params = {"keyword": keyword, "page": page} resp = requests.get(url, headers=HEADERS, params=params, timeout=10) resp.raise_for_status() soup = BeautifulSoup(resp.text, "lxml") jobs = [] for item in soup.select(".job-item"): jobs.append({ "title": item.select_one(".job-name").get_text(strip=True), "company": item.select_one(".company-name").get_text(strip=True), "city": item.select_one(".job-area").get_text(strip=True), "salary": item.select_one(".salary").get_text(strip=True), "education": item.select_one(".edu-level").get_text(strip=True) if item.select_one(".edu-level") else "不限", }) return jobs def save_to_csv(rows, path="jobs_raw.csv"): with open(path, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["title", "company", "city", "salary", "education"]) writer.writeheader() writer.writerows(rows) if __name__ == "__main__": all_jobs = [] for p in range(1, 11): # 抓前 10 页 all_jobs.extend(fetch_page("Python", p)) time.sleep(random.uniform(1.5, 3.0)) # 随机间隔,降低被封风险 save_to_csv(all_jobs) print(f"共采集 {len(all_jobs)} 条")

逻辑说明:fetch_page负责单页请求和 DOM 解析,save_to_csv负责落盘。encoding="utf-8-sig"是为了 Excel 打开 CSV 不乱码,这个细节很多人第一次导出时会踩。

参数说明:timeout=10防止请求挂死;time.sleep的区间建议 1.5 到 3 秒,太短容易触发风控,太长采集效率低。page范围根据目标站点实际页数调整,不要写死太大。

提示:选择器.job-item、.job-name这些是示例,实际用的时候必须换成目标页面真实的 class 名,否则拿到空列表。

2.3 薪资字段清洗:把「15-25K·13薪」变成可计算的数字

原始薪资是字符串,直接存进数据库没法做统计。需要拆成最低薪资、最高薪资两个数值字段,单位统一成「千/月」。

import re def parse_salary(raw): """把 '15-25K·13薪' 解析为 (15.0, 25.0),解析失败返回 (None, None)""" if not raw or "面议" in raw: return None, None match = re.search(r"(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\s*[Kk千]", raw) if match: return float(match.group(1)), float(match.group(2)) # 处理 '20K以上' 这类单值 single = re.search(r"(\d+(?:\.\d+)?)\s*[Kk千]", raw) if single: v = float(single.group(1)) return v, v return None, None

逻辑说明:先用正则匹配「数字-数字K」的区间格式,匹配不到再尝试单值格式,都失败就返回空。这样后续统计时可以直接过滤掉None的记录,不会因为脏数据报错。

参数说明:正则里的[Kk千]覆盖了常见的单位写法。如果数据源里出现「万/月」,需要额外加一条分支把万换算成千。清洗完的字段建议单独存一张表或加两列,不要覆盖原始字符串,方便回溯。

2.4 数据入库前的去重与字段规范化

采集回来的数据经常有重复岗位,同一家公司同一个职位可能出现在多页。入库前做一次去重,用「公司名 + 岗位名 + 城市」作为联合唯一键。

import pandas as pd df = pd.read_csv("jobs_raw.csv") df["min_salary"], df["max_salary"] = zip(*df["salary"].apply(parse_salary)) df = df.drop_duplicates(subset=["company", "title", "city"]) df = df.dropna(subset=["min_salary"]) # 丢掉薪资解析失败的 df["city"] = df["city"].str.replace("·", "").str.strip() df.to_csv("jobs_clean.csv", index=False, encoding="utf-8-sig") print(f"清洗后剩余 {len(df)} 条")

逻辑说明:drop_duplicates按联合键去重,dropna过滤掉薪资无法解析的记录,城市字段去掉中间点并去空格,保证后续分组统计时同一个城市不会因为格式差异被拆成两组。

参数说明:subset里的字段根据实际数据调整,如果公司名有大小写差异,可以先统一转小写再比对。清洗后的 CSV 就是导入 Django 的数据源。

3. Django 后端搭建:模型、导入命令与查询接口

3.1 项目初始化与 app 创建

先确认 Python 和 Django 装好。这一步新手最容易卡在环境上,命令给全。

python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install django pandas requests beautifulsoup4 lxml django-admin startproject job_analysis cd job_analysis python manage.py startapp jobs

逻辑说明:虚拟环境隔离依赖,避免和系统 Python 冲突。startproject建项目骨架,startapp建业务模块。装pandas是因为清洗脚本要用,lxml是 BeautifulSoup 的高效解析器。

参数说明:Django 版本建议 4.x 或 5.x,Python 3.8 以上都兼容。如果pip install慢,换国内镜像源即可,这是常规操作。

建好之后在settings.py的INSTALLED_APPS里加上'jobs',数据库先用 SQLite 跑通,后面要换 MySQL 再改DATABASES配置。

3.2 设计 Job 模型:字段类型与索引怎么定

模型设计决定了后面查询方不方便。核心字段和类型如下。

from django.db import models class Job(models.Model): title = models.CharField(max_length=200, verbose_name="岗位名称") company = models.CharField(max_length=200, verbose_name="公司名称") city = models.CharField(max_length=50, db_index=True, verbose_name="城市") education = models.CharField(max_length=20, default="不限", verbose_name="学历要求") min_salary = models.FloatField(null=True, verbose_name="最低薪资(K)") max_salary = models.FloatField(null=True, verbose_name="最高薪资(K)") avg_salary = models.FloatField(null=True, verbose_name="平均薪资(K)") created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "job" indexes = [models.Index(fields=["city", "education"])] def __str__(self): return f"{self.title}-{self.company}"

逻辑说明:city加了db_index=True,因为按城市分组统计是高频查询。avg_salary冗余存一列,避免每次聚合都现算。Meta里再建一个联合索引覆盖「城市 + 学历」的组合查询。

参数说明:FloatField允许null=True,对应清洗阶段解析失败的记录。如果数据量大,title和company也可以考虑加索引,但会拖慢写入,按查询需求权衡。

建完模型执行迁移:

python manage.py makemigrations jobs python manage.py migrate

3.3 用自定义管理命令批量导入清洗后的 CSV

不要手动一条条插,写一个 Django 管理命令,可重复执行。

# jobs/management/commands/import_jobs.py import pandas as pd from django.core.management.base import BaseCommand from jobs.models import Job class Command(BaseCommand): help = "从清洗后的 CSV 导入岗位数据" def add_arguments(self, parser): parser.add_argument("csv_path", type=str) def handle(self, *args, **options): df = pd.read_csv(options["csv_path"]) objs = [] for _, row in df.iterrows(): objs.append(Job( title=row["title"], company=row["company"], city=row["city"], education=row.get("education", "不限"), min_salary=row["min_salary"], max_salary=row["max_salary"], avg_salary=(row["min_salary"] + row["max_salary"]) / 2, )) Job.objects.bulk_create(objs, batch_size=500) self.stdout.write(self.style.SUCCESS(f"导入 {len(objs)} 条"))

逻辑说明:bulk_create批量插入比逐条save()快一个数量级,batch_size=500控制单次 SQL 大小。avg_salary在导入时就算好,查询时直接用。

参数说明:命令用法是python manage.py import_jobs jobs_clean.csv。如果重复导入会产生重复数据,可以在导入前先Job.objects.all().delete(),或者用update_or_create按联合键更新。

3.4 聚合查询接口:按城市、学历、薪资区间出统计结果

前端图表需要的是聚合后的 JSON,不是原始列表。用 Django ORM 的annotate和values直接出结果。

from django.db.models import Avg, Count, Q from django.http import JsonResponse def city_stats(request): data = (Job.objects.values("city") .annotate(count=Count("id"), avg_sal=Avg("avg_salary")) .order_by("-count")[:15]) return JsonResponse({"data": list(data)}) def salary_distribution(request): """按 5K 一档统计岗位数量""" buckets = {} for job in Job.objects.filter(avg_salary__isnull=False).only("avg_salary"): key = int(job.avg_salary // 5) * 5 buckets[key] = buckets.get(key, 0) + 1 result = [{"range": f"{k}-{k+5}K", "count": v} for k, v in sorted(buckets.items())] return JsonResponse({"data": result})

逻辑说明:city_stats用一条 SQL 完成分组聚合,取前 15 个城市。salary_distribution因为要自定义分档逻辑,用 Python 层处理,only("avg_salary")只取需要的字段减少内存占用。

参数说明:[:15]限制返回条数,避免图表太挤。分档宽度 5K 可以按数据分布调整,如果薪资集中在 10-20K,改成 2K 一档更细。接口返回统一包一层data字段,前端解析方便。

4. 可视化前端:ECharts 图表配置与 Django 模板对接

4.1 模板结构:一个 base 页加多个图表页

Django 模板用继承减少重复。base.html放公共的导航和 ECharts 引入,子页面只写图表容器和初始化脚本。

<!-- templates/base.html --> <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>{% block title %}招聘数据分析{% endblock %}</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> </head> <body> <nav> <a href="{% url 'city_view' %}">城市分布</a> <a href="{% url 'salary_view' %}">薪资分布</a> <a href="{% url 'skill_view' %}">技能需求</a> </nav> <main>{% block content %}{% endblock %}</main> </body> </html>

逻辑说明:ECharts 用 CDN 引入,省去本地打包。导航用{% url %}反向解析,路由改了模板不用动。

参数说明:CDN 地址选稳定的公共源。如果演示环境不能联网,把echarts.min.js下载到static/目录,用{% static %}引用。

4.2 城市岗位数量柱状图:从接口取数到渲染

{% extends "base.html" %} {% block content %} <div id="cityChart" style="width:100%;height:500px;"></div> <script> fetch("{% url 'city_stats' %}") .then(r => r.json()) .then(res => { const cities = res.data.map(d => d.city); const counts = res.data.map(d => d.count); const chart = echarts.init(document.getElementById("cityChart")); chart.setOption({ title: { text: "各城市岗位数量 Top15" }, tooltip: { trigger: "axis" }, xAxis: { type: "category", data: cities, axisLabel: { rotate: 45 } }, yAxis: { type: "value" }, series: [{ type: "bar", data: counts, itemStyle: { color: "#5470c6" } }] }); }); </script> {% endblock %}

逻辑说明:fetch拿到后端 JSON 后,把城市名和数量分别映射成两个数组喂给 ECharts。rotate: 45解决城市名太长重叠的问题,这是横坐标密集时的常规处理。

参数说明:height:500px给图表足够空间。颜色可以按主题调整。如果城市超过 15 个,考虑改成横向条形图,可读性更好。

4.3 薪资区间分布饼图与学历要求环形图

饼图适合看占比,环形图适合在中心放汇总数字。

// 薪资分布饼图 fetch("{% url 'salary_distribution' %}") .then(r => r.json()) .then(res => { const chart = echarts.init(document.getElementById("salaryChart")); chart.setOption({ tooltip: { trigger: "item", formatter: "{b}: {c} ({d}%)" }, series: [{ type: "pie", radius: "60%", data: res.data.map(d => ({ name: d.range, value: d.count })) }] }); });

逻辑说明:formatter里{d}%自动算百分比,不用手动计算。radius: "60%"控制饼图大小。

参数说明:如果分档太多导致饼图碎片化,把小区间合并成「其他」。环形图把radius改成["40%", "65%"]就是空心效果,中心可以用graphic组件放总数。

4.4 技能词云:从岗位描述里提取高频关键词

词云需要先做中文分词。用jieba对岗位名称或描述分词,统计词频后传给 ECharts 的wordCloud插件。

import jieba from collections import Counter def extract_skills(request): stopwords = {"的", "和", "与", "或", "等", "熟悉", "熟练", "掌握", "负责"} counter = Counter() for job in Job.objects.only("title"): for word in jieba.cut(job.title): if len(word) > 1 and word not in stopwords: counter[word] += 1 top = counter.most_common(50) return JsonResponse({"data": [{"name": w, "value": c} for w, c in top]})

逻辑说明:jieba.cut做分词,过滤单字和停用词,most_common(50)取前 50 个高频词。返回格式直接对应词云的数据结构。

参数说明:停用词表要根据实际数据补充,比如「工程师」「岗位」这类通用词也可以加进去。词云插件需要额外引入echarts-wordcloud,在 base 模板里加一行 script 即可。

5. 避坑与排查:这套系统最容易翻车的五个地方

5.1 采集返回空列表,选择器全部失效

现象:脚本跑完len(all_jobs)是 0,或者只有几条。

原因:目标页面是前端渲染的,requests拿到的 HTML 里根本没有岗位节点;或者 class 名和实际不一致。

解决:先用浏览器打开页面,右键查看源代码,确认岗位信息是否在初始 HTML 里。如果不在,改用 Network 面板找接口。选择器用开发者工具的「Copy selector」功能获取,不要凭印象写。

5.2 薪资解析大量返回 None

现象:清洗后dropna把大部分数据都删了,剩不下几条。

原因:正则只覆盖了「15-25K」这一种格式,实际数据里有「15-25千/月」「1.5-2.5万/月」「面议」「200元/天」等多种写法。

解决:先统计原始薪资字段的所有格式,用value_counts()看分布,再针对性补正则分支。万要乘 10 换算成千,天薪要单独处理或直接过滤。宁可多写几条分支,不要一刀切。

5.3 Django 迁移报错 no such table

现象:migrate之后查询还是报表不存在。

原因:app 没加到INSTALLED_APPS,或者makemigrations时没指定 app 名导致迁移文件没生成。

解决:确认settings.py里有'jobs',执行python manage.py makemigrations jobs明确指定 app,再migrate。如果之前迁移乱了,删掉migrations目录下除__init__.py外的文件和数据库,重新来一遍。

5.4 图表不显示,控制台报跨域或 404

现象:页面空白,F12 看到接口请求 404 或者 CORS 错误。

原因:URL 没配、路由名写错,或者前后端分离部署时跨域。

解决:检查urls.py里有没有对应的 path,模板里{% url %}的名字和路由name是否一致。同源部署不会有跨域问题,如果确实要分离,装django-cors-headers并在中间件和配置里加白名单。

5.5 bulk_create 导入后中文乱码

现象:数据库里城市名、公司名显示成问号或乱码。

原因:CSV 读取时编码不对,或者数据库字符集不是 utf8。

解决:pd.read_csv加encoding="utf-8-sig"。如果用 MySQL,建库时指定CHARACTER SET utf8mb4,Django 的DATABASES配置里加"OPTIONS": {"charset": "utf8mb4"}。SQLite 默认就是 UTF-8,一般不会出这个问题。

6. 让系统更像一个作品:进阶技巧与验证方法

把基础功能跑通只是及格线,答辩或展示时想拿高分,得在细节上做文章。我一般会从三个方向加东西。

第一个是加一个「薪资对比」页面,支持按城市和学历交叉筛选。实现上就是给city_stats接口加查询参数,前端加两个下拉框,选完重新 fetch。这个功能不复杂,但演示时交互感很强。

def filtered_stats(request): city = request.GET.get("city") edu = request.GET.get("education") qs = Job.objects.all() if city: qs = qs.filter(city=city) if edu: qs = qs.filter(education=edu) data = qs.aggregate(avg=Avg("avg_salary"), total=Count("id")) return JsonResponse(data)

逻辑说明:用request.GET接收筛选条件,动态拼filter,最后aggregate出汇总值。参数为空时不加过滤条件,返回全量统计。

参数说明:前端下拉框的选项可以从数据库distinct查出来动态生成,避免写死。接口返回的avg要做四舍五入,round(data["avg"], 1),不然前端显示一长串小数。

第二个是给数据加时间维度。如果采集时记录了抓取日期,可以做一个「岗位数量趋势」折线图,按天统计。这需要在模型里加一个crawl_date字段,导入时填上。趋势图用 ECharts 的line类型,x 轴是日期,y 轴是数量。

第三个是导出功能。用pandas把筛选后的数据写成 Excel,通过 Django 的HttpResponse返回,设置Content-Disposition头触发下载。这个功能实现简单,但用户感知很强。

验证系统是否真的可用,我习惯做三件事:一是用一份全新的 CSV 走一遍导入流程,确认没有硬编码路径;二是把数据库清空重新迁移,确认迁移文件完整;三是在另一台机器上 clone 代码,按 README 的步骤从零跑起来,看有没有漏掉的依赖。这三步做完,基本能排除大部分环境问题。

最后一个习惯:所有采集和清洗脚本都保留原始数据和处理后数据两份,中间步骤可回溯。这样一旦发现统计结果不对,能快速定位是采集错了还是清洗错了。做数据分析项目,数据链路的可追溯性比代码写得多漂亮更重要。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询