Python招聘数据采集与可视化实战:从爬虫到三城薪资对比

发布时间:2026/10/31 10:20:27
Python招聘数据采集与可视化实战:从爬虫到三城薪资对比

简介:这是一份面向Python初学者与数据分析入门者的综合实战资料,围绕拉勾网Python职位信息展开,覆盖网络爬虫、数据清洗、统计分析与可视化呈现的完整链路,适合用作课程大作业、毕业设计选题或自学练手项目。压缩包共22个文件,约330KB,包含12张png可视化图表、8个csv数据文件、1个py爬虫脚本和1份md说明文档,图表与数据按上海、深圳、福州等城市分类,便于对照分析。目前已有5226人学习下载,热度较高。通过这份资料,读者可以拿到可直接运行的爬虫脚本、多城市职位原始数据与清洗结果,以及公司分布、学历要求、薪资水平等维度的成图,同时理解requests请求、Pandas处理、Matplotlib与Seaborn绘图、异常处理及反爬应对等关键知识点,快速搭建属于自己的招聘市场分析小项目。

1. 从一份拉勾 Python 岗位数据包说起:它到底能跑出什么结果

如果你手头正好有一份Python爬虫数据可视化分析大作业.zip,打开后看到lagou_spider.py、lagou.csv、lagou_finall.csv,还有上海、深圳、福州三地的薪资、学历、公司分布 PNG,那这基本就是一个已经跑完的招聘数据采集与分析闭环。它解决的不是"怎么装 Python"这种入门问题,而是把 requests 抓取、pandas 清洗、matplotlib 出图串成一条能复现的链路。适合正在做大作业、课程设计,或者想拿真实岗位数据练手数据清洗与可视化的人。下面我按"能跑起来 → 看懂每步 → 避开坑"的顺序拆一遍。

2. 拆开压缩包先看结构:文件命名里藏着整条数据流

2.1 从文件名反推处理链路

拿到一个别人打包好的项目,我第一件事不是急着运行,而是先看文件命名规律。这份资源里的文件名其实已经把处理流程写清楚了:

文件作用所处阶段
lagou_spider.py爬虫主脚本数据采集
lagou.csv原始抓取结果采集输出
lagou_finall.csv清洗合并后的总表数据清洗
上海_python_lagou.csv按城市切分的原始数据分城市采集
上海_python_lagou_finall.csv分城市清洗结果分城市清洗
上海_python_薪资.png薪资分布图可视化输出
上海_python_学历要求.png学历要求分布可视化输出
上海_python_公司分布.png公司类型分布可视化输出

从命名能看出,作者是按"城市 + 关键词 + 阶段"三层来组织文件的。上海、深圳、福州三个城市各有一套原始 CSV、清洗后 CSV 和三张图,最后再汇总成lagou_finall.csv。这种结构的好处是每个城市可以独立跑、独立验证,出问题不会污染全局数据。

2.2 环境依赖与目录约定

在动手之前,先把依赖装齐。这份项目用到的库不算多,但版本差异会直接影响爬取和绘图结果:

# 建议在虚拟环境里操作,避免污染全局 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 核心依赖 pip install requests pandas matplotlib seaborn numpy pip install beautifulsoup4 lxml

这里有几个点值得说清楚。requests负责发 HTTP 请求拿页面,beautifulsoup4配合lxml解析 HTML,pandas做数据清洗和 CSV 读写,matplotlib和seaborn负责出图。numpy虽然不直接调用,但 pandas 的很多统计操作底层依赖它,装上省心。

提示:如果你的 Python 是 3.8 及以上,上面这些库直接 pip 装最新版基本没问题。但 seaborn 在部分老版本 matplotlib 上会报样式冲突,遇到就把 matplotlib 升到 3.5 以上。

目录上,我一般会把脚本、数据、图片分开放,但这份资源是平铺的。平铺的好处是路径简单,pd.read_csv('lagou.csv')直接能跑;坏处是文件一多就乱。如果你要在此基础上扩展,建议改成data/、scripts/、output/三个目录,后面维护会轻松很多。

