Python爬虫数据可视化分析:从拉勾职位到图表的完整实战

发布时间:2026/9/23 12:25:37
Python爬虫数据可视化分析:从拉勾职位到图表的完整实战 简介面向Python与数据分析入门者一份以拉勾网Python岗位为案例的爬虫与可视化综合大作业完整演练从网络爬虫、数据清洗、统计分析到可视化展示的综合性项目流程适合课程设计、毕业设计或自学参考。压缩包共22个文件包含Python爬虫脚本、8份CSV数据表、12张可视化结果图和说明文档整体仅330KB。脚本覆盖requests与Scrapy等爬虫写法CSV储存多城市岗位数据图片展示薪资、学历、公司分布等分析结论。资源内置异常处理、反爬应对和延迟加载等实战技巧利用Pandas完成数据预处理并通过图表直观反映招聘市场特征可快速替换目标网站或字段迁移到其他爬虫项目中。目前已有5223人学习文件结构清晰复现门槛低适合希望掌握数据采集与分析全流程的学习者。1. Python爬虫数据可视化分析大作业.zip从拉勾职位到图表一份能跑通的完整链路Python 爬虫数据可视化分析大作业.zip 这个资源包拆开后第一感受是它把爬虫抓取、数据清洗、可视化输出整条流水线都给你跑通了。lagou_spider.py 是核心爬虫程序十几个 CSV 对应上海、深圳、福州三个城市的 Python 职位原始数据最后那批 PNG 则是从数据里长出来的公司分布、学历要求和薪资分析图。对正在做课程设计、或者想研究 Python 岗位行情的人来说它不只是一份作业而是一套可以替换城市名、更换搜索关键词就能重跑的分析链路。你拿到手不用从零搭环境改几个参数就能产出自己的结果。这份包适合两类人一是急着交作业但不想只交半成品的在校生二是想学爬虫加数据可视化全流程但没时间自己抠反爬的从业者。后面我会按爬虫、清洗、画图、排坑的顺序把这份资源真正拆开用起来。2. 爬虫主程序请求伪装、Ajax 接口解析与字段拉平把拉勾职位变成结构化列表2.1 拉勾列表页是动态加载的直接解析 HTML 会翻车拉勾的职位列表不是传统服务端渲染页面。早期版本还能靠 requests 直接拿 HTML现在你直接 GET 页面源码能看到的只有框架和一个初始化的 JS 变量职位列表是浏览器执行 JavaScript 之后才渲染出来的。这一点在拆 lagou_spider.py 的使用逻辑时必须先讲清楚否则你会陷入“响应码 200却解析不到任何职位”的困惑。这套包能跑通靠的是拉勾前端背后那个真实接口浏览器点翻页时前端会向 positionAjax.json 发一个 POST 请求返回 JSON 数据。包里 CSV 之所以有职位名称、薪资、学历要求、公司名这些字段就是从这段 JSON 里拆出来的。原理上就一句话把请求头伪装成浏览器带着页码参数去请求接口再把返回的嵌套 JSON 拉平成表格。理解到这一层你就不需要依赖 BeautifulSoup 去逐层找 div 了——那是传统服务端渲染页面才高效的方案。2.2 请求头与翻页参数User-Agent、Referer、pn、kd 缺一不可先看这份包里最核心的请求函数写法常见做法是这样import requests import time import random def fetch_lagou(page1, keywordpython, city上海): url https://www.lagou.com/jobs/positionAjax.json headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36, Referer: https://www.lagou.com/jobs/list_python?city%E4%B8%8A%E6%B5%B7, Origin: https://www.lagou.com, } payload { first: true if page 1 else false, pn: page, kd: keyword, } resp requests.post(url, headersheaders, datapayload, timeout15) resp.raise_for_status() return resp.json()这段代码里最容易被忽略的是first参数第一页请求它是true翻页后必须改成false否则接口返回的数据结构可能不稳定。pn从 1 开始递增kd是搜索关键词这份包里统一用的是python。Referer和Origin两个头是拉勾校验的底线直接去掉的话请求很容易被识别成脚本返回一个 success 为 false 的空壳。timeout15是给网络波动留的余量避免某个页面卡住导致整个爬虫挂起。2.3 解析嵌套 JSON把 result 里的字典列表变成 DataFrame接口返回的 JSON 是三层嵌套结构最外层是content中间层是positionResult真正有用的职位列表在result里。写解析函数的时候我最常用的是先判空再遍历import pandas as pd def parse_json_to_df(data): if not data or not data.get(content): return pd.DataFrame() result data[content][positionResult][result] if not result: return pd.DataFrame() records [] for item in result: records.append({ 职位名称: item.get(positionName), 公司名称: item.get(companyFullName), 城市: item.get(city), 薪资: item.get(salary), 学历要求: item.get(education), 工作经验: item.get(workYear), 公司规模: item.get(companySize), }) return pd.DataFrame(records)这里的重点有两个。一是item.get()比item[key]安全招聘数据经常有缺失字段直接取值会抛 KeyError而 get 取不到就返回 None不影响整行记录。二是每次只保留七个字段这是刻意做减法——接口返回的信息远不止这些包含职位标签、发布时间、融资阶段等但课程作业分析用不到字段越少后面 CSV 越清爽。逻辑上先判空再遍历是为了避免翻到最后一页或接口被限流时解析函数直接崩溃。翻页和保存我一般这样组织把每一页的 DataFrame 收集起来最后合并而不是每页都写一次文件all_frames [] for page in range(1, 16): data fetch_lagou(page, python, 上海) df parse_json_to_df(data) if df.empty: break all_frames.append(df) time.sleep(random.uniform(2, 5)) result pd.concat(all_frames, ignore_indexTrue) result.to_csv(上海_python_lagou.csv, indexFalse, encodingutf-8-sig)逐页收集再统一落盘的好处是中间任何一页失败你手上的 DataFrame 还完整保留在内存里不会产生半个损坏的 CSV。sleep 的区间取 2 到 5 秒是拉勾不封 IP 的底线操作这也是后面避坑章节要继续讲的。那个random.uniform比固定 sleep 3 秒更像真人操作配合随机延时请求频率看起来不是均匀的机器节奏。3. 数据处理与 CSV 合并Pandas 清洗薪资字段、补齐缺失值把三个城市拉通成最终分析表3.1 为什么爬下来的 CSV 不能直接拿来分析打开拉勾原始 CSV 你会发现薪资是一整个字符串比如“20k-40k”工作经验和学历也混着中文描述。这种形态画图不行、求均值也不行必须先做数据清洗。包里出现的上海_python_lagou_finall.csv、深圳_python_lagou_finall.csv、福州_python_lagou_finall.csv就是原始数据清洗后的版本而最终合并出的lagou_finall.csv则是三个城市清洗表拼接而成的结果。清洗的思路分三步第一步处理缺失值第二步把薪资字符串拆解成可计算的数值第三步统一字段名并去重。Pandas 在这三步里是绝对主力它读 CSV 一行搞定做列运算和过滤也远比纯 Python 循环高效。还有一个常被忽略的细节原始 CSV 里职位数据可能有多行是全空的清洗时先dropna(subset[职位名称, 公司名称])再往下走否则后面所有统计都会带上空行。3.2 薪资字段标准化把“20k-40k”拆成下限和上限薪资是这次分析里最核心的数值字段但原始格式没法直接算平均值。我惯用的方案是用正则把数字全部摘出来import pandas as pd import re df pd.read_csv(上海_python_lagou.csv, encodingutf-8-sig) def split_salary(s): if pd.isna(s) or 面议 in str(s): return None, None nums re.findall(r\d\.?\d*, str(s)) if len(nums) 2: return float(nums[0]), float(nums[1]) elif len(nums) 1: return float(nums[0]), float(nums[0]) return None, None df[薪资下限] df[薪资].apply(lambda x: split_salary(x)[0]) df[薪资上限] df[薪资].apply(lambda x: split_salary(x)[1])这段代码的逻辑分三类情况处理像“20k-40k”这种区间型正则能抓到两个数字分别赋给下限和上限像“15k以上”这种只有单边值的上下限取同一个数这样后续求均值不会低估“面议”直接返回空值统计时会被 Pandas 自动忽略。re.findall里那个正则\d\.?\d*支持小数因为有的岗位写的是“15.5k-20k”整数正则会把 15.5 拆成 15 和 5薪资直接算错。清洗后我一般顺手存一版带后缀的 finall 文件目的是保留字段拆分结果同时不覆盖原始文件。这样如果后面发现解析规则有问题还能从原始 CSV 重新跑不用回头再去爬一遍。3.3 三城市数据合并与去重得到 lagou_finall.csv包里同时出现lagou_finall.csv和三份城市级 CSV说明原作者做了跨城市合并分析。合并的思路很直接先把每份 CSV 的城市列补齐然后 concat最后按关键字段去重sh pd.read_csv(上海_python_lagou_finall.csv, encodingutf-8-sig) sz pd.read_csv(深圳_python_lagou_finall.csv, encodingutf-8-sig) fz pd.read_csv(福州_python_lagou_finall.csv, encodingutf-8-sig) sh[城市] 上海 sz[城市] 深圳 fz[城市] 福州 all_data pd.concat([sh, sz, fz], ignore_indexTrue) all_data all_data.drop_duplicates(subset[职位名称, 公司名称, 城市]) all_data.to_csv(lagou_finall.csv, indexFalse, encodingutf-8-sig)这里的ignore_indexTrue很关键concat 后索引如果不重置行号会从三份表各自初始值开始后面iloc切片会出问题。去重时我选的三列是职位名称、公司名称、城市同一个公司在同一城市发布完全同名职位基本可以认为是重复数据而不同城市的同名职位不应被去掉。这套逻辑跑完lagou_finall.csv就是后续所有可视化的数据底座。如果你自己扩展这个包最值得改的就是drop_duplicates的字段组合。有人会把公司规模加进去导致同一职位因为公司描述微调被保留两行也有人只按职位名称去重结果把不同城市的数据误删了。这块没有标准答案取决于你的分析目标但记住一点去重字段宁多勿少多去一条可能是一份完整职位少去一条只是多一行噪声。4. 数据可视化公司分布、学历要求、薪资三张图Matplotlib 参数从乱码到出图4.1 中文字体与坐标轴先解决乱码再谈图表好不好看这份包输出的 PNG 文件名里直接带有“公司分布”“学历要求”“薪资”等汉字意味着原作者在画图前已经处理了 Matplotlib 的中文字体问题。Matplotlib 默认字体是 DejaVu Sans不支持中文直接plt.title(公司分布)会得到一排方块。血泪经验是先检查系统字体再设置 rcParams顺序不能反。import matplotlib.pyplot as plt import seaborn as sns import pandas as pd plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False all_data pd.read_csv(lagou_finall.csv, encodingutf-8-sig)font.sans-serif那行的意思是让 Matplotlib 优先用黑体或微软雅黑渲染文字。axes.unicode_minus如果不设成 False坐标轴负号会显示成方块因为默认字体没有对应的 unicode 负号字形。这一行配置是画图前必须固定下来的每次新建脚本我都会复制一遍属于不看就会翻车的细节。4.2 公司分布条形图value_counts 加水平条形图公司分布图是最简单但最直观的一类分析。逻辑上先用 value_counts 统计每家公司在数据里出现的次数取前 10 名画水平条形图。水平方向比垂直方向更适合长公司名这是我在项目中养成的习惯top10 all_data[公司名称].value_counts().head(10) plt.figure(figsize(10, 8)) top10.plot.barh(color#4C72B0) plt.title(Python 职位公司分布 Top10) plt.xlabel(职位数量) plt.tight_layout() plt.savefig(公司分布.png, dpi150)head(10)控制了条形数量防止小型创业公司几十个名字挤满图。figsize(10, 8)是横向宽 10 英寸、纵向高 8 英寸这个比例对 10 条条形图刚刚好。tight_layout()的作用是自动调整留白否则 x 轴标签经常被截掉一半。保存用dpi150是为了放进课程报告里放大看也不模糊默认的 100 dpi 打印出来边缘有锯齿。4.3 学历要求饼图与薪资箱线图用两种图表回答两个问题学历要求用饼图因为它是分类占比数据饼图一眼能看出本科和专科的份额差距。薪资分布用箱线图因为薪资字段是连续数值且存在离群值条形图会把几个“50k-80k”的高管岗位拉得其他数据全部看不清edu_counts all_data[学历要求].value_counts() plt.figure(figsize(8, 8)) edu_counts.plot.pie(autopct%.1f%%, startangle90) plt.title(学历要求分布) plt.savefig(学历要求.png, dpi150) plt.figure(figsize(12, 6)) sns.boxplot(dataall_data, x学历要求, y薪资上限) plt.xticks(rotation30) plt.title(各学历对应薪资分布) plt.savefig(薪资.png, dpi150)饼图里的autopct%.1f%%控制百分比标签保留一位小数startangle90让第一块扇形从 12 点方向开始。箱线图的 y 轴用薪资上限而不是薪资下限因为在招聘场景里上限代表公司愿意给的天花板更值得分析。细心的读者会发现这份包里每个城市单独生成了薪资、学历、公司分布三张图再加上合并后的薪资.png一共十张图对应的就是这套逻辑在每个城市数据上跑了一遍。如果有精力继续扩展我建议把箱线图按城市分面一张图同时展示上海、深圳、福州的薪资中位数和离散程度。做法是给原数据加一列城市然后sns.boxplot(dataall_data, x城市, y薪资上限)代码改动不超过三行但报告里就能直接对比三个城市的薪资差异比单独画三张图更有说服力。5. 拉勾爬虫避坑指南反爬识别、返回空数据、CSV 乱码与延时控制5.1 请求返回 200但 JSON 里 content 是空的现象requests.post 正常返回状态码是 200但解析出来的content为 None或者result是个空列表。原因拉勾服务端校验了请求头或 Cookie。只带 User-Agent 不带 Referer 的请求很容易落到风控逻辑里返回一个看似正常的空壳响应。另一种常见原因是first参数错误翻页时还带着true接口会认为你是异常请求。解决先 GET 一次职位列表页把返回的 Cookie 存到 requests.Session 里再用同一个 Session 去 POST 接口。同时检查first参数是否按页码切换。调试时先把返回的 JSON 打印出来看success字段如果success为 false多半是参数或 Cookie 有问题而不是网络问题。5.2 翻页到第三页就被封 IP后续请求全部 403现象前两页数据正常第三页开始所有请求都返回 403 或被重定向到验证页面。原因请求频率太高。拉勾的限流策略不是看单次请求速度而是看单位时间内的请求密度和规律性。固定 sleep 3 秒其实也容易被识别因为间隔时间完全一致机器节奏太明显。IP 被封之后换 User-Agent 也没有用因为封的是出口 IP。解决第一选择是降低频率并引入随机延时time.sleep(random.uniform(2, 5))是我个人的底线区间低于 2 秒风险陡增。第二选择是断开重连每次请求前重新建立 Session避免连接复用留下特征。如果只是课程作业完全不需要上代理池控制频率加单线程足够跑完 15 页数据了。为三十条数据去搭分布式爬虫是本末倒置。5.3 CSV 用 Excel 打开乱码或者少了一整列现象爬虫跑完CSV 生成用 Excel 打开后中文字符全是乱码但用记事本打开又正常。原因to_csv默认用 utf-8 编码保存Windows 版 Excel 默认按 GBK 解码编码不对应就是乱码。这个问题只在 Windows 平台出现Mac 和 Linux 上的 Pandas 用户很少遇到所以很容易被忽视。解决写 CSV 时指定encodingutf-8-sig。加个-sig的意思是让 Pandas 在文件开头写入 BOM 标记Excel 看到 BOM 就会自动切到 UTF-8 解码。这份包里所有 CSV 在 Windows 下能正常打开说明原作者应该也是吃了这个亏之后修正的。如果你要重新生成文件标准写法就是df.to_csv(output.csv, indexFalse, encodingutf-8-sig)5.4 合并后数据量虚高同一条职位出现多次现象爬完三个城市合并出的lagou_finall.csv有一万多行但明显感觉拉勾 Python 职位没那么多核对后发现大量重复记录。原因翻页时接口偶尔会重复返回同一职位尤其是每页最后几条和下一页开头几条重叠。这是招聘网站接口的常见行为不是拉勾独有的问题很多分页接口为了保障数据连续性都会对边界记录做一次重复下发。解决在合并步骤里做drop_duplicates(subset[职位名称, 公司名称, 城市])。这个操作必须在合并完成后统一执行不能分别对三个城市单独去重因为重复数据可能跨页面出现在同一城市文件里单独去重也能解决但统一去重还有一个额外好处能顺带检查三个城市之间是否存在同一公司发布的同名岗位。我一般还会在去重前后打印行数对比来确认到底删了多少重复数据before len(all_data) all_data all_data.drop_duplicates(subset[职位名称, 公司名称, 城市]) after len(all_data) print(f去重前 {before} 行去重后 {after} 行移除 {before - after} 行)5.5 学历字段混入“不限”和空值统计时占比失真现象画学历要求饼图时“不限”占了很大一块且有一部分行学历字段是空的导致其他学历类型的占比被严重稀释。原因拉勾接口对学历字段可能返回空字符串、不限、学历不限等多种写法爬虫代码如果没有统一格式这些值最终都会原样进 CSV。统计时它们被当成独立类目饼图就会多出几个莫名其妙的分类。解决清洗时做一层映射把不限、学历不限、None、空字符串统一替换为不限或者干脆在分析学历分布时过滤掉。如果分析目的是看企业要求保留“不限”是合理的如果只关注本科和专科对比就过滤掉。我更推荐保留但规范化而不是丢弃毕竟“不限”本身也是市场信号。6. 进阶验证用 Flask 加 ECharts 把这套 CSV 数据变成可交互的筛选看板到这一步其实 pandas matplotlib 已经完全满足课程作业的需求了但如果想让数据可视化分析作业看起来更有区分度或者你想在答辩现场直接演示一个能交互的界面最快的方式是给这套 CSV 数据套一层薄薄的 Web 接口前端用 ECharts 渲染。常见做法是我在本地起一个 Flask 应用把lagou_finall.csv读进内存暴露两个 API一个返回城市列表一个接受城市和字段参数返回统计数据。前端用一个静态 HTML 页面通过 fetch 拉数据再用 ECharts 画柱状图和箱线图。因为数据量不大全部走内存也不需要上数据库。from flask import Flask, jsonify, render_template import pandas as pd app Flask(__name__) data pd.read_csv(lagou_finall.csv, encodingutf-8-sig) app.route(/) def index(): return render_template(index.html) app.route(/api/company/city) def company_distribution(city): subset data[data[城市] city] company_counts subset[公司名称].value_counts().head(10) return jsonify({names: company_counts.index.tolist(), counts: company_counts.values.tolist()}) if __name__ __main__: app.run(debugTrue, port5000)这个接口的逻辑很直接按城市筛选 DataFrame取公司名做 value_counts转成两个列表塞进 JSON。前端 ECharts 拿这两个列表直接喂给xAxis.data和series.data即可。接口层做筛选而不是前端做筛选是因为前端拿到的只是聚合结果数据量小且逻辑简单放后端更好维护。验证这套数据可信度的方法我在每次跑完爬虫后会强制做一遍先断言lagou_finall.csv行数大于三个城市原始 CSV 行数之和的一半排除大面积去重异常再断言薪资上限列的中位数大于 0排除薪资解析全面失败的情况最后断言每个城市的职位数都大于零排除某个城市爬取时被风控截断。这三条断言是用 pytest 函数封装的几秒钟跑完比自己盯着控制台输出的数字可靠得多。我第一次跑这个包时贪快把 sleep 调成了 0.5 秒结果两页之后直接 403最终被迫换 IP 重来白白耽误了半小时。从那以后我每次跑爬虫都会强制走一遍“先单页调试确认字段、再看清洗结果、最后翻全量”的流程这个顺序能省下大量排错时间。这份包本身已经把流程走通了你拿到手要做的是在它的基础上改城市、改关键词、换图表类型然后把它真正变成你自己的分析作品。希望帮到你。本文还有配套的精品资源点击获取