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, paramsparams, headersheaders, timeout10) 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(fpage {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(skilldf[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(datacity_data, xcity_main, ysalary_avg, ordertop_city) plt.title(主要城市薪资分布对比) plt.ylabel(平均月薪千元) plt.xticks(rotation30) plt.tight_layout() plt.savefig(city_salary.png, dpi150)逻辑说明boxplot 能同时看到中位数、四分位和离群点比柱状图信息量大得多。参数上 ordertop_city 保证城市按记录数排序避免图太乱dpi150 保证导出清晰。看这张图时重点看中位数高低和箱体宽窄箱体宽说明该城市薪资分化大。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( indexexp_clean, columnseducation, valuessalary_avg, aggfuncmedian ) 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, ascendingFalse) 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, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse) 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 导出的 JSONinit 绑定容器setOption 配置坐标轴和系列。参数上 axisLabel.rotate 防止城市名重叠resize 监听保证窗口变化时图表自适应。词云配置类似把 series type 换成 wordClouddata 换成技能名和频次即可。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] FalseLinux 环境下如果没有 SimHei换成系统里已有的中文字体比如 WenQuanYi 或 Noto Sans CJK。7. 让这套分析真正可复用的两个进阶技巧第一个技巧是把整条链路脚本化用一条命令跑完采集到出图。我一般会拆成fetch.py、clean.py、analyze.py、export.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 技能则是用来和上一次跑的结果对比如果突然变化很大可能是采集源改了或者被限制了。这套东西做下来你会发现招聘数据分析的价值不在于图多好看而在于每个结论都能追溯到具体的数据处理和统计方法。我自己踩过最深的坑就是一开始急着画图结果薪资没洗干净画出来的分布全是错的返工重来。后来养成习惯清洗完先打印统计摘要确认数据合理再动手画。希望帮到你。本文还有配套的精品资源点击获取