news 2026/9/24 19:29:06

Python招聘数据分析实战:从采集清洗到可视化大屏

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python招聘数据分析实战:从采集清洗到可视化大屏

简介:这是一套面向计算机相关专业学生与Python初学者的招聘网站数据分析与可视化实战源码,可作为毕业设计、期末大作业或课程设计参考,帮助读者完整走通从数据获取、清洗到图表呈现的分析链路。压缩包共54个文件,约6.68MB,以csv原始与清洗数据、ipynb分析笔记、py脚本、html可视化页面为主,另含png/jpg图表素材、xml与iml工程配置及少量js、css等前端资源,覆盖数据获取、数据清洗、数据可视化、Echarts大屏展示等模块,结构清晰便于按流程学习。目前已有573人学习下载。读者可从中获得招聘信息采集与清洗思路、工作经验与学历分布饼图、词云图生成方法,以及基于Echarts的大屏展示实现,适合作为数据分析入门与项目复现的参考素材。

1. 招聘数据抓下来之后,为什么多数人卡在“有表没结论”

招聘网站的数据分析,难点从来不在“爬”,而在“爬完之后怎么办”。你拿到几千条岗位记录,字段有职位名、薪资、城市、学历、经验、公司、技能标签,看着挺全,可真要回答“现在市场到底要什么能力”“哪个城市薪资虚高”“应届生该往哪投”,很多人就卡住了——数据躺在 CSV 里,画出来的图只是把表格换了个颜色。

这个标题指向的是一套完整链路:用 Python 把招聘数据采集下来,做清洗和结构化,再用可视化把结论讲清楚。它适合三类人:想做一个能写进简历的数据分析项目的人、想验证某个岗位方向真实行情的人、以及刚学完 pandas 和 matplotlib 想找个真实数据集练手的人。核心不是炫技,而是让每一张图都能回答一个具体问题。下面按“数据怎么来 → 怎么洗 → 怎么分析 → 怎么画 → 怎么避坑”的顺序,把这条链路拆开讲。

2. 数据从哪来:采集、字段设计与存储选型

2.1 招聘数据的三个来源与取舍

做招聘数据分析,数据来源决定了后面能分析什么。常见做法有三类:

第一类是公开的招聘信息列表页。这类页面结构相对规整,职位名、公司、薪资、城市、学历、经验通常都在列表或详情页里,适合做批量采集。缺点是分页有上限、字段可能不全,需要翻详情页补技能要求。

第二类是招聘网站开放的搜索接口返回的 JSON。如果你在浏览器开发者工具里能看到返回结构化数据的请求,直接请求接口比解析 HTML 稳定得多,字段也更干净。这是我最推荐的方式,因为 HTML 结构一改,选择器就全废,而接口字段相对稳定。

第三类是现成的公开数据集或竞赛数据。好处是省去采集环节,坏处是时效性差,薪资和技能热度可能已经过时。如果你只是想练分析和可视化,用现成数据集完全够;但如果你想做出“当下行情”的结论,还是得自己采。

选型上我的建议是:先花十分钟看目标站点有没有可用的结构化返回,有就用接口,没有再用页面解析。不要一上来就写复杂的爬虫框架,招聘数据量级通常几千到几万条,requests 加简单循环就够,上 Scrapy 反而增加调试成本。

2.2 字段设计:决定后面能画什么图

采集之前先把字段定下来,否则爬到一半发现缺字段,返工成本很高。一份能支撑分析的招聘数据,至少要有这些列:

字段名含义后续用途
job_title职位名称词频、岗位分类
company公司名称公司规模、行业分布
city工作城市地域薪资对比
salary_raw原始薪资文本清洗后转数值
education学历要求学历门槛分布
experience经验要求经验与薪资关系
skills技能标签技能热度词云
publish_date发布时间时效性过滤

