news 2026/10/12 19:21:46

招聘数据分析实战:从爬虫到可视化的完整流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
招聘数据分析实战:从爬虫到可视化的完整流程

简介:面向Python爬虫、数据分析与可视化初学者,以及准备求职大数据岗位的毕业生,这份资源完整实践了从Boss直聘采集北京地区大数据岗位信息、数据清洗聚合、统计分析与可视化展示的全流程。压缩包共111个文件,大小6.58MB,包含4个Python源码文件、xls/csv数据源、db数据库,以及可视化展示页面的html/css/js等前端资源,目录结构清晰,便于按爬虫、处理、展示三个模块对照学习。目前已有727人学习下载。学习者可掌握requests/Scrapy爬虫、BeautifulSoup网页解析、pandas清洗聚合等核心技能,并借助matplotlib、seaborn或前端图表呈现薪资区间、岗位数量、经验要求等分布趋势。资源自带可直接运行的数据与脚本,省去重复采集流程,适合作为课程设计、毕业项目或数据分析入门练手,实战价值高。

1. 北京大数据岗位招聘数据分析:爬虫、清洗到可视化的完整闭环

这个项目我在本地跑通的时候第一反应是“它比大多数教学 Demo 都完整”。它不是那种只给你一个 Jupyter Notebook 让你点点运行就出图的演示,而是从 Boss 直聘抓取北京市大数据相关岗位的招聘信息开始,一路走到数据清洗、统计分析、可视化展示的完整项目。换句话说,你拿到的不只是代码,是一整条“从网页到图表”的生产链路。

它的核心价值在于:第一,爬虫部分不是玩具级的单页抓取,而是带请求头伪装、翻页逻辑和网页解析的真实采集脚本;第二,数据处理直接面对招聘网站最常见的脏数据——薪资写成“20-40K·14薪”、经验要求写成“3-5年”、学历写成“本科”这种非结构化文本;第三,可视化输出覆盖了静态图表和交互式图表两种形态。

适合谁?正在学 Python 数据分析、想找一份能写进简历的综合实战项目的人,以及已经能写简单爬虫但不知道怎么把数据做成分析结论的从业者。你不需要先会大数据框架,pandas + matplotlib + pyecharts 这套组合足够支撑整个项目跑完。

2. 招聘数据爬虫:requests 加 BeautifulSoup 抓取 Boss 直聘的关键实现

2.1 请求构造:为什么不能直接 requests.get

Boss 直聘的页面结构不算特别复杂,但它对请求头的校验比一般站点严格。直接用 requests.get 不带任何请求头去访问,大概率返回一个安全验证页面,HTML 里根本没有职位列表数据。

这个项目里的爬虫思路是:先手动从浏览器复制一份完整的请求头,重点是 User-Agent、Referer 和 Cookie 三个字段。User-Agent 用来伪装成真实浏览器,Referer 告诉服务器你是从搜索页跳转过来的,Cookie 则是维持会话状态的关键——Boss 直聘的搜索接口依赖 Cookie 里的登录态和会话标识。

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.0.0 Safari/537.36", "Referer": "https://www.zhipin.com/web/geek/job?query=大数据&city=101010100", "Cookie": "你的浏览器 Cookie,建议从实际登录后的页面复制" } url = "https://www.zhipin.com/web/geek/job" params = { "query": "大数据", "city": "101010100", # 北京的城市编码 "page": 1 } resp = requests.get(url, headers=headers, params=params, timeout=10) resp.encoding = "utf-8" print(resp.status_code, len(resp.text))

这段代码的逻辑是先构造一个带完整请求头的 GET 请求,然后把搜索关键词和城市编码放在 query 参数里。city=101010100 是北京的城市编码,这个值是从 Boss 直聘前端页面的请求参数里抓出来的。timeout=10 是超时保护,防止某个请求卡住导致整个爬虫停摆。

拿到响应后先打印 status_code 和文本长度,这是爬虫调试的第一步:如果状态码是 200 但文本长度只有几千字符,基本就是被安全策略拦截了,返回的是验证页。

2.2 解析职位列表:把 HTML 变成结构化字段

拿到 HTML 之后,需要定位职位卡片所在的 DOM 节点。Boss 直聘的职位列表在 class 为 job-card-wrapper 的 div 里,每条职位卡片包含职位名称、薪资、公司名、城市、经验学历要求等字段。用 BeautifulSoup 的 find_all 方法可以一次取出所有卡片,再逐个提取字段。