2.3 先跑通再优化:最小验证路径

不要一上来就改代码。先确认原始数据能读、图能出,这是判断资源可用性的最快方式:

import pandas as pd import matplotlib.pyplot as plt # 先读一份分城市数据,确认编码和列名 df = pd.read_csv('上海_python_lagou_finall.csv', encoding='utf-8') print(df.columns.tolist()) print(df.shape) print(df.head(3))

这段代码的作用是探路。encoding='utf-8'是常见编码,但如果报UnicodeDecodeError,大概率是gbk或utf-8-sig,换成这两个再试。df.columns.tolist()打印列名,你能立刻知道薪资、学历、公司这些字段叫什么,后面画图才不会因为列名对不上而翻车。df.shape看数据量,如果只有几行,说明爬取阶段就没抓全,得回头查爬虫。

3. 爬虫脚本怎么读:requests 抓取与字段提取的关键参数

3.1 请求构造与反爬基础处理

lagou_spider.py的核心逻辑无非是"构造请求 → 拿响应 → 解析 → 存字段"。但招聘网站对请求头很敏感,直接裸奔大概率拿不到数据。常见做法是带上 User-Agent 和 Referer:

import requests import time import random 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.lagou.com/', 'Accept-Language': 'zh-CN,zh;q=0.9', } def fetch_page(url, retry=3): for i in range(retry): try: resp = requests.get(url, headers=headers, timeout=10) if resp.status_code == 200: return resp.text # 状态码异常时退避重试 time.sleep(random.uniform(2, 5)) except requests.RequestException as e: print(f'第{i+1}次请求失败: {e}') time.sleep(random.uniform(3, 6)) return None

headers里 User-Agent 伪装成正常浏览器,Referer 告诉服务器请求来源。fetch_page做了三件事:设置超时防止卡死、状态码非 200 时退避重试、捕获网络异常。random.uniform(2, 5)是随机延迟,比固定time.sleep(2)更不容易被识别为机器行为。retry=3是重试次数,一般三次拿不到就说明这个 URL 或当前策略有问题,继续重试意义不大。

3.2 字段提取与数据落盘

拿到 HTML 后,用 BeautifulSoup 定位职位卡片,逐条提取字段。不同时期页面结构会变,所以选择器要以实际页面为准,下面给的是常见结构:

from bs4 import BeautifulSoup import csv def parse_jobs(html): soup = BeautifulSoup(html, 'lxml') rows = [] # 职位列表通常在一个统一的容器里,逐条遍历 for item in soup.select('.con_list_item'): job = { '职位名称': item.select_one('.position_link').get_text(strip=True) if item.select_one('.position_link') else '', '公司': item.select_one('.company_name').get_text(strip=True) if item.select_one('.company_name') else '', '薪资': item.select_one('.money').get_text(strip=True) if item.select_one('.money') else '', '城市': item.select_one('.add').get_text(strip=True) if item.select_one('.add') else '', '学历': item.select_one('.li_b_l').get_text(strip=True) if item.select_one('.li_b_l') else '', } rows.append(job) return rows def save_csv(rows, path): if not rows: print('无数据可写') return with open(path, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.DictWriter(f, fieldnames=rows[0].keys()) writer.writeheader() writer.writerows(rows)

parse_jobs里每个字段都做了空值判断,这是血泪经验——页面里总有几条缺薪资或缺学历的记录,不判断就会AttributeError直接崩。save_csv用utf-8-sig编码,这样 Excel 打开不会乱码,比纯utf-8更省事。newline=''是 Windows 下防止多出空行的标准写法。

3.3 分城市采集的参数差异

资源里上海、深圳、福州各有一套数据,说明脚本支持按城市参数化。常见做法是把城市名和对应的 URL 拼在一起,循环采集:

cities = ['上海', '深圳', '福州'] base_url = 'https://www.lagou.com/zhaopin/Python/{}/' for city in cities: # 实际 URL 需按网站当前规则调整,这里示意参数化思路 url = base_url.format(city) html = fetch_page(url) if html: rows = parse_jobs(html) save_csv(rows, f'{city}_python_lagou.csv') print(f'{city} 采集完成,共 {len(rows)} 条') time.sleep(random.uniform(5, 10)) # 城市之间拉长间隔