其中 salary_raw 一定要保留原始文本,因为招聘网站的薪资写法五花八门:“15-25K·13薪”“8千-1.2万”“面议”“200-300元/天”。清洗脚本要能处理这些格式,原始列留着方便回溯。skills 字段如果列表页没有,就得进详情页抓,这是采集阶段最耗时的部分,建议先小批量跑通再放大。

2.3 用 requests 抓列表并落库的最小实现

下面这段代码演示从接口拉数据并写入 SQLite 的最小流程。实际站点接口地址和参数需要你自己在开发者工具里确认,这里只给结构和处理逻辑。

import requests import sqlite3 import time import pandas as pd # 建表:字段和 2.2 的表格对应 conn = sqlite3.connect("jobs.db") conn.execute(""" CREATE TABLE IF NOT EXISTS job ( id INTEGER PRIMARY KEY AUTOINCREMENT, job_title TEXT, company TEXT, city TEXT, salary_raw TEXT, education TEXT, experience TEXT, skills TEXT, publish_date TEXT ) """) headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Accept": "application/json" } def fetch_page(keyword, page): # 接口地址和参数以你实际抓到的请求为准 url = "https://example.com/api/job/list" params = {"query": keyword, "page": page, "pageSize": 30} resp = requests.get(url, params=params, headers=headers, timeout=10) resp.raise_for_status() return resp.json().get("data", {}).get("list", []) def save_rows(rows): for r in rows: conn.execute( "INSERT INTO job (job_title, company, city, salary_raw, education, experience, skills, publish_date) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?)", ( r.get("jobName"), r.get("companyName"), r.get("cityName"), r.get("salaryDesc"), r.get("education"), r.get("experience"), ",".join(r.get("skillTags", [])), r.get("publishTime") ) ) conn.commit() for page in range(1, 11): # 先跑 10 页验证 rows = fetch_page("python", page) if not rows: break save_rows(rows) time.sleep(1.5) # 控制频率,别把对方打挂 print(f"page {page} done, {len(rows)} rows") df = pd.read_sql("SELECT * FROM job", conn) print(df.shape)

逻辑说明:先建表固定字段,再封装 fetch_page 负责请求和解析,save_rows 负责入库,主循环控制页码和频率。参数上,pageSize 一般 20 到 50,太大容易被限制;time.sleep 的间隔建议 1 到 2 秒,这是最基本的礼貌,也能降低被封的概率。跑完先看 df.shape,确认条数和预期一致再继续。

提示:采集前先确认目标站点的 robots 协议和使用条款,控制请求频率,只采公开信息,不要涉及个人隐私字段。

3. 数据清洗:薪资、城市、技能三个字段怎么标准化

3.1 薪资文本转数值:处理五种常见格式

薪资是招聘分析里最有价值也最脏的字段。常见格式有:区间加薪数(15-25K·13薪)、中文单位(8千-1.2万)、面议、日薪(200-300元/天)、纯数字。清洗目标是统一成“月薪下限”和“月薪上限”两个数值列,单位统一为千元。

import re import numpy as np def parse_salary(text): if not text or "面议" in text: return np.nan, np.nan text = text.replace("·13薪", "").replace("·14薪", "").strip() # 日薪单独处理:200-300元/天,按 22 天折算月薪 day = re.findall(r"(\d+\.?\d*)-(\d+\.?\d*)元/天", text) if day: low, high = float(day[0][0]) * 22 / 1000, float(day[0][1]) * 22 / 1000 return low, high # 统一单位:万 -> 千 text = text.replace("万", "0000").replace("千", "000") nums = re.findall(r"(\d+\.?\d*)", text) if len(nums) >= 2: low, high = float(nums[0]), float(nums[1]) # 如果数字很小说明单位是 K,直接除以 1000 得到千 if high > 1000: low, high = low / 1000, high / 1000 return low, high if len(nums) == 1: v = float(nums[0]) return v / 1000, v / 1000 return np.nan, np.nan df[["salary_low", "salary_high"]] = df["salary_raw"].apply( lambda x: pd.Series(parse_salary(x)) ) df["salary_avg"] = (df["salary_low"] + df["salary_high"]) / 2 print(df[["salary_raw", "salary_low", "salary_high", "salary_avg"]].head(10))

