简介:面向求职平台数据研究场景的 Python 数据分析毕业设计项目,围绕 Boss直聘热门城市岗位信息,完整实现数据采集、清洗、分析与可视化流程。项目基于 Scrapy 爬虫框架抓取岗位数据并以 CSV 格式落盘,针对高耦合脏数据设计预处理方案,再通过 Python + SQL 进行多维度分析,借助 Pyecharts 生成可视化图表,最后由轻量级 Flask 框架完成部署,适合作为数据采集与可视化方向课程设计或毕业设计的参考,能直观理解端到端实现链路。资源包共 38 个文件,包含 13 个 Python 脚本(爬虫、数据处理、Flask 应用)、12 个 JS 文件、4 个 XML 配置文件及 CSS、HTML、CSV 等文件,压缩包大小仅 241KB,目录结构清晰,便于快速定位和二次开发。目前已有 4863 人学习浏览,作为小型完整项目范例,可帮助读者掌握爬虫框架运用、数据清洗思路、SQL 分析及可视化部署的串联方法,具有较强的模仿与复用价值。
1. Boss直聘岗位数据分析:毕业设计从选题到落地
很多计算机相关专业的毕业设计把「Boss直聘岗位数据分析」做成爬虫展示页,最后被答辩老师一句「你的分析结论从哪来」问住。这个课题的核心难点不在采集,而在把脏乱的非结构化招聘文本转换成可量化、可验证的岗位画像。本文以 Boss 直聘公开页面作为数据源,梳理一条从接口请求、数据清洗到可视化输出的完整路径,覆盖中小样本量下复现该课题的常见做法与关键参数设置。适合准备做数据分析类毕业设计的学生、刚接触招聘数据处理的初级工程师,以及需要理解岗位数据结构的业务分析人员。读完你可以直接复现一套可运行的分析流程,也能回答「样本怎么来、口径怎么定、结论怎么验证」这类必问问题。
2. 岗位数据获取:Boss直聘数据采集的技术路径与合规边界
2.1 数据源特征与接口请求结构
Boss直聘的岗位数据在 Web 端通过异步接口加载,搜索结果的 HTML 里不直接包含完整岗位字段,真实数据来自https://www.zhipin.com/wapi/zpgeek/search/joblist.json这类 JSON 接口。该接口的主要参数包括query(搜索关键词)、city(城市编码)、page(页码)与pageSize(每页数量)。
接口返回的 JSON 结构里,业务数据在zpData.jobList数组中,每条记录有 20 多个字段。做毕业设计时不需要全量保存,核心字段如下:
| 字段名 | 示例值 | 用途 |
|---|---|---|
| jobName | Java开发工程师 | 岗位名称 |
| salaryDesc | 15-25K·13薪 | 薪资描述,需拆解 |
| brandName | 字节跳动 | 公司名称 |
| cityName | 北京 | 工作城市 |
| experienceDesc | 3-5年 | 经验要求 |
| eduDesc | 本科 | 学历要求 |
| jobLabels | ["股票期权","弹性工作"] | 福利标签 |
| jobDesc | 负责...系统开发 | 职位描述,用于技能提取 |
提示:
salaryDesc、experienceDesc、eduDesc都是非结构化文本,直接做统计分析前必须拆成数值字段,这是后续清洗章节的重点,也是答辩时展示技术含量的地方。
2.2 用Python请求岗位搜索接口的最小代码
在本地环境复现请求逻辑,推荐使用requests库,将浏览器复制到的 Cookie 放入请求头,伪装成正常访问。下面代码演示单页请求与数据提取:
import requests import json import pandas as pd headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Cookie": "你的浏览器Cookie", # 手动从浏览器复制 "Referer": "https://www.zhipin.com/web/geek/job" } def fetch_jobs(keyword: str, city_code: str, page: int) -> list: url = "https://www.zhipin.com/wapi/zpgeek/search/joblist.json" params = { "query": keyword, "city": city_code, "page": page, "pageSize": 30 } resp = requests.get(url, params=params, headers=headers, timeout=10) data = resp.json() if data.get("code") != 0: print(f"请求失败: {data.get('message')}") return [] return data["zpData"]["jobList"] jobs = fetch_jobs("数据分析", "100010000", 1) print(f"获取到 {len(jobs)} 条岗位数据") df = pd.DataFrame(jobs) print(df[["jobName", "salaryDesc", "cityName"]].head())这段代码先通过params构造查询参数,其中city使用城市编码而非中文名,因为接口识别的是数字编码。code == 0表示请求成功,否则需要检查 Cookie 是否过期。拿到返回的jobList后直接创建 DataFrame,字段都保持后端原始值,方便后续清洗时对照。
2.3 采集策略与反爬规避的边界
Boss直聘对高频请求有较强的风控策略,毕业设计只需要百级到千级样本时,不需要设计复杂代理池。常见做法是设置 1 到 3 秒的随机延时,控制单日请求量在数百次以内,连续请求超过 20 页会出现验证码。如果你发现响应中code为常见风控错误码,对应的处理方法是降低请求频率、更换关键词或等待一段时间再继续。
提示:本课题重点关注数据分析过程而非爬虫对抗,采集到 500 条以上有效样本就足够支撑统计分析。如果接口请求量过大,也可以保存一次请求结果到本地 JSON 文件,后续分析全部离线进行,减少变量干扰。
3. 数据清洗与存储:让原始岗位数据变成可分析的二维表
3.1 字段规整与数据漂移处理
从接口拿到的原始字段里,最容易出问题的是「字段偶尔不存在」或「值为空」。最稳妥的做法是在进入清洗流程前先做 schema 校验,用白名单字段过滤:
required = ["jobName", "salaryDesc", "brandName", "cityName", "experienceDesc", "eduDesc", "jobDesc"] def normalize_jobs(jobs: list) -> list: cleaned = [] for job in jobs: row = {} for field in required: val = job.get(field) if isinstance(val, list): val = "|".join(val) # jobLabels等数组字段转字符串 row[field] = val if val is not None else "" cleaned.append(row) return cleaned df_clean = pd.DataFrame(normalize_jobs(jobs)) df_clean = df_clean.drop_duplicates(subset=["jobName", "brandName", "cityName"]) print(df_clean.shape)这里的核心逻辑有两点:一是用get方法避免 KeyError,缺失值统一填充空字符串;二是对数组型字段(如福利标签)用|拼接,便于后续分词统计。drop_duplicates按jobName + brandName + cityName去重,这比只按岗位名去重更准确,因为不同公司可能发布同名岗位。
3.2 薪资区间拆分与经验年限映射
薪资字段是最需要打磨的部分。原始文本形如"20-30K·13薪"或"25-50K·15薪",要拆分出低薪、高薪和月薪中位数,同时把"13薪"这类信息单独记录下来:
import re def parse_salary(s: str): if not isinstance(s, str) or s == "": return None, None, None, None s = s.strip() # 匹配数字K区间 nums = re.findall(r"(\d+)-(\d+)K", s) if nums: low, high = int(nums[0][0]), int(nums[0][1]) median = (low + high) / 2 bonus = re.search(r"(\d+)薪", s) bonus_val = int(bonus.group(1)) if bonus else 12 return low, high, median, bonus_val # 匹配 "面议" return None, None, None, None df_clean["salary_low"] = df_clean["salaryDesc"].apply(lambda x: parse_salary(x)[0]) df_clean["salary_high"] = df_clean["salaryDesc"].apply(lambda x: parse_salary(x)[1]) df_clean["salary_median"] = df_clean["salaryDesc"].apply(lambda x: parse_salary(x)[2]) df_clean["salary_months"] = df_clean["salaryDesc"].apply(lambda x: parse_salary(x)[3])正则(\d+)-(\d+)K匹配的是"15-25K"这类最标准写法。如果数据里有"10-15K·13薪"甚至"3-5K·13薪",都能正确解析。遇到"面议"或空值返回None,后续分析时直接剔除薪资缺失的记录,避免影响整体统计。salary_months默认赋 12,也就是没有注明年终奖时按 12 薪计算。
经验字段同理,将"3-5年"拆成起始年限exp_low与结束年限exp_high。不需要做太复杂的映射,学历字段则建议保留原始值,因为不同公司对"本科"与"统招本科"的表述不一致,统一映射成本科会丢失一部分数据语义。
3.3 存储选型与SQL查询优化
数据量在万级以下,SQLite 是最省事的方案。不需要额外安装数据库服务,Python 内置sqlite3模块直接建表:
CREATE TABLE IF NOT EXISTS job_analysis ( id INTEGER PRIMARY KEY AUTOINCREMENT, job_name TEXT, brand_name TEXT, city_name TEXT, salary_low INTEGER, salary_high INTEGER, salary_median REAL, experience_low REAL, experience_high REAL, edu_desc TEXT, job_desc TEXT );写入时用executemany批量插入,比逐条 insert 快一个量级。考虑到后面会频繁做分组聚合,建议在city_name和salary_median两个字段上建立联合索引。分析阶段的 SQL 例如「按城市统计平均薪资」可以写成:
SELECT city_name, COUNT(*) AS job_count, ROUND(AVG(salary_median), 1) AS avg_salary FROM job_analysis WHERE salary_median IS NOT NULL GROUP BY city_name ORDER BY job_count DESC;SQLite 对这种千行级别的聚合查询耗时在毫秒级,不需要额外引入 Spark 或 Hadoop。毕业设计里主动使用 SQLite 做数据存取,比直接读写 CSV 更能体现工程意识。
4. 岗位数据分析与可视化:从分布趋势到岗位画像
4.1 城市维度分析与岗位活跃度排行
分析的第一步先看数据覆盖的城市分布。用 pandas 做分组统计,然后输出每个城市的岗位数量、平均薪资中位数和公司数:
city_stats = df_clean.groupby("cityName").agg( job_count=("jobName", "count"), avg_salary=("salary_median", "mean"), company_count=("brandName", "nunique") ).reset_index() city_stats = city_stats.sort_values("job_count", ascending=False) print(city_stats.head(10))agg在一条语句里同时聚合了岗位数、平均薪资和公司数,其中nunique统计的是去重后的公司数量,这个指标比岗位数更能反映城市的就业多样性。薪资观察建议用中位数而不是平均数,因为头部互联网公司的高薪岗位会拉高均值,造成“城市平均薪资虚高”的误判。
可视化核心推荐 pyecharts 的柱状图与地图组合。地图需要城市名称与区域编码匹配,如果只想快速展示,用横向柱状图就行:
from pyecharts.charts import Bar from pyecharts import options as opts bar = ( Bar() .add_xaxis(city_stats["cityName"].head(10).tolist()) .add_yaxis("岗位数量", city_stats["job_count"].head(10).tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="数据分析岗位城市分布TOP10"), xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=45)) ) ) bar.render("city_bar.html")设置rotate=45避免城市名重叠,render生成独立 HTML 文件,答辩时直接浏览器打开即可,不需要额外部署 Web 服务。
4.2 薪资-经验-学历三维交叉分析
单看薪资中位数不够深入,更有价值的分析是「经验要求与薪资的关系」,这能回答求职者最关心的预期问题。先按经验区间分桶,再计算每个桶内的薪资分位数:
bins = [0, 1, 3, 5, 10, 20] labels = ["0-1年", "1-3年", "3-5年", "5-10年", "10年以上"] df_clean["exp_group"] = pd.cut(df_clean["experience_low"], bins=bins, labels=labels) exp_salary = df_clean.groupby("exp_group", observed=True)["salary_median"].agg( ["median", "quantile", "count"] ) exp_salary.columns = ["薪资中位数", "75分位薪资", "岗位数"] print(exp_salary)pd.cut按经验下限分箱,比直接按字符串排序更稳定。agg同时计算中位数、75 分位数和岗位数,能看到不同经验档位的薪资跨度。注意observed=True参数需要 pandas 2.0+,否则 Categorical 类型分组会报警告。
学历与薪资的交叉分析可以用透视表完成:
pivot = df_clean.pivot_table( index="eduDesc", values="salary_median", aggfunc=["count", "median"] )透视表能直观看出「硕士学历的中位数薪资比本科高多少」这类结论。如果只展示最高学历要求,会让数据信息变弱——招聘岗位常写「本科及以上」,这个语义需要单独说明,不要直接当作严格的学历门槛。
4.3 技能要求词云与岗位画像构建
岗位描述jobDesc是长文本,最适合做关键词提取与词云。清洗流程是:先用 jieba 分词,再过滤停用词、标点和单个字,最后统计词频:
import jieba from collections import Counter stopwords = set("的 了 和 在 与 及 或 负责 参与 能 熟悉 相关 等".split()) def extract_keywords(text: str) -> list: words = jieba.lcut(text) words = [w.strip() for w in words if len(w.strip()) > 1] words = [w for w in words if w not in stopwords] return words keyword_counter = Counter() for desc in df_clean["jobDesc"].dropna(): keyword_counter.update(extract_keywords(desc)) top_keywords = keyword_counter.most_common(30) print(top_keywords)需要注意的坑是 jieba 默认词典对「机器学习」「数据挖掘」这类专业名词切分不完整,会产出"机器"、"学习"这样的碎片。解决方式是用jieba.add_word("机器学习", freq=200)提前加入自定义词表,或者基于业务预置一份领域词典。
岗位画像的构建思路:将词频最高的 20 个词按「硬技能」「软技能」「行业属性」人工归类,统计每个类别的占比,形成结论文案,例如「该城市 65% 的分析师岗位要求 SQL,45% 要求 Python」。词云图推荐 pyecharts 的WordCloud,生成 HTML 可交互展示,比静态图片更有展示效果。
5. 从清洗到验证:一套可复用的岗位数据准确性校验技巧
数据清洗完成后,做任何可视化之前先跑一遍「数据体检脚本」验证清洗结果是否符合预期。具体做法是输出薪资字段的描述统计与异常值清单:
print(df_clean["salary_median"].describe()) abnormal = df_clean[ (df_clean["salary_median"] < 3) | (df_clean["salary_median"] > 80) ] print(abnormal[["jobName", "salaryDesc", "salary_median"]].head())中位数薪资低于 3K 或高于 80K 的岗位大概率是数据解析问题。常见错误包括"1.5-2万"这种以「万」为单位的薪资描述没被正则匹配到,以及"500-800元/天"这类日薪字段被误当作月薪。留意「K」与「万」的单位差异,正则匹配时会漏掉这些值,需要补充单独的parse_salary分支。
第二个常用验证方案是「随机抽样回看」:随机抽取 20 条清洗后的数据,人工比对原始文本与结构化字段是否一致。能通过人工校验,才能证明这个清洗逻辑是可靠的,这部分在答辩时尤其重要——答辩老师不关心你写了多少行代码,但很在意数据可信度。
最后做一次「结论稳定性验证」:将样本按城市分层随机重采样 1000 次,计算平均薪资中位数的标准差。如果多次重采样的结果有显著差异,说明样本量不足以支撑结论,需要补充数据。这个验证逻辑用 pandas 几行代码就能完成,却比一张炫酷的可视图更能体现数据分析的严谨性。
本文还有配套的精品资源,点击获取