城市之间sleep拉到 5 到 10 秒,是因为连续快速请求不同城市页面更容易触发限制。f'{city}_python_lagou.csv'的命名和资源里的文件名对上了,说明作者就是这么组织的。如果你只关心一个城市,把cities改成单元素列表即可,不用动其他逻辑。

4. 数据清洗与可视化:从 CSV 到三张图的完整操作

4.1 薪资字段清洗:把"15k-25k"变成可计算的数字

原始薪资是字符串,直接画图没有意义。必须先拆成最低和最高两个数值列:

import pandas as pd import re df = pd.read_csv('上海_python_lagou_finall.csv', encoding='utf-8') def parse_salary(s): if pd.isna(s): return pd.Series([None, None]) # 匹配 15k-25k、15-25K、15k以上 等常见格式 nums = re.findall(r'(\d+)', str(s).lower()) if len(nums) >= 2: return pd.Series([int(nums[0]), int(nums[1])]) elif len(nums) == 1: return pd.Series([int(nums[0]), int(nums[0])]) return pd.Series([None, None]) df[['薪资下限', '薪资上限']] = df['薪资'].apply(parse_salary) df['平均薪资'] = (df['薪资下限'] + df['薪资上限']) / 2 df = df.dropna(subset=['平均薪资']) # 丢掉无法解析的行

re.findall(r'(\d+)', ...)把所有数字抠出来,两个就是区间,一个就当作固定值。apply(parse_salary)返回的是 Series,所以用df[['薪资下限', '薪资上限']] = ...一次接两列。dropna把解析失败的记录去掉,避免后面统计被 NaN 干扰。这一步做完,薪资才真正变成能算平均值、画分布图的数据。

4.2 学历与公司分布统计

学历和公司类型是分类字段,统计逻辑简单,但要注意去重和排序:

# 学历分布 edu_counts = df['学历'].value_counts() # 公司分布,取前 10 避免图太挤 company_counts = df['公司'].value_counts().head(10) print(edu_counts) print(company_counts)

value_counts()默认按数量降序,直接就能拿去画条形图。公司取前 10 是因为招聘数据里公司名很分散,全画出来横坐标会密到看不清——这也是热搜里"python画图横坐标太密集"的典型场景。取 Top 10 既保留主要信息,又让图可读。

4.3 三张图的绘制与保存

资源里每个城市都有薪资、学历、公司三张图,下面用 seaborn 统一风格:

import matplotlib.pyplot as plt import seaborn as sns plt.rcParams['font.sans-serif'] = ['SimHei'] # 中文显示 plt.rcParams['axes.unicode_minus'] = False # 薪资分布直方图 plt.figure(figsize=(8, 5)) sns.histplot(df['平均薪资'], bins=15, kde=True) plt.title('上海 Python 岗位薪资分布') plt.xlabel('平均薪资 (k)') plt.ylabel('岗位数量') plt.tight_layout() plt.savefig('上海_python_薪资.png', dpi=150) plt.close() # 学历要求条形图 plt.figure(figsize=(8, 5)) sns.barplot(x=edu_counts.index, y=edu_counts.values) plt.title('上海 Python 岗位学历要求') plt.xticks(rotation=30) plt.tight_layout() plt.savefig('上海_python_学历要求.png', dpi=150) plt.close()

SimHei是 Windows 自带中文字体,Linux 或 Mac 上可能没有,换成Arial Unicode MS或WenQuanYi Micro Hei。axes.unicode_minus = False解决负号显示成方块的问题。dpi=150让保存的图清晰度够用,又不至于文件太大。plt.close()很重要,不关的话循环画多个城市时会内存堆积。

注意:如果SimHei报字体找不到,图里中文会全变方块。这不是代码错,是环境缺字体,换一个系统里有的中文字体名即可。

5. 避坑与排查:这份资源最容易翻车的五个地方

