Python招聘数据可视化:从爬虫到岗位热度监控实战

发布时间:2026/9/18 17:01:02
Python招聘数据可视化:从爬虫到岗位热度监控实战 简介这是一份基于Python的招聘信息可视化分析技术文档面向互联网行业数据分析初学者、求职者及HR相关从业者围绕数据获取、清洗、分析到可视化展示的完整流程展开。文档以docx格式整理全包仅1个文件大小约198KB内容覆盖Pandas、NumPy、Matplotlib与Seaborn等核心库的应用并结合词云图、地理热图、箱线图等可视化案例讲解职位需求、薪资分布与技能频次的分析方法。目前已有129人学习读者可借助其中的爬虫抓取思路、数据预处理技巧和可视化代码框架快速搭建自己的招聘数据观察流程洞察岗位趋势与技能要求。整体篇幅精炼适合作为入门实操参考。1. 招聘数据可视化分析从职位文本里挖出市场信号判断一个岗位是否热门不必等季度报告。把招聘网站上每天更新的职位抓下来按岗位、城市、薪资、技能字段做统计就能看到真实的供需信号。这个过程用 Python 实现很顺Requests/BeautifulSoup 负责采集Pandas 负责清洗和聚合Matplotlib、Seaborn、Plotly 负责把结果画成图。整套链路做完就是一份能持续更新的招聘行情看板。多数人第一次做这个项目卡住的不是画图而是数据规整。“10k-15k”这种薪资字段不能直接参与计算岗位名在每家公司叫法不一样技能关键词埋在职位描述里需要词表或分词处理。这篇整理按“采集 → 清洗 → 统计 → 可视化 → 增量监控”的顺序拆开讲适合正在学爬虫和数据分析、想拿真实业务数据练手的人。2. 职位采集与字段规整Requests 抓取 Pandas 清洗抓招聘列表页不一定上 Scrapy。如果只是定期拉几百页数据Requests 配合 BeautifulSoup 更轻单文件就能调试加 Cookie、加延时都方便。Scrapy 的调度和下载中间件能力强但前期配置链长数据量没到分布式级别时反而拖慢节奏。我的习惯是先拿 Requests 快速验证字段等需要做大规模增量抓取时再考虑迁移到 Scrapy。2.1 用 Requests 抓取列表页并抽取职位字段下面函数从招聘列表页抓取每页的职位卡片把需要的字段先原样塞进 dict。不同平台的 DOM 结构差别大job-card、job-name这类选择器要按实际页面调整先用浏览器的“检查元素”找到对应节点再写选择器。import requests from bs4 import BeautifulSoup 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_job_list(page1): url fhttp://your-job-site/jobs?page{page} resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 # 乱码时换成 resp.apparent_encoding soup BeautifulSoup(resp.text, html.parser) rows [] for card in soup.select(.job-card): rows.append({ title: card.select_one(.job-name).text.strip(), company: card.select_one(.company-name).text.strip(), salary: card.select_one(.salary).text.strip(), city: card.select_one(.area).text.strip(), experience: card.select_one(.exp).text.strip(), education: card.select_one(.edu).text.strip(), description: card.select_one(.desc).text.strip(), }) return rows这段函数只做两件事请求页面、用select/select_one抽取节点。resp.encoding写成 utf-8 是为了演示很多站点返回的是 gbk抓下来发现乱码时把它替换成resp.apparent_encoding让程序自动识别。timeout10防止某个页面卡住拖慢整个循环。description字段如果列表页没有需要进入详情页再抓一次不要拿空文本做后面的技能分析。2.2 清洗空值、重复值与空白字符抓到的数据要先过一遍 Pandas。字段缺失、整行重复在早期抓取里很常见在进入统计之前先把明显有问题的行处理掉。import pandas as pd rows [] for page in range(1, 4): rows.extend(fetch_job_list(page)) df pd.DataFrame(rows) df df.dropna(subset[title, company, salary]) df[title] df[title].str.replace(r\s, , regexTrue) df df.drop_duplicates(subset[title, company, salary]) print(df.shape, df[salary].head())dropna只对 title、company、salary 三个关键字段生效description缺失不影响整行保留后面统计技能时用fillna()兜底即可。岗位名里的空格是制表符和多个空格混出来的用正则\s一次清掉。去重时不用 title 单独判断而是 title company salary 三个字段组合避免同一公司在不同城市挂相同岗位被误删。2.3 薪资区间拆分与岗位归一化字段规整的关键一步是把薪资、经验这类文本变成数值型。下面是常见的源文本和统一后的存储方式原始字段常见文本统一后的字段处理说明salary10k-15k / 15-20K / 面议salary_low, salary_high去掉 k/K 后缀按-拆成两个数值面议无法解析就置 NaN 后剔除city上海·浦东新区city只保留“·”前的城市名便于按市聚合experience经验不限 / 3-5年exp_low, exp_high提取最小年数和最大年数不限可以统一填 0titleJava开发工程师 / 高级Java后端job_type不保留完整名称映射成“Java”“Python”等岗位族用 Pandas 解析 salary 字段时文本里经常混着“k”和“K”先做lower()再替换掉 “k”正则只要匹配数字部分就可以。df[salary] df[salary].str.lower().str.replace(k, , regexFalse) df[salary_low] df[salary].str.extract(r(\d\.?\d*))[0].astype(float) df[salary_high] df[salary].str.extract(r-(\d\.?\d*))[0].astype(float) df df.dropna(subset[salary_low, salary_high])extract的第一个参数是匹配数字的正则\d\.?\d*可以兼容“15k”和“15.5k”这类写法。只要字符串里没有-salary_high 就是 NaN说明薪资写死没给区间这类样本会被dropna清掉。后面画箱线图时只保留区间薪资会影响整体分布所以还要单独统计一下“面议”占比解读结果时心里有数。岗位名归一化直接落一个映射函数def job_type_of(title): for key in [数据分析, Java, Python, 前端, 算法, 测试, 运维]: if key in title: return key return 其他 df[job_type] df[title].map(job_type_of)这个映射把“Java开发工程师”和“高级Java后端”归到同一类先归并再做统计Top 岗位排名才有意义。关键词顺序有讲究我把“数据分析”放在“Java”前面避免“数据分析师”这种长词被后面更短的“数据”截胡。如果市场上“Golang”这类词多往列表里追加即可。3. 热门岗位与技能频次统计Pandas 聚合与文本分词标签化和清洗完成以后数据已经是一张规整表。下一步做两个统计岗位需求量排序、技能关键词频次。前者用groupby几行能出来后者需要处理非结构化文本。3.1 岗位需求 Top N 与城市交叉岗位需求量本质是计数。先按job_type分组再用size()统计每类有多少条职位排序后取 Top 10。为了保留城市维度同一张表还可以用crosstab做交叉表看每个城市对哪类岗位的需求更旺。job_rank (df.groupby(job_type) .size() .rename(count) .sort_values(ascendingFalse) .head(10)) print(job_rank) city_job pd.crosstab(df[city], df[job_type]) city_job.loc[北京].sort_values(ascendingFalse).head(5)groupby(job_type).size()返回的是 Seriesrename(count)给数量列起个名后面存成 DataFrame 或接到图表里都方便。sort_values(ascendingFalse)把数量多的放前面。crosstab输出的每一行是一个城市每一列是一种岗位族想比较两个城市招“算法”的力度直接取列对比即可。这种统计只是计数没有考虑职位发布时长。有的职位挂了几个月一直在招可能代表流动性高而不是需求量大。更贴近真实需求的做法是记录采集页上的发布日期把超过 30 天的职位排除只统计最近一个月发布的有效岗位。3.2 技能关键词频次统计词表匹配优先于通用分词职位描述属于非结构化文本。通用分词器比如 jieba在招聘场景下容易把技能词拆碎“Spring Cloud”会被分成“Spring”和“Cloud”“C”直接丢失。我一般会先维护一份技能词表用str.contains在 description 里做词表匹配命中规则简单且结果可解释之后再配合自定义词典给分词器补词。skill_list [Python, Java, Golang, Spring, MySQL, Redis, Kafka, Docker, Kubernetes, Hadoop] skill_counts {} for skill in skill_list: skill_counts[skill] int( df[description].fillna().str.contains(skill, caseFalse).sum() ) skill_series pd.Series(skill_counts).sort_values(ascendingFalse) skill_series.head(10)str.contains逐条判断 description 里是否有技能词返回布尔序列sum()相当于统计命中职位数。caseFalse让 Python 和 python 都算命中。fillna()只对 description 缺失的行生效不会影响计数。注意这里统计的是“多少个职位提到了 Golang”而不是“Golang 这个单词出现了多少次”两者反映的倾向不同词云适合用前者的频次。想让分词结果更准可以给 jieba 加用户自定义词典把“Spring Cloud”“Docker”当作整体词加入这样jieba.lcut就不会拆开。我的习惯是两种方案同时跑词表匹配结果用于词云分词结果用于主题归类两边对不上的词汇再去人工质检。3.3 生成词云与字体坑点词云是展示技能热度的直观图表。生成前不用把原始文本直接丢进去而是按命中次数把词汇复制 N 遍拼成一个空格间隔的长字符串。from wordcloud import WordCloud word_text .join([f{skill} * count for skill, count in skill_series.items()]) wc WordCloud(font_pathC:/Windows/Fonts/msyh.ttc, width1200, height800, background_colorwhite, max_words60, colormapviridis).generate(word_text) wc.to_file(skill_cloud.png)font_path必须指定中文字体文件Windows 上用msyh.ttc微软雅黑Linux 上用/usr/share/fonts/下的wqy-microhei.ttc否则中文技能词显示成方块。词表里全是英文技能词时这个问题看不出来一旦加“大数据”“分布式”这类中文词会立刻暴露。max_words控制词云中的词条数量习惯设 50~100太多视觉上会糊成一团。4. 薪资分布与城市需求可视化从直方图到地理热图观察薪资不能只看平均值。招聘网站上的薪资是区间文本解析后得到 low 和 high 两个边界先取中值再配合分布图看全貌。4.1 薪资中位值与直方图用salary_low和salary_high取均值得到该职位的薪资估算值。然后describe看分位数再用直方图观察分布形态。df[salary_mid] (df[salary_low] df[salary_high]) / 2 df[salary_mid].describe() df[salary_mid].plot.hist(bins30, edgecolorwhite)describe()的结果里有 count、mean、std、min、25%、50%、75%、max。先看 50% 而不是 mean是因为个别“50k 以上”的职位会把平均值拉高中位数能反映大多数人的薪资水平。直方图bins的取值影响分布展示的平滑度数据量小的时候设 20数据量大时设 30~50观察分布是左偏还是右偏。招聘薪资分布一般是右偏的长尾在右侧符合多数行业“高薪岗位占比小”的直觉。4.2 岗位薪资箱线图与城市需求条形图箱线图比直方图更适合对比多个岗位它同时展示中位数、四分位距和离群点。清洗后的数据用 seaborn 一行代码能画出来。import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [Microsoft YaHei] plt.figure(figsize(12, 6)) sns.boxplot(datadf, xjob_type, ysalary_mid) plt.xticks(rotation45) plt.tight_layout() plt.savefig(salary_by_jobtype.png)注意“面议”薪资已被过滤这部分职位的薪资通常是隐性的箱线图只反映明确标出薪资范围的数据。数据清洗不干净时离群点会特别多比如“5k-5k”这种被拆成 low5、high5 的职位在图上会变成一条细横线代表该岗位薪资没有区间要回到源数据判断是否合法还是字段解析出了问题。城市需求用条形图更清楚top_cities df[city].value_counts().head(15) top_cities.plot.barh() plt.tight_layout() plt.savefig(top_cities.png)value_counts返回城市出现次数降序排列取前 15 个。横向条形图方便读城市名长度代表职位数。如果此时发现城市里混着“上海·浦东新区”这种带区域后缀的文本说明 2.3 里的 city 清洗没做干净回去处理再重跑绘图脚本。4.3 地理热图的落地写法给定城市和需求数量地理热图比条形图更直观。用 pyecharts 的 Geo 组件是常见做法城市坐标包在组件内部传入city_count字典即可。from pyecharts.charts import Geo from pyecharts import options as opts city_count df[city].value_counts().to_dict() geo Geo().add_schema(maptypechina) geo.add(岗位需求, data_pairlist(city_count.items()), type_effectScatter) geo.set_global_opts(title_optsopts.TitleOpts(title各城市招聘需求热力分布)) geo.render(city_demand.html)Geo 组件对 pyecharts 版本有要求老版本直接用新版本需要额外安装地图文件。真出问题又不方便装包时我通常降级成普通条形图城市只有十几个的时候条形图的数值差异比散点图更明显。地理热图的价值在于一眼看到区域聚集效应比如长三角、珠三角、京津冀三个圈层有没有拉开用它看最直接。4.4 用 Plotly 输出可交互的 HTML 图表分享给招聘或业务同事时对方不一定有 Python 环境。Plotly 直接输出 HTML 文件浏览器打开即用还自带悬浮数字和缩放。import plotly.express as px fig px.bar(top_cities.reset_index(), xcount, ycity, orientationh) fig.write_html(city_demand_interactive.html)px.bar接收 DataFramex 必须是数值列y 是城市列orientationh让条形横向排布。写出来的 HTML 是自包含文件不需要对方解释怎么跑代码、装库。如果数据带时间维度px.line也能做动态折线交互组件的写法类似把 x 换成日期列即可。5. 增量更新与趋势判断按日快照做岗位热度变化招聘分析做成一次性脚本意义不大更好的方式是每天定时抓一次把当天数据落成快照文件后续做历史对比。from pathlib import Path snap_dir Path(snapshots) snap_dir.mkdir(exist_okTrue) today pd.Timestamp.today().strftime(%Y%m%d) df.to_csv(snap_dir / fjobs_{today}.csv, indexFalse)snapshots 目录下保留每天的 CSV。文件名带日期既方便回头查某天的原始数据也方便按文件顺序读取历史记录。比较两日变化时先要处理同一职位跨天重复的问题。同一职位在平台上挂了三天历史表里就会多出两行。csv_files sorted(snap_dir.glob(jobs_*.csv)) history pd.concat([pd.read_csv(f) for f in csv_files], ignore_indexTrue) history history.drop_duplicates(subset[title, company, city])去重组合用 title company city同一岗位在不同城市同时招聘不会因为 company 相同被误删。concat时如果某天新增了字段历史表的列会不一致需要在合并前统一列集合否则后面的聚合会报 KeyError。基于历史表统计每个job_type的日增量可以看哪类岗位热度正在起来today_cnt history[history[date] 20241210].groupby(job_type).size() yesterday_cnt history[history[date] 20241209].groupby(job_type).size() delta (today_cnt - yesterday_cnt).sort_values(ascendingFalse)代码里的 date 列是抓取日期实际应该用今天的日期字符串和昨天的日期字符串动态拼接写死是为了示意。两天差值受抓取波动影响很大比如平台周末更新频率低、周一猛涨全是正常现象。想减少这类噪音用滚动 7 日均值代替单日差把历史表按日期和岗位分组再做移动平均。daily history.groupby([date, job_type]).size().unstack(fill_value0) trend daily.rolling(7, min_periods3).mean()每天 append 一条快照等数据积累到两周以上趋势线就开始有意义。判断岗位热度不要只看数量把趋势和salary_mid放在同一张图里薪资上涨伴随需求增加这个信号才更值得关注。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询