
简介这是一套面向计算机相关专业学生与项目实战学习者的毕业设计完整方案主题为基于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 层和 ORMECharts 或 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, headersHEADERS, paramsparams, timeout10) 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(stripTrue), company: item.select_one(.company-name).get_text(stripTrue), city: item.select_one(.job-area).get_text(stripTrue), salary: item.select_one(.salary).get_text(stripTrue), education: item.select_one(.edu-level).get_text(stripTrue) if item.select_one(.edu-level) else 不限, }) return jobs def save_to_csv(rows, pathjobs_raw.csv): with open(path, w, newline, encodingutf-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负责落盘。encodingutf-8-sig是为了 Excel 打开 CSV 不乱码这个细节很多人第一次导出时会踩。参数说明timeout10防止请求挂死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, indexFalse, encodingutf-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.xPython 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_length200, verbose_name岗位名称) company models.CharField(max_length200, verbose_name公司名称) city models.CharField(max_length50, db_indexTrue, verbose_name城市) education models.CharField(max_length20, default不限, verbose_name学历要求) min_salary models.FloatField(nullTrue, verbose_name最低薪资(K)) max_salary models.FloatField(nullTrue, verbose_name最高薪资(K)) avg_salary models.FloatField(nullTrue, verbose_name平均薪资(K)) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table job indexes [models.Index(fields[city, education])] def __str__(self): return f{self.title}-{self.company}逻辑说明city加了db_indexTrue因为按城市分组统计是高频查询。avg_salary冗余存一列避免每次聚合都现算。Meta里再建一个联合索引覆盖「城市 学历」的组合查询。参数说明FloatField允许nullTrue对应清洗阶段解析失败的记录。如果数据量大title和company也可以考虑加索引但会拖慢写入按查询需求权衡。建完模型执行迁移python manage.py makemigrations jobs python manage.py migrate3.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, typestr) def handle(self, *args, **options): df pd.read_csv(options[csv_path]) objs [] for _, row in df.iterrows(): objs.append(Job( titlerow[title], companyrow[company], cityrow[city], educationrow.get(education, 不限), min_salaryrow[min_salary], max_salaryrow[max_salary], avg_salary(row[min_salary] row[max_salary]) / 2, )) Job.objects.bulk_create(objs, batch_size500) self.stdout.write(self.style.SUCCESS(f导入 {len(objs)} 条))逻辑说明bulk_create批量插入比逐条save()快一个数量级batch_size500控制单次 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(countCount(id), avg_salAvg(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__isnullFalse).only(avg_salary): key int(job.avg_salary // 5) * 5 buckets[key] buckets.get(key, 0) 1 result [{range: f{k}-{k5}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 langzh head meta charsetUTF-8 title{% block title %}招聘数据分析{% endblock %}/title script srchttps://cdn.jsdelivr.net/npm/echarts5/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 idcityChart stylewidth: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加encodingutf-8-sig。如果用 MySQL建库时指定CHARACTER SET utf8mb4Django 的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(citycity) if edu: qs qs.filter(educationedu) data qs.aggregate(avgAvg(avg_salary), totalCount(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 的步骤从零跑起来看有没有漏掉的依赖。这三步做完基本能排除大部分环境问题。最后一个习惯所有采集和清洗脚本都保留原始数据和处理后数据两份中间步骤可回溯。这样一旦发现统计结果不对能快速定位是采集错了还是清洗错了。做数据分析项目数据链路的可追溯性比代码写得多漂亮更重要。希望帮到你。本文还有配套的精品资源点击获取