逻辑说明:先排除面议,再单独处理日薪(按每月 22 个工作日折算),然后把“万”“千”统一替换成数字再提取。参数上,22 这个折算系数可以根据实际岗位调整,实习岗可能按 20 天算。清洗完一定要抽样看前 10 行,确认没有把“15-25K”错算成 15 和 25 千以外的值。这一步是后面所有薪资分析的地基,宁可多花时间核对。

3.2 城市字段归一化与技能标签拆分

城市字段常见问题是“北京·朝阳区”“上海-浦东新区”这种带区域的写法,分析地域分布时要先截取主城市。技能标签如果存成逗号分隔的字符串,分析前要拆成列表再展开。

# 城市归一化:按分隔符取第一段 df["city_main"] = df["city"].astype(str).str.split(r"[·\-]").str[0].str.strip() # 技能拆分并展开成长表,便于统计词频 df["skills"] = df["skills"].fillna("") skill_df = df.assign(skill=df["skills"].str.split(",")).explode("skill") skill_df = skill_df[skill_df["skill"].str.strip() != ""] skill_count = skill_df["skill"].str.strip().value_counts().head(20) print(skill_count)

逻辑说明:城市用正则按“·”或“-”切分取第一段,能覆盖绝大多数写法。技能用 explode 把一行多技能展开成多行,再 value_counts 统计。参数上 head(20) 只是先看前 20 个,实际画图时可以根据需要取前 15 到 30。这一步做完,你就有了“哪些技能出现最多”的直接答案,也是词云和柱状图的数据源。

3.3 缺失值与异常值的处理边界

清洗绕不开缺失值。学历、经验缺失可以填“不限”,薪资缺失建议直接剔除而不是填均值,因为薪资缺失往往意味着岗位本身不透明,填均值会污染分布。异常值方面,月薪低于 1 千或高于 100 千的记录要单独看,可能是单位解析错误,也可能是高管岗,处理方式不同。

# 缺失处理 df["education"] = df["education"].fillna("不限") df["experience"] = df["experience"].fillna("不限") # 薪资异常值检查 print(df[df["salary_avg"] < 1].shape[0], "条低于1千") print(df[df["salary_avg"] > 100].shape[0], "条高于100千") # 分析时只保留薪资有效的记录 df_valid = df.dropna(subset=["salary_avg"]).copy() df_valid = df_valid[(df_valid["salary_avg"] >= 1) & (df_valid["salary_avg"] <= 100)] print("有效记录:", df_valid.shape[0])

逻辑说明:学历和经验填“不限”是业务上合理的默认值,薪资则用 dropna 加区间过滤。参数上 1 和 100 这两个边界是根据市场常识定的,你可以根据自己采集的岗位类型调整。这一步之后,df_valid 才是真正用于分析的干净数据,后面所有图表都基于它。

4. 分析建模:从薪资分布到技能共现

4.1 薪资分布与城市对比怎么做才不误导

薪资分析最容易犯的错是用平均值代表整体。招聘薪资是典型的右偏分布,少数高薪岗位会把均值拉高。正确做法是同时看中位数和分位数,用箱线图或小提琴图展示分布,而不是只画一根平均柱。

import matplotlib.pyplot as plt import seaborn as sns plt.rcParams["font.sans-serif"] = ["SimHei"] # 中文显示 plt.rcParams["axes.unicode_minus"] = False # 城市薪资对比:取记录数前 8 的城市 top_city = df_valid["city_main"].value_counts().head(8).index city_data = df_valid[df_valid["city_main"].isin(top_city)] plt.figure(figsize=(12, 6)) sns.boxplot(data=city_data, x="city_main", y="salary_avg", order=top_city) plt.title("主要城市薪资分布对比") plt.ylabel("平均月薪(千元)") plt.xticks(rotation=30) plt.tight_layout() plt.savefig("city_salary.png", dpi=150)