soup = BeautifulSoup(resp.text, "html.parser") cards = soup.find_all("div", class_="job-card-wrapper") for card in cards: job_name = card.find("span", class_="job-name").text.strip() salary = card.find("span", class_="salary").text.strip() company = card.find("div", class_="company-name").text.strip() # 经验、学历、城市在 job-card-footer 里的 ul 标签下 footer_li = card.find("ul", class_="tag-list").find_all("li") tags = [li.text.strip() for li in footer_li] print(job_name, salary, company, tags)

这里有一个很容易踩的细节:经验、学历、城市这些信息不在同一个 class 里,它们在职位卡片底部的一个无序列表里,标签可能包含“3-5年”“本科”“北京·海淀区”等信息。用 find_all("li") 提取后,再根据关键词做分类,比如包含“年”的字段归为经验,包含“本科”或“硕士”的归为学历。

解析完成后,每一行数据就是一个职位的结构化记录。把这些记录追加到列表里,最后统一转成 DataFrame,比边爬边写 CSV 要高效得多。

2.3 翻页与多关键词抓取:控制节奏才能拿全数据

Boss 直聘的搜索结果最多只能看到前几页,这是平台的反爬限制。项目里处理翻页的方式是循环修改 params 里的 page 参数,同时每次请求之间加一个随机延时,模拟人类操作节奏。

