☰
Python大数据岗位招聘分析:爬虫采集到可视化展示全流程
2026/11/2 14:45:08 网站建设 项目流程

简介:这是一份基于 Python 的北京市招聘数据分析与可视化实战项目,覆盖网络爬虫、数据清洗、统计分析到前端展示的完整链路。项目以 Boss 直聘为采集目标,演示了 Scrapy 与 requests 等工具在真实网页结构下的应用,并借助 pandas 完成缺失值处理与聚合统计,最终用图表呈现岗位地域、薪资区间等趋势。面向大数据分析与爬虫开发的初学者、进阶者,也适合作为毕业设计或课程项目的参考。压缩包共 111 个文件,大小 6.58MB,主体包含 4 个 py 源码文件、csv/db/xls 格式的原始及清洗数据、html/css/js 构建的仪表盘页面,以及配套图片与样式资源,目录结构清晰便于按模块学习。项目已有 727 人学习,通过阅读完整爬虫逻辑与可视化实现,可直观理解从数据采集到结论产出的每一步,为独立开展类似项目提供可复用的工程思路。

1. 北京市大数据岗位招聘分析:这份源码包把数据、爬虫和可视化串成了什么

做大数据岗位分析,第一步永远不是画图,而是手上有没有一份能吃进 Python 的数据。手工复制招聘信息一百条就累了,网上现成数据集多半过期,这时候一套基于 Python 实现的北京市大数据岗位招聘数据分析及可视化展示项目就派得上用场:爬虫把岗位信息抓下来,清洗成规整表格,再做岗位需求、技能词频、薪资区间这类分析,最后用图表和大屏展示,源码、数据、爬虫三样都在一套流程里。适合大数据专业做毕业设计的人、想在本行摸行情底的求职者,以及需要给团队定薪酬的招聘负责人——三类人拿它当起点都合适,前提是你愿意把数据重新跑一遍,而不是拿现成结论直接抄。

2. 爬虫部分:采集岗位数据的选型与最小实现

2.1 选型:requests+BeautifulSoup 还是 selenium

招聘网站的列表页大致分两类。老一点的站点返回服务端渲染的 HTML,requests 拿页面源码,BeautifulSoup 解析即可;新一些的站点走前后端分离,页面数据藏在接口 JSON 里,直接用 requests 请求接口更省事。这套项目里我优先选 requests + BeautifulSoup,原因很直接:跑得快、依赖少、断点续爬好控制,一台普通笔记本就能在十几分钟内抓完几百条有效岗位数据。

selenium 的适用场景要窄得多。只有当页面数据完全靠 JavaScript 动态渲染、接口又加密到没法直接复现的时候,才需要拉一个浏览器环境去渲染页面。代价是内存占用高、启动慢、中间还可能被浏览器验证弹窗打断。我的建议是先打开开发者工具看 Network 面板,如果找到了返回 JSON 的接口,就完全没必要上 selenium,这是 python爬虫项目里最常见的过度设计。

2.2 最小实现:抓取列表页并提取字段

以招聘网站的搜索接口为例,先写一个请求封装。这里的核心参数是 headers 里的 User-Agent 和 Referer,很多第一次写爬虫的人栽在默认 UA 上,被服务器直接拒绝。

import time import random import requests KEYWORD = "大数据" CITY = "北京" 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://search.example.com/jobs", "Accept": "application/json, text/plain, */*", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch_job_list(page: int = 1): """拉取招聘搜索接口的岗位列表,返回 JSON 字典。""" url = ( f"https://search.example.com/api/joblist" f"?keyword={KEYWORD}&city={CITY}&page={page}" ) resp = requests.get(url, headers=HEADERS, timeout=10) resp.raise_for_status() return resp.json()