逻辑说明:boxplot 能同时看到中位数、四分位和离群点,比柱状图信息量大得多。参数上 order=top_city 保证城市按记录数排序,避免图太乱;dpi=150 保证导出清晰。看这张图时重点看中位数高低和箱体宽窄,箱体宽说明该城市薪资分化大。

4.2 经验、学历与薪资的关系验证

“经验越多薪资越高”这个结论需要用数据验证,而不是想当然。做法是把经验要求映射成有序的数值(应届、1-3年、3-5年、5-10年),再按学历分组看薪资中位数。

exp_order = ["应届", "1年以内", "1-3年", "3-5年", "5-10年", "10年以上"] df_valid["exp_clean"] = df_valid["experience"].str.extract( r"(应届|1年以内|1-3年|3-5年|5-10年|10年以上)" )[0].fillna("不限") pivot = df_valid.pivot_table( index="exp_clean", columns="education", values="salary_avg", aggfunc="median" ) print(pivot.round(1))

逻辑说明:用正则把经验文本映射到固定档位,再用 pivot_table 做交叉表,aggfunc 用 median 而不是 mean。参数上,经验档位的划分要和你采集到的实际文本对齐,如果站点用的是“经验不限”“在校/应届”,正则要相应调整。这张交叉表能直接回答“本科 3-5 年大概什么价位”这类问题。

4.3 技能共现:哪些技能总是一起出现

单看技能词频只能知道什么技能热,看不出技能组合。用共现矩阵能发现“Python 和 SQL 经常一起要求”“机器学习岗位往往还要 Docker”。做法是把技能展开后,对同一岗位的技能两两组合计数。

from itertools import combinations from collections import Counter # 只保留出现次数前 30 的技能,避免矩阵太稀疏 top_skills = set(skill_count.head(30).index) co = Counter() for skills in df_valid["skills"].str.split(","): s = [x.strip() for x in skills if x.strip() in top_skills] for a, b in combinations(sorted(set(s)), 2): co[(a, b)] += 1 top_pairs = co.most_common(15) for (a, b), c in top_pairs: print(f"{a} + {b}: {c}")

逻辑说明:先限定高频技能集合,再用 combinations 生成两两组合,Counter 统计。参数上 head(30) 控制矩阵规模,太小会漏掉重要组合,太大会让结果稀疏。输出的高频技能对可以直接做成热力图,是简历优化和课程设计的直接依据。

5. 可视化落地:用 ECharts 做可交互的招聘数据大屏

5.1 为什么分析完还要单独做可视化层

matplotlib 适合自己看,但要给别人看、要能筛选和悬停查看,就得用 ECharts 这类前端图表库。常见做法是 Python 负责出数据(导出 JSON),前端用 ECharts 渲染。这样分析和展示解耦,改图不用动分析代码。

import json # 导出城市薪资数据给前端 city_stats = df_valid.groupby("city_main")["salary_avg"].agg( ["count", "median"] ).reset_index() city_stats = city_stats[city_stats["count"] >= 20].sort_values("median", ascending=False) result = { "cities": city_stats["city_main"].tolist(), "medians": city_stats["median"].round(1).tolist(), "counts": city_stats["count"].tolist() } with open("city_stats.json", "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False) print("导出完成", len(result["cities"]), "个城市")

逻辑说明:按城市聚合出记录数和中位薪资,过滤掉样本太少的城市(少于 20 条结论不可靠),导出成前端能直接读的 JSON。参数上 count >= 20 是经验阈值,样本太少中位数波动大。这个 JSON 就是 ECharts 的数据源。