import time import random all_jobs = [] for page in range(1, 11): params["page"] = page resp = requests.get(url, headers=headers, params=params, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") cards = soup.find_all("div", class_="job-card-wrapper") if not cards: print(f"第 {page} 页无数据,停止抓取") break for card in cards: # 提取字段逻辑同上一节 pass all_jobs.extend(cards_data) time.sleep(random.uniform(2, 5))

翻页逻辑的核心是“没数据就停”,不要盲目爬满 10 页。因为平台对翻页深度有控制,超过一定页数后返回的卡片列表是空的,此时继续请求只会增加被封风险。random.uniform(2, 5) 是每页之间 2 到 5 秒的随机等待,这个间隔在多数招聘网站上是安全的。

我的习惯是先把单页解析跑通、字段确认无误后再加循环,否则翻页时如果解析报错,很难判断是页面结构变了还是翻页参数错了。

3. 数据清洗与预处理:薪资文本拆分、缺失值处理和字段规整

3.1 薪资字段是所有坑的源头

从网页上直接抓下来的薪资字段长这样:“20-40K·14薪”“10-15K”“8千-1.2万”。这些文本没法直接参与计算,需要拆成数字。项目里的处理方式是用正则表达式提取数字范围,再统一单位。

import re def parse_salary(salary_text): if not salary_text: return None, None, None # 先统一单位:将千转成 K,万转成 K text = salary_text.replace("千", "K").replace("万", "K") nums = re.findall(r"(\d+\.?\d*)", text) if len(nums) == 1: # 只有单个值,比如 "20K",取上下限相同 low = high = float(nums[0]) elif len(nums) >= 2: low = float(nums[0]) high = float(nums[1]) else: return None, None, None # 如果原始文本里出现 "万",需要乘以 10 转成 K if "万" in salary_text: low, high = low * 10, high * 10 return low, high, salary_text

这个函数的处理逻辑分三步:先把“千”“万”统一成 K,然后提取所有数字,最后根据数字个数判断是区间还是单值。这里有个细节值得注意:如果原始文本里写了“万”,提取出来的数字是以“万”为单位的,必须先乘以 10 转成 K,否则 8千-1.2万 会被错误解析成 8 到 1.2。转成 K 之后在后续分析里统一用 K 作单位,计算均值时就不会出现量级混乱。

解析完成后,把 low 和 high 存成两个新列,还可以直接算出中位数作为“平均薪资”的替代指标。招聘数据里薪资大多是一个范围,直接用上限或下限都偏离真实情况,用 (low + high) / 2 作为单点值做排序和对比最稳妥。

3.2 经验与学历字段的文本规整

经验和学历字段抓下来后也不干净。“3-5年”这种格式是混在标签列表里的,有的职位写“经验不限”,有的写“在校生/应届生”,学历字段会出现“本科”“硕士”“学历不限”等。处理方式是把文本映射成统一的等级数字,方便后续排序和分组。

exp_map = { "经验不限": 0, "在校生/应届生": 1, "1年以内": 1, "1-3年": 2, "3-5年": 3, "5-10年": 4, "10年以上": 5 } def parse_experience(tags): for tag in tags: if "经验" in tag or "在校生" in tag or "应届生" in tag: return exp_map.get(tag, -1) return -1

这是一个典型的“有限字段枚举映射”思路,先把网站可能出现的所有经验值收集齐,然后建一个映射字典。为什么不用正则直接提取数字?因为“经验不限”“在校生/应届生”这类文本里没有数字,正则提取不到。把字段映射成语义化的等级数字后,pandas 的 groupby 就可以按等级排序展示,图表轴上的顺序也是对的。

学历字段同理,建立“大专”:1、“本科”:2、“硕士”:3、“博士”:4 的映射,学历不限单独作为 0。

3.3 缺失值处理与第一轮统计分析

抓下来的原始数据会有各种缺失:有的职位没写薪资、有的公司名称为空。pandas 的 dropna 不能直接乱用,因为删除整行会导致样本量大幅缩水。我的处理习惯是先看各列缺失比例,如果某列缺失超过 30%,就考虑这列不参与分析;如果只是个别行缺失,才用 dropna 或填充默认值。

import pandas as pd df = pd.DataFrame(all_jobs) df["salary_low"] = df["salary"].apply(lambda x: parse_salary(x)[0]) df["salary_high"] = df["salary"].apply(lambda x: parse_salary(x)[1]) df.loc[df["salary_low"].isna(), "salary_low"] = df["salary_low"].median() df.loc[df["salary_high"].isna(), "salary_high"] = df["salary_high"].median() # 经验字段映射 df["exp_level"] = df["tags"].apply(parse_experience) # 第一轮统计:按经验等级分组,看职位数量和平均薪资 stats = df.groupby("exp_level").agg( 职位数量=("job_name", "count"), 平均薪资下限=("salary_low", "mean"), 平均薪资上限=("salary_high", "mean") ).reset_index() print(stats)

这段代码里有个关键的 pandas 用法:groupby 配合 agg 传入多个聚合函数,一次性算出职位数量和薪资均值。apply 配合 lambda 把每一行的薪资文本拆成上下限,拆出来是 None 的用整列中位数填充,避免后续计算出现空值。

缺失值处理的原则是:能用中位数填充就不用均值,因为薪资分布是右偏的,个别超高薪职位会把均值拉高,中位数更能代表整体水平。

4. 数据可视化展示:从统计数字到可读图表的完整过程

4.1 matplotlib 静态图表:先让数据分布说话

数据分析的产出不能只是一张统计表,图表才是给非技术角色看的核心交付物。项目里用 matplotlib 输出的第一张图是“不同经验要求的职位数量分布”和“薪资区间箱线图”,这两张图配合起来能回答一个招聘市场最常被问到的问题:什么经验档位的岗位最多、薪资涨得最快。

import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] # 解决中文乱码 plt.rcParams["axes.unicode_minus"] = False # 解决负号显示异常 fig, ax = plt.subplots(1, 2, figsize=(14, 6)) # 左图:经验等级 vs 职位数量 ax[0].bar(stats["exp_level"].astype(str), stats["职位数量"], color="#4C72B0") ax[0].set_title("不同经验要求的大数据岗位数量分布") ax[0].set_xlabel("经验等级") ax[0].set_ylabel("职位数量") # 右图:经验等级 vs 平均薪资 ax[1].plot(stats["exp_level"].astype(str), stats["平均薪资下限"], marker="o", label="薪资下限") ax[1].plot(stats["exp_level"].astype(str), stats["平均薪资上限"], marker="s", label="薪资上限") ax[1].set_title("不同经验等级的平均薪资区间") ax[1].set_ylabel("平均薪资(K)") ax[1].legend() plt.tight_layout() plt.savefig("salary_by_experience.png", dpi=200) plt.show()

两个图表放在同一张画布上,用 subplot 分成左右两列,左侧看岗位供需、右侧看薪资变化。这里最容易翻车的点是中文乱码,Windows 下需要指定 SimHei 或微软雅黑字体,Mac 下则可以用 PingFang SC。如果画出来的图全是方块,优先检查字体配置而不是数据。

4.2 pyecharts 交互式图表:地图、词云与动态报表