这段代码里有两个容易被忽略的参数。timeout=10是给每个请求设上限,避免某个接口卡住时整个脚本停在那里干等;raise_for_status()会在返回 4xx 或 5xx 时直接抛异常,让失败尽早暴露,好过把错误页面当成正常数据存进 CSV。请求头里的Accept按接口实际返回类型写,JSON 接口填application/json,HTML 页面则不需要这一行。

拿到 JSON 之后要做的不是直接存下来,而是把嵌套字段拍平成一行一列的表格结构。招聘数据的常见坑是「技能标签」这一项,接口里是一个数组,直接存会变成 Python 的列表对象,后面没法分析,我用竖线拼成字符串,分析阶段再拆开。

def parse_jobs(payload: dict): """把接口返回的原始 JSON 拍平成 DataFrame 行。""" rows = [] for item in payload.get("data", {}).get("list", []): rows.append({ "job_title": item.get("jobName", ""), # 岗位名称 "company": item.get("brandName", ""), # 公司名称 "salary": item.get("salary", ""), # 原始薪资文本 "location": item.get("area", "") + item.get("subway", ""), "experience": item.get("jobExperience", ""), # 经验要求 "education": item.get("eduLevel", ""), # 学历要求 "skills": "|".join(item.get("skillTagList", [])), # 技能标签 "publish_date": item.get("updateDate", ""), }) return rows

字段名统一成下划线风格,后边所有清洗和分析都用这一套名字,省去来回映射的麻烦。item.get("jobName", "")这种写法保证某个字段缺失时拿到空字符串而不是抛 KeyError,招聘数据里字段缺失太常见了,宁可空了后边统一处理,也不能让一条坏数据中断整个抓取。

2.3 分页、去重与保存策略

搜索接口一般限制最大翻页数在 20 页左右,一页 15 到 30 条,实际上北京大数据岗位翻到第 10 页就已经能看到重复内容了。分页抓取时最需要的不是代码技巧,是请求间隔——连续高频请求会被反爬系统标记,轻则当前 IP 被限流,重则直接返回验证码页面。我一般把间隔设在 1.5 到 3.5 秒之间随机抖动,既能跑完数据,又不至于触发风控。

import pandas as pd all_rows = [] for page in range(1, 11): try: payload = fetch_job_list(page=page) rows = parse_jobs(payload) if not rows: break all_rows.extend(rows) except requests.RequestException as e: print(f"第 {page} 页抓取失败: {e}") continue time.sleep(random.uniform(1.5, 3.5)) # 随机间隔,避免规律性请求 df = pd.DataFrame(all_rows) df = df.drop_duplicates(subset=["job_title", "company", "salary"]) df.to_csv("beijing_bigdata_jobs_raw.csv", index=False, encoding="utf-8-sig")

保存 CSV 时必须用utf-8-sig而不是utf-8,这个编码会写入 BOM 头,Excel 打开才不乱码,后文避坑章节会再强调一遍。drop_duplicates按岗位、公司、薪资三个字段组合判断重复,因为同一家公司同岗位不同招聘渠道会有不同的发布日期,只按标题去重会误删。保存原始数据这一步千万别省,清洗分析翻车时,原始 CSV 就是你回滚的“后悔药”。

3. 字段规整与薪资解析:把脏数据洗成可分析的样子

3.1 先处理空值和重复记录

原始数据落盘之后,不要急着画图,先用df.info()和df.isna().sum()看一眼每列的缺失情况。招聘数据分析的脏数据比例远高于想象:学历留空的、经验写「在校生/应届生」的、薪资写「面议」的,全都要在这一步给定口径,否则后边统计全是黑匣子。

import pandas as pd df = pd.read_csv("beijing_bigdata_jobs_raw.csv", encoding="utf-8-sig") print(df.shape) print(df.isna().sum()) print(df.duplicated(subset=["job_title", "company", "salary"]).sum()) # 删除完全重复的记录,保留首条 df = df.drop_duplicates(subset=["job_title", "company", "salary"])

处理空值的方式分三种。education为空但job_title里有「高级」字样的时候,可以留空不进学历分析;薪资为「面议」或者缺失,直接删掉这部分记录,因为它们进不了薪资分位数计算;skills为空则填充空字符串,技能词频统计时会自动忽略。我的原则是:数值型字段缺失就删除,类别型字段缺失就按未知归类,绝不随便填一个默认值进去。

3.2 薪资文本解析:K、万、薪怎么统一成数值

薪资字段是招聘数据里最想用、也最容易洗错的列。原始文本常见三种形态:「20-30K·14薪」「2-3万/月」「1-1.5万·15薪」。要统一转换成两个数值列:月薪下限、月薪上限,单位都折算成 K。正则里最容易漏的是「万」这个单位,它和 K 差了 10 倍,漏掉之后薪资统计整体偏低一个数量级。

import re def parse_salary(s): """把薪资文本解析成 (月薪下限K, 月薪上限K, 月薪数)。""" if pd.isna(s) or not isinstance(s, str): return None, None, None s = s.strip() match = re.match(r"(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)([Kk万])", s) if not match: return None, None, None low, high, unit = match.groups() if unit.lower() == "k": low_k, high_k = float(low), float(high) else: # 单位是「万」, 折算成K直接乘以10 low_k, high_k = float(low) * 10, float(high) * 10 # 找「N薪」, 没写默认为12薪 months_match = re.search(r"(\d+)薪", s) months = int(months_match.group(1)) if months_match else 12 return low_k, high_k, months df[["salary_low_k", "salary_high_k", "salary_months"]] = ( df["salary"].apply(parse_salary).apply(pd.Series) )

正则(\d+(?:\.\d+)?)同时兼容整数和小数,1-1.5万这种带小数的文本不会被拆错。单位按[Kk万]匹配,一篇文章里 K 和 k 混用的情况很常见。处理「万」的时候乘以 10 而不是乘以 10,000,是因为这里统一以 K 为基准,1 万等于 10K,这样后边算中位数、画箱线图时单位一致。salary_months这个字段一定要保留,后边如果想算年薪才有依据,只留月薪会丢掉 15 薪、16 薪这批高薪岗位的信息。

这个解析函数有一个已知边界:30K以上、20-40万/年这类非标文本会被直接丢弃。我的处理方式是把这些无法解析的文本单独导出到salary_unparsed.csv,人为抽样看一眼占比。占比小于 5% 可以直接忽略,大于 10% 说明正则写得不够全,得补规则。

3.3 经验与学历字段的归类规则

experience 字段原始值是「3-5年」「5-10年」「不限」「应届生」这类文本,直接做分析没法排序。我把它转换成年份区间和中值两个字段,「3-5年」转成 (3, 5, 4),「不限」转成 (0, 99, 0)。这样既能做区间分布,又能算平均经验年限。

def parse_experience(x): if pd.isna(x) or not isinstance(x, str): return None, None, None x = x.strip() if "不限" in x: return 0, 99, 0 if "应届" in x: return 0, 1, 0.5 m = re.search(r"(\d+)-(\d+)年", x) if m: lo, hi = int(m.group(1)), int(m.group(2)) return lo, hi, (lo + hi) / 2 return None, None, None

学历字段看起来规整,实际上「本科及以上」「统招本科」「硕士/双学士」混在一起。我一般只保留最高学历关键词,用映射表清洗:

EDU_MAP = { "博士": "博士", "硕士": "硕士", "双学士": "硕士", "本科": "本科", "统招本科": "本科", "本科及以上": "本科", "大专": "大专", "专科": "大专", "不限": "不限", } def normalize_education(x): if pd.isna(x) or not isinstance(x, str): return "未知" for key, value in EDU_MAP.items(): if key in x: return value return "其他" df["education_level"] = df["education"].apply(normalize_education)

这里有个顺序细节:映射表里「统招本科」必须排在「本科」前面,因为 Python 字典遍历是有序的,先命中的先返回。如果把「本科」写在前面,遇到「统招本科」会先被「本科」匹配掉,语义上没错,但你会丢失「统招」这一层信息。学历归类宁可粗不能细,最终分析报告里写「本科及以上占比」的时候,口径才站得住。

4. 分析维度与指标口径:岗位需求、技能、薪资怎么算才有说服力

4.1 三个必做维度:需求量、技能词频、薪资区间

清洗完的数据要回答三个问题:哪些公司招大数据岗位招得多、岗位都要求什么技能、薪资集中在什么区间。这是招聘数据分析项目里最经典的三板斧,也是招聘决策时最常被追问的三组数据。

维度一,岗位需求量按公司分组计数,筛出用人需求最旺盛的公司。维度二,把 skills 列按竖线拆开做词频统计,得到技能需求排行,这个排行比任何简历模板都诚实,因为它是企业在招聘广告里实际写出来的要求。维度三,用salary_mid = (salary_low_k + salary_high_k) / 2算出每个岗位的中位月薪,再按学历、经验分组看薪资分布。

4.2 分组聚合与描述性统计的代码落地

分析之前先补两个派生列:月薪中值和经验中值。然后按学历分组看薪资,这是项目里最核心的代码块。

# 派生两个数值列用于聚合 df["salary_mid"] = (df["salary_low_k"] + df["salary_high_k"]) / 2 df["exp_mid"] = df["experience"].apply( lambda x: (int(x.split("-")[0]) + int(x.split("-")[1].replace("年", ""))) / 2 if isinstance(x, str) and "-" in x else 0 ) # 按学历分组:岗位数、月薪中位数、平均经验 edu_pivot = df.groupby("education_level").agg( job_count=("job_title", "count"), salary_median=("salary_mid", "median"), exp_mean=("exp_mid", "mean"), ).sort_values("job_count", ascending=False) print(edu_pivot) # 按经验区间分组看薪资 exp_bins = [0, 1, 3, 5, 10, 99] df["exp_band"] = pd.cut(df["exp_mid"], bins=exp_bins, right=False) exp_salary = df.groupby("exp_band", observed=False)["salary_mid"].median() print(exp_salary)

groupby().agg()是这一步的核心,agg里可以同时算多个指标的写法,比groupby后逐个mean()再合并要清晰得多。pd.cut把经验切成 0-1、1-3、3-5、5-10、10 年以上五档,right=False表示区间左闭右开,避免 3 年经验同时落在「1-3」和「3-5」两个区间里。

这段代码有个容易翻车的地方:exp_mid的 lambda 里用了x.split("-"),如果前面步骤没把「不限」「应届生」处理成数值,这里会直接把字符串拆出脏数字。所以经验字段的解析必须在分析前完成,任何半成品的文本字段进入机械运算都会悄悄变成垃圾值。

4.3 指标口径:为什么用中位数不用平均值

薪资统计里,中位数比平均值诚实得多。这个项目的数据里偶尔会出现「50-80K·16薪」的架构师岗位,直接把平均值拉高两三万,让整份报告失真。平均值对极端值敏感,中位数则稳定反映“一个普通的北京大数据岗位能拿到多少钱”。对外汇报的时候,我会同时给 P25、P50、P75 三个分位数,招聘方关心的是中位数,求职者关心的是 P75——那是跳一跳够得着的薪资。

quantiles = df["salary_mid"].quantile([0.25, 0.5, 0.75]) print("薪资分位数(K/月):", quantiles.to_dict())

4.4 保存中间结果,给可视化留好输入

分析结果要落盘成中间表格,可视化阶段直接读,不用重跑计算。我一般会把三份中间结果分别导出:清洗后的明细数据、学历薪资透视表、技能词频统计。这样做的好处是,可视化调试调颜色、调布局的时候,不需要每次重复执行前面的爬虫和数据清洗。

df.to_csv("beijing_bigdata_jobs_clean.csv", index=False, encoding="utf-8-sig") edu_pivot.to_csv("analysis_edu_salary.csv", encoding="utf-8-sig") skill_counter = Counter() for tags in df["skills"].dropna(): for tag in str(tags).split("|"): tag = tag.strip() if tag: skill_counter[tag] += 1 skill_top = skill_counter.most_common(15)

Counter的most_common(15)只取前 15 个技能,后边词云图和柱状图都用这份数据,避免把长尾低频技能画成密密麻麻一团。中间结果文件名带上前缀analysis_,和原始数据区分开,这是个人表达习惯,也是给第二个接手项目的人留的线索。

5. 避坑:爬虫反爬、薪资清洗和可视化的五个翻车点

5.1 第二页开始全部 403:反爬不只是频率问题

现象:第一页数据正常,翻到第二页开始请求失败,返回 403 Forbidden,换 IP 重试还是一样。

原因:招聘网站的搜索接口普遍做了分页签名,第二页及以后的请求需要带上前一页响应里下发的 token 或签名参数。直接改page参数是模拟不出真实翻页行为的,请求头里缺这个动态参数,风控系统一验一个准。

解决:先不要急着加代理、降频次,打开浏览器开发者工具,手动翻页观察请求 URL 的变化。常见做法是 URL 里多了sign、token这类参数,那就得先用第一页请求拿响应里的签名值,拼到下一页的请求参数里。我在项目里用的是简单方案:请求前先 GET 一次列表首页拿到payload["data"]["cursor"],翻页时带上这个值。

5.2 薪资字段「15-25K·15薪」被拆丢

现象:清洗后检查发现,一大批高薪岗位的salary_low_k、salary_high_k是空值,岗位数量凭空少了三分之一。

原因:正则只写了\d+-\d+K的匹配规则,遇到「15-25K·15薪」时,·后面的内容不影响开头匹配,问题不在这;真正翻车的是「薪资面议」「15-25K·15薪」这类文本里如果混入了全角字符「·」和「-」以外的符号,匹配就被打断了。更隐蔽的坑是「1-1.5万/月」里数字是小数,而正则漏掉了小数部分。

解决:按 3.2 节的正则升级为(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)([Kk万]),并且每次清洗后输出未解析名单。我习惯 debug 时写一行not_parsed = df[df["salary_low_k"].isna()]["salary"],把它存成独立文件看一眼比例。这套血泪经验的核心就是:清洗函数写完之后先跑一遍,打印解析覆盖率,低于 95% 就停下来补正则,不要直接往后做分析。

5.3 CSV 用 Excel 打开一片乱码

现象:df.to_csv("xxx.csv")之后用 Excel 打开,中文全变成「锟斤拷」或拼音乱码,但用记事本打开是正常的。

原因:pandas 默认保存编码是 utf-8,而 Excel 打开无 BOM 的 utf-8 文件时按本地化编码(GBK)解析,中日韩字符全部错乱。

解决:所有to_csv统一写成encoding="utf-8-sig",这是最终的后悔药。另外写数据时用index=False,别把行号也写进去,否则后续用 pandas 读回来时多一列Unnamed: 0,还要再删一次。

5.4 可视化图表中文全变方块

现象:用 pyecharts 或 matplotlib 画图,图正常出,但所有中文标签变成一个个「口」形方块。

原因:pyecharts 底层输出 HTML/JS 渲染,一般不会方块;但 matplotlib 的中文字体路径没有配置,系统里找不到中文字体就回退成了默认的 DejaVu Sans。

解决:matplotlib 里每次开头固定一段配置代码,不用每次翻文档:

import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "PingFang SC"] plt.rcParams["axes.unicode_minus"] = False

axes.unicode_minus这个参数经常被漏掉,不关掉的话纵轴的负号会显示成方块。如果这行配置之后汉字还是方块,说明系统里确实没有这几个字体,需要显式指定字体文件路径。

5.5 词云被「大数据」三个字霸屏

现象:技能词云出来之后,画面上最醒目的是「大数据」三个字,接着是「分析」「开发」,Hadoop、Spark、Flink 这些真正的技能反而不显眼。

原因:岗位标题里普遍含「大数据」三个字,而技能标签里顿多岗位又都把「大数据」当作标签重复填写,词频统计把标题词和技能标签混在一起了。词云基于纯词频画大小,标题词霸屏以后,词云的信息量接近于零。

解决:统计技能词频的字段只取skills列,不拼接job_title;同时建一个常用停用词表,把「大数据」「分析」「开发」「工程师」这类高频率低区分度的词过滤掉。这样词云展示的才是 Hadoop、Spark、Flink、Kafka、ClickHouse 这类真正能指导学习方向的关键技能。

6. 可视化展示:从静态图表到一键更新的大屏

6.1 三张核心图的 ECharts 参数

可视化大屏不需要花哨,关键是信息密度。我用 pyecharts 做三张主图:技能词云图、学历薪资箱线图、公司需求 Top10 柱状图。词云参数里shape选circle更省空间,word_size_range控制在 12 到 50 之间,太小看不出主次,太大溢出画布。

from pyecharts.charts import WordCloud from pyecharts import options as opts wc = WordCloud() wc.add( series_name="技能需求", data_pair=skill_top, # 来自4.4的 [(词, 频次), ...] word_size_range=[12, 50], shape="circle", width="100%", height="400px", ) wc.set_global_opts(title_opts=opts.TitleOpts(title="北京大数据岗位技能需求词云")) wc.render("wordcloud_skills.html")

data_pair的格式必须是[(词1, 频次1), (词2, 频次2)],很多人传成字典直接报错。词云背后是 echarts-wordcloud 插件,本地渲染时需要联网加载 JS,如果要在内网做展示,需要把依赖的 JS 一并下载放到同级目录,否则打开 HTML 图表空白。

6.2 本地大屏与数据自动更新

多张图表分散的 HTML 不方便汇报,常见做法是用一个主页面通过 iframe 嵌多个图表文件,或者用 Flask 起一个本地服务统一托管。我更推荐后者,因为能顺带解决自动更新的问题:把爬虫和分析脚本写进同一个run.py,每晚定时跑一次,可视化服务读的是最新中间结果。

"""run.py — 一键更新数据并生成图表""" import subprocess import schedule import time def job(): print("开始更新招聘数据...") subprocess.run(["python", "crawler.py"], check=True) subprocess.run(["python", "clean_analyze.py"], check=True) subprocess.run(["python", "visualize.py"], check=True) print("更新完成") schedule.every().day.at("02:00").do(job) # 每天凌晨2点跑 if __name__ == "__main__": job() # 先手动跑一次 while True: schedule.run_pending() time.sleep(3600)

用schedule库做定时,比写死time.sleep(86400)强在可读性和可维护性,机器休眠错过任务后下次启动会重新执行。这里提醒一句,凌晨 2 点爬招聘信息是为了避开服务高峰,但反爬系统可不管你是不是善意,请求间隔不能省。

6.3 验证一份数据是否可信的土办法

可视化做得再漂亮,数据错了也是白搭。我每次出报告之前都会做两件事:第一,从清洗后的数据里随机抽 20 条记录,人工到招聘网站核对岗位名称、薪资区间、学历要求是否一致;第二,统计company字段的去重比例,如果前 10 家公司占了 80% 以上的岗位数,说明爬虫大概率只抓到了几个头部公司的招聘页,样本代表性不够,需要扩大关键词范围或者增加翻页深度。

这套流程走到现在,我养成的唯一习惯是:原始数据 CSV 永远不删,清洗脚本永远加注释。因为每次翻车都不是因为计算复杂,而是因为隔了两周再回来看代码,想不起来当时为什么把某个字段置成空值。数据分析和可视化本身不难,难的是把你的处理过程留痕到让第二个人三分钟能接手。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询