5.2 ECharts 柱状图与词云的最小配置

前端拿到 JSON 后,用 ECharts 渲染柱状图,技能热度可以用词云插件。下面是最小可运行结构。

// 假设页面已引入 echarts.min.js 和 echarts-wordcloud.min.js fetch('city_stats.json') .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('cityChart')); chart.setOption({ title: { text: '各城市招聘薪资中位数(千元)' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: data.cities, axisLabel: { rotate: 30 } }, yAxis: { type: 'value', name: '月薪/千元' }, series: [{ type: 'bar', data: data.medians, itemStyle: { color: '#5470c6' }, label: { show: true, position: 'top' } }] }); window.addEventListener('resize', () => chart.resize()); });

逻辑说明:fetch 读取 Python 导出的 JSON,init 绑定容器,setOption 配置坐标轴和系列。参数上 axisLabel.rotate 防止城市名重叠,resize 监听保证窗口变化时图表自适应。词云配置类似,把 series type 换成 wordCloud,data 换成技能名和频次即可。

5.3 大屏布局的三个实用约束

做招聘数据大屏,布局上有几个硬约束:一是图表数量控制在 6 到 8 个,太多反而没重点;二是每个图表配一句结论性标题,比如“北京中位薪资领先但分化最大”,而不是“城市薪资图”;三是颜色统一色系,薪资用蓝、技能用绿、分布用灰,避免花哨。常见做法是用 CSS Grid 做三列布局,顶部放核心指标卡(总岗位数、平均薪资、最热技能),下面放图表。

注意:大屏是给人快速看结论的,不是把所有图堆上去。每加一个图先问它回答什么问题,答不上来就删掉。

6. 避坑与排查:招聘数据分析里最容易翻车的五件事

6.1 薪资解析把“万”和“K”混算

现象:清洗后出现月薪 15000 千元这种离谱值。原因:正则先替换了“万”为“0000”,又对大于 1000 的数除以 1000,导致“1.5万”被算成 15000 再除 1000 变成 15,而“15K”本身是 15 却被当成 15000 再除。解决:先判断原始文本里有没有“万”或“千”,有中文单位就按中文单位算,没有才按 K 处理,两条分支分开写,别混在一个正则里。

6.2 城市没归一化导致同一城市被拆成多条

现象:地域分布图里出现“北京”“北京·朝阳”“北京-海淀”三个条目。原因:采集时直接存了带区域的原始字符串,分析时没截取主城市。解决:在清洗阶段统一用分隔符切分取第一段,并且把“北京市”这类带“市”的也去掉,保证一个城市只有一个名字。

6.3 技能字段为空导致词云空白

现象:词云图什么都没有,或者只有“不限”两个字。原因:列表页没有技能字段,采集时该列全是空字符串,explode 后没有有效数据。解决:确认技能字段是否必须进详情页抓;如果暂时拿不到,就先用职位名称做分词替代,或者明确在项目里说明技能数据来源,不要用空数据硬画。

6.4 样本量太小就下结论

现象:某个三线城市中位薪资排第一,结论说“该城市薪资最高”。原因:该城市只有 3 条记录,中位数没有统计意义。解决:所有分组统计前先过滤样本量,城市、公司、经验档位都设一个最小记录数阈值(比如 20),低于阈值的合并成“其他”或不展示。

6.5 图表中文乱码

现象:matplotlib 图上中文全是方框。原因:默认字体不支持中文。解决:在绘图前设置plt.rcParams["font.sans-serif"] = ["SimHei"]plt.rcParams["axes.unicode_minus"] = False,Linux 环境下如果没有 SimHei,换成系统里已有的中文字体,比如 WenQuanYi 或 Noto Sans CJK。

7. 让这套分析真正可复用的两个进阶技巧