静态图表适合嵌入报告,但项目里“可视化展示”这个标题意味着还需要能交互的页面级图表。pyecharts 是实现这个目标的直接方案,它生成的 HTML 文件可以在浏览器里打开,支持鼠标悬停显示数值、缩放平移。

from pyecharts.charts import Map, WordCloud from pyecharts import options as opts # 北京各区岗位数量分布地图 areas = df["area"].value_counts().reset_index() area_map = Map() area_map.add( "北京大数据岗位分布", [list(z) for z in zip(areas["area"], areas["count"])], maptype="北京" ) area_map.set_global_opts( title_opts=opts.TitleOpts(title="北京市大数据岗位区域分布"), visualmap_opts=opts.VisualMapOpts(max_=areas["count"].max()) ) area_map.render("beijing_job_map.html")

代码里用 value_counts 统计各区域出现的频次,然后转成 pyecharts 需要的 [区域名, 数值] 列表格式。maptype 参数指定了“北京”地图,这个需要 pyecharts 的地图数据包支持,新版本会自动下载。

另外还有一个交互式的词云图,从职位名称里提取关键词,把“大数据”“数据分析”“Hadoop”“Spark”这些高频词按出现频率渲染成词云。词云图在汇报场景里很讨喜,能一眼看出市面上需求量最大的技能方向。

4.3 把图表拼成一份可视化看板

项目里没有用 Flask 或 Django,而是采用了更轻量的方案:把多个 pyecharts 生成的 HTML 文件通过一个总页面聚合在一起,用 iframe 嵌套或者直接链式渲染。这样做的优点是零后端依赖,双击就能打开,分享给同事也方便。

如果你想把可视化做得更完整,可以把统计表、柱状图、地图、词云全部串到一个 HTML 里,用 bootstrap.css 和 animate.css 做页面布局和动效,这也是项目里带这些前端资源文件的原因。

5. 避坑与常见问题:这套项目里最容易翻车的四个环节

5.1 拿到 200 状态码却解析不到数据

  • 现象:请求返回状态码 200,但 BeautifulSoup 找 job-card-wrapper 时结果为空。
  • 原因:Boss 直聘的安全策略在某些情况下会返回一个带验证逻辑的页面,状态码仍然是 200,但 body 里不是职位列表数据,而是验证脚本或提示文案。
  • 解决:打印响应文本的前 500 个字符,检查是否包含“验证”或“安全”关键词。如果确认被拦截,停止程序,等 30 分钟以上再跑,同时检查 Cookie 是否过期。更稳妥的做法是先用浏览器打开目标页手动搜索一次关键词,让服务器认为你是一个活跃用户,再重新复制 Cookie 启动爬虫。

5.2 薪资字段拆出负数

  • 现象:parse_salary 之后的 salary_low 出现负数或异常小值。
  • 原因:薪资文本里出现了“15-25K·15薪”这种结构,正则提取数字时把后面的“15薪”的 15 也提取了,导致高低区间错位。
  • 解决:提取数字之前先用 split 按“·”拆分,只处理第一个分段里的内容,也就是“15-25K”部分;薪期信息单独存一列,不参与薪资区间计算。

5.3 岗位数据量刚过百,可视化地图显示空白

  • 现象:区域分布地图上只有零星几个点,或者直接空白。
  • 原因:Boss 直聘的职位卡片里“区域”字段不是统一格式,有的写“北京·海淀区”,有的写“北京市海淀区”,直接用 value_counts 统计时相同区域被拆成多个不同条目。
  • 解决:先对区域字段做归一化,用正则把“北京·”和“北京市”前缀去掉,只保留区名。地图上显示区名才能正确匹配到地理边界数据。

5.4 pyecharts 版本升级后地图文件打不开

  • 现象:用新版 pyecharts 生成的地图 HTML 在浏览器里打开,提示找不到地图数据或加载失败。
  • 原因:pyecharts 从 1.x 升级到 2.x 后地图数据包的引入方式变了,老代码里直接 map.add 的地图名称在新版本里失效。
  • 解决:确认 pyecharts 版本,新版需要先安装 pyecharts-snapshot 或地图扩展包,并在代码里显式注册地图数据。如果不想纠结版本问题,直接锁 pyecharts==1.9.0 搭配 echarts-countries-pypkg 是最省心的组合。这个坑我每次带新人做可视化学员项目时都会遇到,建议拿到项目后先跑一下 requirements 里的版本,不要用最新版硬碰。

