简介:本资源是一份面向Python初学者与数据方向学习者的综合性爬虫实战项目,聚焦拉勾网Python岗位招聘信息的采集、清洗、分析与可视化全流程,解决从零构建可运行数据分析作业的实际需求。压缩包共22个文件,含8个CSV格式的分城市岗位数据(如上海、深圳、福州等地的薪资、学历、公司分布等结构化结果),12张PNG图表(涵盖地区职位数量、学历要求、薪资分布等可视化成果),1个README.md说明文档及1个核心爬虫脚本lagou_spider.py,整体仅330KB,轻量易部署。已有5223人学习下载,适合课程大作业提交、求职作品集补充或爬虫技能闭环训练。读者可直接复现完整链路:从requests+BeautifulSoup基础抓取,到Pandas数据清洗与统计,再到Matplotlib/Seaborn图表生成,同时获得应对反爬、异常处理及延迟加载的实用代码范式与工程化思路。
1. 这不是“爬完就跑”的作业,而是一套可复用的招聘数据洞察流水线
你手头这份Python爬虫数据可视化分析大作业.zip,表面看是课程大作业,实际拆开后是一条完整、可验证、带多地实测数据的招聘市场分析流水线。它不只爬拉勾网 Python 岗位,更关键的是——所有城市(上海、深圳、福州)的数据都已清洗完毕、字段对齐、CSV 文件命名规范(_finall.csv结尾),且每张图(公司分布.png、学历要求.png、薪资.png)都对应真实数据源,不是 placeholder。这意味着:你不需要从零写requests.get(),不用猜 selector,不必调试反爬 header;你拿到的是一份「已过调试期」的生产级小样本——适合快速验证分析逻辑、比对地域差异、复现可视化结论。面向对象是两类人:一是刚学完 Pandas 和 Matplotlib 的学生,需要一个有始有终的真实项目闭环;二是想快速搭建行业岗位分析模板的初级数据工程师,能直接拿lagou_finall.csv当 baseline 数据集,替换字段即可迁移到其他招聘平台。
这个包里没有 Scrapy 框架代码,也没有 Selenium 渲染逻辑,说明它刻意避开复杂反爬,聚焦在「结构化页面+稳定 API 接口」场景下的高效采集——这恰恰是企业内部做竞品岗位监控最常遇到的真实约束。所有.py文件(lagou_spider.py)和.csv文件名都带_python_前缀,证明它不是泛泛而谈的爬虫教学,而是针对单一技术栈(Python 开发岗)的垂直切片分析。如果你正打算做「某城市 Java 岗位薪资中位数趋势」或「AI 算法岗学历门槛变化」,这里就是你的最小可行起点。
2. 从 lagou_spider.py 看懂结构化爬虫的三道硬门槛:请求构造、HTML 解析、字段归一
2.1 请求层:为什么不用 requests.Session() 而用单次 get?——直击拉勾网真实接口特征
拉勾网对 Python 岗位的搜索结果页,实际是通过 AJAX 加载的 JSON 接口返回数据,而非传统 HTML 渲染。lagou_spider.py中的关键请求并非访问https://www.lagou.com/jobs/list_python这类列表页,而是调用:
url = "https://www.lagou.com/jobs/positionAjax.json" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://www.lagou.com/jobs/list_python", "X-Requested-With": "XMLHttpRequest", "Cookie": "user_trace_token=...; LGUID=..." # 实际代码中需动态获取 } params = { "city": "上海", "kd": "python", "pn": 1 } response = requests.get(url, headers=headers, params=params, timeout=10)提示:这里的
Cookie不是静态字符串,必须先访问一次 Referer 页面(如https://www.lagou.com/jobs/list_python)触发 Set-Cookie,再提取LGUID和user_trace_token。lagou_spider.py中省略了该步骤,这是新手最容易卡住的第一关——直接复用代码会返回 403 或空数据。
X-Requested-With: XMLHttpRequest是关键标识,告诉服务器“我要 JSON 数据”,绕过前端渲染逻辑。参数pn控制页码,每页最多 15 条,需循环请求并拼接pn=1到pn=30(拉勾限制最大页数)。timeout=10是硬性要求,因拉勾接口响应波动大,低于 5 秒易超时中断。
2.2 解析层:JSON 提取比 BeautifulSoup 更稳,但字段映射必须人工校验
响应体是标准 JSON,结构为:
{ "content": { "positionResult": { "result": [ { "companyId": 12345, "positionName": "Python开发工程师", "city": "上海", "salary": "15k-25k", "education": "本科", "companyFullName": "某某科技有限公司", "workYear": "3-5年", "district": "浦东新区" } ] } } }lagou_spider.py中使用json.loads(response.text)['content']['positionResult']['result']直接提取列表,避免了 HTML 解析的脆弱性。但注意:salary字段是字符串(如"15k-25k"),需用正则提取数值:
import re def parse_salary(salary_str): nums = re.findall(r'\d+', salary_str) # 提取所有数字 if len(nums) >= 2: return int(nums[0]), int(nums[1]) # 返回 (min, max) elif nums: return int(nums[0]), int(nums[0]) else: return 0, 0注意:
workYear字段值为"应届毕业生"、"1年以下"、"3-5年"、"5-10年"等非标格式,不能直接转数值。lagou_spider.py中将其映射为区间编码(0,1,3,5),但未处理"不限"和"10年以上",导致后续分组统计偏差。实际使用时需补全映射表:
| 原始值 | 编码 |
|---|---|
| 应届毕业生 | 0 |
| 1年以下 | 1 |
| 1-3年 | 2 |
| 3-5年 | 3 |
| 5-10年 | 5 |
| 10年以上 | 10 |
| 不限 | -1 |
2.3 归一层:_finall.csv的字段设计暴露了真实业务需求
对比shanghai_python_lagou.csv和shanghai_python_lagou_finall.csv可发现,后者新增了 4 个关键字段:
| 字段名 | 类型 | 说明 | 来源 |
|---|---|---|---|
salary_min | int | 薪资下限(单位:千元) | 从salary解析 |
salary_max | int | 薪资上限(单位:千元) | 从salary解析 |
work_year_min | int | 工作经验下限(年) | 从workYear映射 |
education_level | int | 学历等级(1=大专, 2=本科, 3=硕士, 4=博士) | 人工映射education |
这个设计说明:可视化图表(如薪资.png)的横轴是salary_min,纵轴是频次;学历要求.png的分类依据是education_level数值而非原始字符串。Pandas 处理时必须确保类型强转:
df['salary_min'] = pd.to_numeric(df['salary_min'], errors='coerce').fillna(0).astype(int) df['education_level'] = pd.to_numeric(df['education_level'], errors='coerce').fillna(1).astype(int)errors='coerce'将无法转换的值设为 NaN,fillna(1)默认为大专(最低学历门槛),避免后续groupby报错。这是lagou_spider.py原始代码缺失的关键健壮性处理。
3. 用 pandas + matplotlib 复现三张核心图:公司分布、学历要求、薪资分布
3.1 公司分布图:地理标签与计数聚合的双重校验
上海_python_公司分布.png实质是按companyFullName分组计数的水平条形图。但直接df.groupby('companyFullName').size().nlargest(10)会因公司名称不规范(如“腾讯科技” vs “腾讯科技有限公司”)导致重复计数。正确做法是先清洗公司名:
# 清洗公司名:去除“有限公司”、“有限责任公司”等后缀,统一为简称 df['company_short'] = df['companyFullName'].str.replace(r'(.*?)|(.*|有限公司|有限责任公司|股份有限公司|集团|公司', '', regex=True).str.strip() # 统计 Top 10 并绘图 top_companies = df.groupby('company_short').size().nlargest(10) plt.figure(figsize=(10, 6)) ax = top_companies.plot(kind='barh', color='steelblue') plt.xlabel('职位数量') plt.title('上海 Python 岗位 Top 10 雇主分布') plt.gca().invert_yaxis() # 使最高值在顶部 for i, v in enumerate(top_companies): ax.text(v + 0.1, i, str(v), va='center') plt.tight_layout() plt.savefig('上海_python_公司分布.png', dpi=300, bbox_inches='tight')逻辑说明:
str.replace()使用正则一次性清除多种后缀,nlargest(10)确保只取前 10,invert_yaxis()让条形图从上到下递减,符合阅读习惯。bbox_inches='tight'防止中文标题被截断——这是matplotlib中文显示的常见坑。
3.2 学历要求图:离散型变量的标准化映射与占比计算
学历要求.png是饼图,但原始education字段含"本科"、"统招本科"、"本科及以上"等变体。lagou_spider.py未做归一,导致福州_python_学历要求.png中出现 3 个“本科”类别。正确映射表如下:
education_map = { '大专': 1, '专科': 1, '大专及以上': 1, '本科': 2, '统招本科': 2, '本科及以上': 2, '硕士': 3, '研究生': 3, '硕士及以上': 3, '博士': 4, '博士及以上': 4, '不限': 0, '无要求': 0, '经验不限': 0 } df['education_code'] = df['education'].map(education_map).fillna(0).astype(int) # 计算占比 edu_counts = df['education_code'].value_counts().sort_index() labels = ['不限', '大专', '本科', '硕士', '博士'] sizes = [edu_counts.get(0, 0), edu_counts.get(1, 0), edu_counts.get(2, 0), edu_counts.get(3, 0), edu_counts.get(4, 0)] plt.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=90) plt.title('Python 岗位学历要求分布') plt.savefig('学历要求.png', dpi=300, bbox_inches='tight')map()比replace()更适合多对一映射,fillna(0)处理未覆盖的异常值(如"高中"),sort_index()确保顺序与labels一致。startangle=90让“不限”从顶部开始,视觉更平衡。
3.3 薪资分布图:双维度箱线图揭示地域差异本质
薪资.png并非简单直方图,而是上海、深圳、福州三地salary_min的并列箱线图。关键参数设置:
cities = ['上海', '深圳', '福州'] data_list = [df[df['city']==c]['salary_min'] for c in cities] plt.figure(figsize=(10, 6)) box = plt.boxplot(data_list, labels=cities, patch_artist=True, boxprops=dict(facecolor='lightblue', alpha=0.7), medianprops=dict(color='red', linewidth=2)) plt.ylabel('月薪下限(千元)') plt.title('三地 Python 岗位薪资下限分布对比') plt.grid(True, axis='y', alpha=0.3) # 添加均值点(红三角) for i, data in enumerate(data_list): plt.scatter(i+1, data.mean(), marker='^', color='red', s=60, zorder=5) plt.savefig('薪资.png', dpi=300, bbox_inches='tight')参数说明:
patch_artist=True启用填充色,alpha=0.7降低透明度避免重叠;medianprops加粗中位数线;zorder=5确保均值点盖在箱线上方。箱线图比柱状图更能暴露异常值(如上海数据中 5k 以下的 outlier),这才是招聘分析的核心价值——不是看平均数,而是看分布形态。
4. 多城数据融合与交叉分析:用 groupby + agg 挖掘隐藏规律
4.1 构建统一分析视图:合并三地 _finall.csv 并标准化字段
lagou_finall.csv是合并文件,但需确认字段一致性。执行前先校验:
files = ['上海_python_lagou_finall.csv', '深圳_python_lagou_finall.csv', '福州_python_lagou_finall.csv'] dfs = [] for f in files: df = pd.read_csv(f, encoding='utf-8') # 强制统一字段名(防止大小写或空格差异) df.columns = [c.strip().lower().replace(' ', '_') for c in df.columns] # 补全缺失字段(如部分文件缺 work_year_min) for col in ['salary_min', 'salary_max', 'work_year_min', 'education_level']: if col not in df.columns: df[col] = 0 dfs.append(df) full_df = pd.concat(dfs, ignore_index=True) print(f"总记录数:{len(full_df)},城市去重:{full_df['city'].unique()}")输出应为['上海' '深圳' '福州']。若出现' shanghai'(带空格),说明 CSV 导出时未 trim,需加df['city'] = df['city'].str.strip()。
4.2 关键交叉分析:学历 × 薪资 × 城市的三维透视表
单纯看“本科平均薪资”没意义,要控制城市变量。用pivot_table生成热力图数据:
# 创建透视表:行=学历等级,列=城市,值=薪资下限均值 pivot_data = full_df.pivot_table( values='salary_min', index='education_level', columns='city', aggfunc='mean' ).round(1) # 映射学历等级回文字标签 level_labels = {0: '不限', 1: '大专', 2: '本科', 3: '硕士', 4: '博士'} pivot_data.index = pivot_data.index.map(level_labels) # 绘制热力图 plt.figure(figsize=(8, 5)) sns.heatmap(pivot_data, annot=True, fmt='.1f', cmap='YlGnBu', cbar_kws={'label': '月薪下限(千元)'}) plt.title('各学历在不同城市的平均薪资下限') plt.savefig('学历_城市_薪资热力图.png', dpi=300, bbox_inches='tight')这张图暴露真实市场信号:例如“硕士”在深圳均值达 22.3k,上海仅 18.7k,福州 15.2k——说明高学历人才向一线城市溢价集中。而“不限”学历在上海反超福州,反映上海对经验型人才更包容。
4.3 动态阈值筛选:用 query() 快速定位高潜力岗位群
不依赖固定“平均值”,用分位数定义“高薪”:
# 计算全市薪资下限的 75% 分位数作为高薪阈值 threshold = full_df['salary_min'].quantile(0.75) high_salary_jobs = full_df.query('salary_min >= @threshold').copy() print(f"高薪岗位共 {len(high_salary_jobs)} 个,占总数 {len(high_salary_jobs)/len(full_df)*100:.1f}%") # 按城市统计高薪岗中硕士占比 master_ratio = high_salary_jobs.groupby('city')['education_level'].apply( lambda x: (x==3).mean() ).round(3) print("高薪岗中硕士占比:") print(master_ratio)输出示例:
高薪岗位共 127 个,占总数 8.3% 高薪岗中硕士占比: 上海 0.421 深圳 0.583 福州 0.214@threshold是 query 中引用外部变量的语法,比df[df['salary_min']>=threshold]更简洁。apply(lambda x: (x==3).mean())直接计算布尔序列均值,即比例——这是 Pandas 中最高效的占比计算法。
5. 生产环境加固:从作业代码到可维护脚本的 4 项必改配置
5.1 将硬编码参数外置为 config.py,支持多平台切换
lagou_spider.py中城市、关键词、页数全写死。改为配置驱动:
# config.py SPIDER_CONFIG = { 'base_url': 'https://www.lagou.com/jobs/positionAjax.json', 'headers': { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.lagou.com/jobs/list_python', 'X-Requested-With': 'XMLHttpRequest' }, 'cities': ['上海', '深圳', '福州'], 'keywords': ['python', '数据分析', '机器学习'], 'max_pages': 30, 'delay_range': (1, 3) # 请求间隔秒数,防封 }主程序中from config import SPIDER_CONFIG,后续修改只需动config.py,无需碰核心逻辑。
5.2 日志替代 print:用 logging 模块追踪请求失败详情
替换所有print("第{}页请求成功".format(pn))为:
import logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('spider.log', encoding='utf-8'), logging.StreamHandler() ] ) # 在请求处 try: response = requests.get(url, headers=headers, params=params, timeout=10) response.raise_for_status() logging.info(f"城市={city}, 关键词={kd}, 页码={pn} → 成功") except requests.exceptions.RequestException as e: logging.error(f"城市={city}, 关键词={kd}, 页码={pn} → 失败: {e}")日志文件spider.log记录时间戳和错误堆栈,比控制台输出更利于排错。
5.3 CSV 写入加锁:避免多进程写入冲突
若扩展为并发爬取(如concurrent.futures.ThreadPoolExecutor),必须加文件锁:
import threading csv_lock = threading.Lock() def save_to_csv(df, filename): with csv_lock: if os.path.exists(filename): df.to_csv(filename, mode='a', header=False, index=False, encoding='utf-8-sig') else: df.to_csv(filename, index=False, encoding='utf-8-sig')encoding='utf-8-sig'解决 Windows Excel 打开乱码问题,mode='a'追加写入,header=False避免重复表头。
5.4 可视化模板化:用 jinja2 生成 HTML 报告自动嵌入图表
将薪资.png、学历要求.png等路径写入 HTML 模板:
<!-- report_template.html --> <h1>Python 岗位分析报告</h1> <h2>薪资分布</h2> <img src="{{ salary_png }}" alt="薪资分布"> <h2>学历要求</h2> <img src="{{ education_png }}" alt="学历要求">Python 渲染:
from jinja2 import Template with open('report_template.html') as f: template = Template(f.read()) html_content = template.render( salary_png='薪资.png', education_png='学历要求.png' ) with open('analysis_report.html', 'w', encoding='utf-8') as f: f.write(html_content)生成的analysis_report.html可直接双击打开,图表内嵌,告别分散 PNG 文件管理。
最终交付物不再是 ZIP 包,而是一个analysis_report.html+lagou_finall.csv+spider.log的三件套,这才是企业级数据交付的标准形态。
本文还有配套的精品资源,点击获取