第一个技巧是把整条链路脚本化,用一条命令跑完采集到出图。我一般会拆成fetch.pyclean.pyanalyze.pyexport.py四个文件,用一个run.sh串起来。这样下次换个关键词或换个城市,只改配置不改逻辑。配置项包括搜索关键词、目标城市、采集页数、最小样本阈值,全部放在一个config.py里。血泪经验是:不要把参数写死在函数里,否则每次调整都要翻代码。

#!/bin/bash # run.sh:一键跑完整条链路 python fetch.py --keyword "python" --pages 20 python clean.py python analyze.py python export.py echo "全部完成,图表在 output/ 目录"

第二个技巧是给分析结果加一层“可验证”的检查。每次跑完,自动输出几个关键数字:有效记录数、薪资解析成功率、城市数量、Top5 技能。如果薪资解析成功率低于 80%,说明清洗规则没覆盖新格式,要回去补正则。这个检查能帮你在数据出问题前就发现,而不是等图画出来才发现不对。

# 数据质量自检 total = len(df) parsed = df["salary_avg"].notna().sum() print(f"总记录 {total},薪资解析成功 {parsed},成功率 {parsed/total:.1%}") print(f"城市数量 {df['city_main'].nunique()}") print("Top5 技能:") print(skill_count.head(5))

参数上,成功率阈值我一般定在 80%,低于这个数就说明采集到的薪资格式有变化,需要更新解析逻辑。城市数量和 Top5 技能则是用来和上一次跑的结果对比,如果突然变化很大,可能是采集源改了或者被限制了。

这套东西做下来,你会发现招聘数据分析的价值不在于图多好看,而在于每个结论都能追溯到具体的数据处理和统计方法。我自己踩过最深的坑就是一开始急着画图,结果薪资没洗干净,画出来的分布全是错的,返工重来。后来养成习惯:清洗完先打印统计摘要,确认数据合理再动手画。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 19:27:54

用Playground脚本快速搭建Hadoop三节点完全分布式集群

先把话说在前面&#xff1a;干大数据这行&#xff0c;自己手动搭过一套Hadoop集群的人&#xff0c;十有八九都被配置文件和进程日志折磨过。网上那些动辄几十步的教程&#xff0c;你照着敲到凌晨两点&#xff0c;最后发现是hosts没配对&#xff0c;真的很泄气。所以当我第一次用…

作者头像 李华
网站建设 2026/9/24 19:27:08

api-ms-win-core-profile-l1-1-0.dll缺失修复:Windows系统DLL报错完整指南

平时帮朋友修电脑&#xff0c;最常遇到的一类弹窗错误就是“无法启动此程序&#xff0c;因为计算机中丢失 api-ms-win-core-profile-l1-1-0.dll”&#xff0c;或者是“找不到 api-ms-win-core-profile-l1-1-0.dll&#xff0c;无法继续执行代码”。很多人第一次看到这个提示都会…

作者头像 李华
网站建设 2026/9/24 19:27:08

Netty粘包拆包源码解析:ByteToMessageDecoder与LengthFieldBasedFrameDecoder深度剖析

Netty源码分析写了好几篇了&#xff0c;后台不断有朋友催更“认真系列”第二篇。上一篇我们把 Netty 的整体脉络、NioEventLoop 线程模型和启动流程啃了一遍&#xff0c;这次我想换个角度&#xff0c;挑一个实际工作中几乎每天都会碰到、面试也高频被问的方向来拆——就是粘包拆…

作者头像 李华
网站建设 2026/9/24 19:27:07

时间序列预测Python实战:三个经典数据集跑通ARIMA与SARIMA

简介&#xff1a;面向 Python 时间序列预测初学者与分析人员&#xff0c;这份代码资源覆盖金融、气象、销售等常见时序场景&#xff0c;系统演示 Pandas 预处理、ARIMA/SARIMA 建模、状态空间方法、Prophet 以及机器学习模型的应用。压缩包共 214 个文件&#xff0c;含 181 个可…

作者头像 李华