5.5 多关键词合并后出现大量重复职位

  • 现象:爬了“大数据”“数据分析”“数据挖掘”多个关键词后,去重后发现同一家公司同一个职位重复出现多次。
  • 原因:同一岗位可能同时匹配多个搜索关键词,Boss 直聘会把同一个职位重复展示在不同关键词的搜索结果里。
  • 解决:以“职位名称 + 公司名 + 薪资区间”三个字段拼接作为唯一键去重,薪资区间用小范围内近似,比如误差在 5% 以内视为同一职位。

6. 进阶:把爬取、清洗、分析和可视化串成一条可复用流水线

项目做到这里已经是一个完整的闭环,但离“可复用”还差一步:现在每一步都是独立脚本,换一个城市或者换一个关键词,需要手动改多处参数。我会把它拆成一个 config.py 加三个模块,形成一条可复用的流水线。

config.py 里放城市编码、搜索关键词列表、请求头模板、延时区间和输出路径。爬虫模块读取这些配置,把抓到的数据统一落成 CSV;清洗模块负责把薪资、经验、学历字段解析成结构化数据;分析模块输出统计汇总表和图表。改城市只需要改一个配置文件,不需要动任何逻辑代码。

# config.py CITY_CODE = "101010100" # 北京 KEYWORDS = ["大数据", "数据分析", "数据挖掘", "Hadoop"] OUTPUT_CSV = "output/bj_jobs.csv" SLEEP_INTERVAL = (2, 5) # 流水线入口 main.py from crawler import crawl from cleaner import clean from analyzer import analyze if __name__ == "__main__": raw_df = crawl(CITY_CODE, KEYWORDS) clean_df = clean(raw_df) analyze(clean_df)

清洗模块里可以加一层更细的质检逻辑:检查薪资字段解析成功率是否低于 90%、学历映射失败的行数是否超过阈值,超过就打印警告日志。这一步是防止数据质量劣化的最后一道闸门。

如果是自己做招聘市场调研这类周期性需求,这套流水线可以直接定时触发。把 main.py 挂到 cron 或 Windows 计划任务里,每周跑一次,自动生成最新的市场薪资报告。比起每次手动改参数重新爬,这个模式才是项目从“能跑”走向“能用”的分水岭。

我自己在跑这个项目时最深的体会是:爬虫不是这个项目的难点,清洗才是。前端页面改一次结构,解析代码就得跟着改;薪资文本换一种写法,正则又得调。所以我现在做这类项目,第一件事不是写爬虫,而是先手动收集 20 条真实页面数据,把所有文本规律列出来,再开始写解析函数——这个习惯帮我避开了大半的后续返工。希望帮到你。

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

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

AI编程的入口文件——CLAUDE.md的正确写法与TaoToken统一Key配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/12 19:15:33

MineCarWithPeople数据集实战指南:格式校验、划分与训练调优

简介:MineCarWithPeople-yolo-data数据集面向矿业自动化与计算机视觉开发者,聚焦轨道上矿车及人员检测任务,采用YOLO格式组织,适合用于训练和评估实时目标检测模型。压缩包共2000个文件,包含1045张jpg现场图像、954个t…

作者头像 李华
网站建设 2026/10/12 19:13:14

双向广搜实战:从Solitaire题解到通用模板

1. 从一道经典题说起:双向广搜到底解决什么问题第一次接触“Solitaire”这道题的人,大概率会被它看似简单的规则骗到。棋盘固定是88,上面只放4枚棋子,每枚棋子可以往上下左右四个方向移动一格,如果相邻位置有棋子且再远…

作者头像 李华
网站建设 2026/10/12 19:12:58

Sine之舞递归详解:蓝桥杯字符串拼接与An/Sn规律全解析

Sine之舞这道题,刷过蓝桥杯基础练习的人多少都见过。题目本身不给任何高级算法,就一串sin来回套,却能把不少新手卡在原地一整天。当初我练的时候也栽过跟头,后来把An和Sn的生成规律彻底捋了一遍,发现其实就是“递归规律…

作者头像 李华
网站建设 2026/10/12 19:12:55

Ansible自动化部署Prometheus监控:从设计到实践

1. 项目概述与需求拆解1.1 这个项目的核心是什么简单说一下这个事情的背景。Easypack这个名字你可能不熟,它本质上是一个围绕常用基础组件部署的自动化工具集,底层用Ansible来编排,目标就是让你不用手动一行一敲命令,就能把一套中…

作者头像 李华