5.1 页面结构变了,选择器全部失效

现象:脚本能跑,但parse_jobs返回空列表,CSV 里一条数据都没有。原因:招聘网站前端改版,.con_list_item、.position_link这些 class 名已经不存在。解决:打开目标页面,F12 看实际 DOM 结构,把选择器换成当前有效的。这类项目最大的不确定性就在这,选择器不是一劳永逸的。

5.2 请求被限制,返回空页面或验证页

现象:resp.status_code是 200,但resp.text里没有职位数据,而是一段验证提示。原因:请求频率过高或请求头不完整,被识别为异常流量。解决:补全 headers,拉大sleep间隔,必要时降低单次采集量。不要试图用极端高频去压,只会更快被封。

5.3 薪资解析出负数或异常值

现象:平均薪资里出现 0 或者几百这种明显不合理的数。原因:薪资字段里混入了"薪资面议"或"15k以上"这类非标准格式,正则抠出的数字不对。解决:在parse_salary里加范围过滤,比如低于 1k 或高于 200k 的直接置为 None,再dropna。

5.4 CSV 用 Excel 打开乱码

现象:pandas 读没问题,Excel 打开中文全是乱码。原因:保存时用了纯utf-8,Excel 默认按 GBK 解析。解决:保存时统一用encoding='utf-8-sig',这个 BOM 头能让 Excel 正确识别编码。

5.5 画图时中文字体缺失

现象:图能出,但标题和坐标轴中文全是方块。原因:当前环境没有SimHei字体,或 matplotlib 没找到。解决:先import matplotlib.font_manager查可用字体,选一个支持中文的设进去。Linux 服务器上通常需要手动装中文字体包。

6. 进阶玩法:把三城数据合并做横向对比

跑通单城市之后,最有价值的扩展是把上海、深圳、福州三份_finall.csv合并,做城市间薪资对比。这一步能直接产出比单城图更有说服力的结论:

import pandas as pd import matplotlib.pyplot as plt import seaborn as sns cities = ['上海', '深圳', '福州'] frames = [] for city in cities: tmp = pd.read_csv(f'{city}_python_lagou_finall.csv', encoding='utf-8') tmp['城市'] = city frames.append(tmp) all_df = pd.concat(frames, ignore_index=True) # 各城市平均薪资对比 city_salary = all_df.groupby('城市')['平均薪资'].mean().sort_values(ascending=False) print(city_salary) plt.figure(figsize=(8, 5)) sns.barplot(x=city_salary.index, y=city_salary.values) plt.title('三城 Python 岗位平均薪资对比') plt.ylabel('平均薪资 (k)') plt.tight_layout() plt.savefig('三城薪资对比.png', dpi=150) plt.close()

pd.concat(frames, ignore_index=True)把三份数据纵向堆叠,ignore_index重置索引避免重复。groupby('城市')['平均薪资'].mean()一行算出各城均值,sort_values让柱子从高到低排。这个对比图比单城分布图更能支撑"哪个城市 Python 岗位更值钱"的判断。

再进一步,可以做薪资与学历的交叉分析:

# 不同学历在各城市的薪资表现 pivot = all_df.pivot_table( values='平均薪资', index='学历', columns='城市', aggfunc='mean' ) print(pivot.round(1))

pivot_table把学历做行、城市做列、平均薪资做值,一张表就能看出"本科在上海"和"本科在福州"差多少。round(1)保留一位小数,输出更干净。这种交叉表在写大作业报告时特别有用,比单纯堆图更有分析深度。

我自己的习惯是,每次拿到一份别人打包的数据项目,先不改代码,而是按"读数据 → 跑通图 → 再动爬虫"的顺序走一遍。因为爬虫受外部影响最大,而数据和绘图是纯本地的,先确认本地链路没问题,再去排查网络环节,能省掉大量来回折腾的时间。这份资源的价值不在于代码多复杂,而在于它把采集、清洗、可视化三个环节的真实产物都留下来了,你可以对着 CSV 和 PNG 反推每一步做了什么,这比只看脚本更容易理解整条链路。希望帮到你。

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