ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Boss直聘数据爬取与可视化分析:从requests到pyecharts完整实战

2026/9/15 16:51:08 拓冰建站 浏览量
Boss直聘数据爬取与可视化分析:从requests到pyecharts完整实战 简介这套Python期末大作业围绕Boss直聘职位数据实现从在线爬虫采集、数据清洗到可视化分析展示的完整流程。项目面向计算机相关专业学生尤其适合需要完成课程设计、期末大作业或毕业设计的初学者评审得分99分代码完整、可直接运行配有文档说明能快速上手。压缩包共554个文件大小7.37MB其中25个Python脚本承担爬虫与数据分析核心逻辑229个JS与107个HTML构建了前端展示界面61个CSS负责样式另有54个PNG、19个JPG等图片素材以及JSON、CSV、Pickle数据文件分层清晰。已有144人学习下载。通过该项目可掌握requests/Scrapy等爬虫编写、Pandas数据处理、ECharts可视化等关键技能并参照文档理解项目结构、调试思路与扩展方向是实战练手与作业交付的高性价比选择。1. 做招聘数据的 Python 爬虫项目最难的不是爬而是让数据“能分析”做招聘方向的期末项目最尴尬的不是不知道分析什么而是作为 Python 爬虫数据源的数据全靠手点。Boss直聘岗位多、更新快用手点出来的几十条样本别说做数据分析可视化连画饼状图都撑不起分组。这套“Boss直聘在线爬虫及数据分析可视化系统”源码的思路很直接用 Python 的 requests 直接请求招聘接口把岗位、薪资、城市、学历、经验字段落到本地结构化文件再用 pandas 清洗成可计算的数据集最后用 pyecharts 生成可视化面板。适合正在做 Python 期末大作业的学生也适合想拿真实业务数据做练手项目的开发者——这套链路踩过的坑比看十篇教程都值。2. Boss直聘数据采集请求链路、请求头伪装与反爬规避策略爬虫项目最不重要的反而是“跑通一次”真正决定项目成败的是能不能稳定连续地采集到足够样本数。Boss直聘页面本身是服务端渲染加前端异步请求的混合结构直接解析 HTML 会陷入字段不完整、页面结构改版就报废的困境。常见做法是直接用 requests 打接口拿 JSON字段干净、解析成本低也方便后续接入 pandas。2.1 抓包定位先弄清楚页面数据是从哪来的在写任何爬虫代码之前先做抓包定位。这一步用不了十分钟能省掉后面几小时的排查时间。我的固定流程是打开求职页面按 F12 进入开发者工具切到 Network 面板勾选 Fetch/XHR 过滤只保留异步请求在搜索框输入目标岗位关键词观察新增请求逐个查看响应体找到返回岗位列表 JSON 的那个接口确认字段结构Boss直聘的搜索接口路径和字段名会定期调整因此在源码或脚本里把接口写死之前务必打印一次实际响应确认jobName、salaryDesc、cityName这些字段真实存在。提示接口字段变动是这个项目最常见的“跑不动”原因。如果请求返回 200 但解析后是空列表优先去 Network 面板看实时响应结构而不是怀疑请求头配置。2.2 请求头伪装与登录态直接决定采集能否连续生效不登录直接访问页面时服务端拿到的身份信息非常有限连续请求几十次后如果 User-Agent 固定、无 Referer、请求间隔恒定很容易被识别为脚本流量。因此这个环节的核心是会话维持和请求头伪装。import requests import random import time from fake_useragent import UserAgent class JobSpiderSession: def __init__(self, cookies_str: str): self.session requests.Session() self.ua UserAgent() self.session.headers.update({ Referer: https://www.zhipin.com/web/geek/job, Accept: application/json, text/plain, */*, User-Agent: self.ua.random, }) self.session.cookies.update(self._parse_cookie(cookies_str)) def _parse_cookie(self, cookie_text: str) - dict: pairs {} for item in cookie_text.split(;): if in item: k, v item.strip().split(, 1) pairs[k] v return pairs def fetch_json(self, url: str, params: dict) - dict: resp self.session.get(url, paramsparams, timeout10) if resp.status_code 200: return resp.json() if resp.status_code in (403, 412): raise RuntimeError(触发风控请检查Cookie是否过期或降低采集频率) resp.raise_for_status()代码逻辑说明requests.Session()会复用底层 TCP 连接和 Cookie避免每次请求重新握手连续翻页时性能差距非常明显fake_useragent里的UserAgent().random每次调用都会返回一个不同的浏览器 UA部署在没有外网环境的机器上时建议准备一个本地 UA 列表避免在线获取失败_parse_cookie把浏览器复制出来的 Cookie 文本拆成字典再通过cookies.update()注入会话。登录后的 Cookie 是维持接口访问权限的关键采集前先手动登录一次再从开发者工具里复制完整的 Cookie 文本2.3 分页与限速别让采集器变成压力测试工具确认单页请求能通之后再考虑翻页。分页参数常见的是page和query响应体里通常有jobList或类似字段。写翻页逻辑时要有两个明确的边界最大页数和失败退出条件。def collect_jobs(spider, base_url, query, max_pages10): all_items [] for page in range(1, max_pages 1): params {query: query, page: page} try: data spider.fetch_json(base_url, params) job_list data.get(zpData, {}).get(jobList, []) if not job_list: print(f第{page}页无数据停止翻页) break all_items.extend(job_list) except RuntimeError as e: print(f第{page}页失败: {e}) time.sleep(60) continue delay random.uniform(2, 5) time.sleep(delay) return all_items参数说明max_pages建议调试时先设成 3流程跑通后再放开。一次性大规模采集如果触发风控连调试机会都没有这个代价远大于多跑一次脚本的时间成本random.uniform(2, 5)表示每次请求后随机等待 2 到 5 秒比固定 sleep 3 秒更接近操作节奏这也是单机爬虫里最常用的限速策略捕获RuntimeError后退避 60 秒再继续下一轮比立刻重试更有效403 或 412 这类响应通常有持续风控窗口短时间重试只会拉长封禁时间2.4 常见状态码与处理策略一份可以对照的排错表状态码含义处理策略200请求成功正常解析 JSON302重定向到登录页Cookie 失效重新登录并更新 Cookie403拒绝访问降低频率检查 UA 和 Referer 是否完整412前置条件失败风控校验未通过需停采一段时间再继续418被识别为脚本立即停止采集检查请求间隔和请求头配置这些状态码里最容易被忽略的是 302。因为 requests 默认会自动跟随重定向表面上还是返回 200但响应体变成登录提示页解析出来的数据全部为空。遇到这种“响应正常但没有数据”的情况先关闭自动重定向看一次真实状态码。3. 数据清洗与职位薪酬分析从 JSON 到可分析的 DataFrame爬下来的 JSON 不能直接进图表。原始数据里的薪资字段是“15-20K·14薪”这种混合字符串岗位标签是一层套一层的列表字典不经过标准化处理pandas 根本没法做分组聚合。这一章把“JSON 转 DataFrame”的完整链路拆开写出能直接落地的清洗函数。3.1 JSON 字段抽取明文数据也要过一道防御式解析接口返回的每条岗位记录是一个多层嵌套的字典这里只抽取分析会用到的字段其余冗余字段全部丢弃能减少内存占用也让后面检查数据时更直观。import pandas as pd def parse_job_item(item: dict) - dict: return { job_name: item.get(jobName, ), salary_text: item.get(salaryDesc, ), city: item.get(cityName, ), district: item.get(areaDistrict, ), experience: item.get(jobExperience, ), education: item.get(jobDegree, ), company: item.get(brandName, ), industry: item.get(brandIndustry, ), labels: |.join([lab.get(name, ) for lab in item.get(jobLabels, [])]), }逻辑说明所有字段都用.get()带默认值单个字段缺失时不会中断整条记录的解析。这是防御式解析的核心思想宁可字段为空不让清洗流程整体崩掉jobLabels是一个列表列表里每个元素是包含name键的字典用列表推导式取出名称后用|拼接。实际接口里这个字段偶尔会是None因此稳妥的写法是先判断类型再遍历返回的是一个新字典字段名统一转成小写下划线风格方便后面创建 DataFrame3.2 薪酬文本标准化从“15-20K·14薪”到数值区间Boss直聘薪酬文本格式比较统一基本是“最低-最高K”或“最低-最高K·薪期”但个别岗位会有小数或是“薪资面议”这些边界情况都要处理。import re def parse_salary(salary_text: str): if not salary_text or not isinstance(salary_text, str): return None, None, None nums re.findall(r(\d(?:\.\d)?), salary_text) if len(nums) 2: return None, None, None low, high float(nums[0]), float(nums[1]) return low * 1000, high * 1000, (low high) / 2 * 1000参数说明re.findall按数字提取薪资文本中的数值\d(?:\.\d)?同时兼容整数和小数len(nums) 2时说明是“薪资面议”或缺失文本直接返回三个None后续清洗时统一过滤返回值设计成下限、上限、中位数三个数值单位统一换算成元/月避免图表里出现 K 和元混用做平均薪资聚合时用中位数而不是上限因为上限代表理想情况中位数能压掉个别虚高岗位的长尾影响结论更有参考意义3.3 数据去重与异常过滤可分析数据集的最后一道门槛df df.drop_duplicates(subset[job_name, company, salary_text, city]) df df[df[salary_low].notna()] df df[df[salary_low] 0] df.to_csv(jobs_cleaned.csv, indexFalse, encodingutf-8-sig)这里去重用“岗位名 公司 薪资文本 城市”四字段联合判断而不是只看公司名因为同一家公司会在不同城市发布大量相似岗位按公司去重会把有效样本全部删掉。清洗后的数据用to_csv落盘注意encodingutf-8-sig这个参数——不加的话用 Excel 打开生成的 CSV 会出现中文乱码评审现场非常尴尬。注意如果去重后数据量骤减到原始数据的四分之一以下说明采样窗口内重复岗位占比过高优先调整采集关键词和时间跨度再重新跑采集脚本不要在清洗阶段硬凑样本量。3.4 字段联动校验在进入可视化之前做一次质量体检清洗流程结束时建议打印一份简单的质量报告总行数、城市覆盖数、薪资缺失数。这一步不需要复杂工具手动在脚本里加几行就能完成。print(f清洗后总记录数: {len(df)}) print(f覆盖城市数: {df[city].nunique()}) print(f薪资字段为空数: {df[salary_low].isna().sum()}) print(城市分布:\n, df[city].value_counts().head(10))这份报告的价值在于如果城市分布里出现了“北京市”和“北京”两种写法并存说明城市字段有命名不一致的问题要提前做归一化处理否则后面按城市聚合时会把同一城市拆成两行。很多可视化图表“看起来不对”的根因其实在清洗阶段就已经埋下了。4. 可视化面板与多维度交叉分析让结论自己说话数据清洗完成后下一步是用 pyecharts 生成可视化图表。这里不引入 Flask 或 Django 做动态站点直接用 pyecharts 渲染独立 HTML 文件双击就能在浏览器打开期末展示和答辩现场演示都足够。生成静态文件还有一个好处不需要额外启动服务不会因为端口被占用或依赖缺失而翻车。4.1 图表选型什么维度配什么图分析维度建议图表统计口径岗位名称词频横向柱状图按岗位名计数取 Top15薪资分布区间直方图按薪资中位数分箱城市岗位量条形图按城市聚合岗位数学历与薪资关系分组柱状图按学历分组计算平均薪资经验与岗位需求折线图按经验等级聚合选型原则很朴素单变量看分布用直方图多变量对比用分组柱状图多对象排序用条形图。尽量避免使用饼图当分类超过五类时饼图的面积对比很难直观读取答辩时容易被打断提问。4.2 城市与薪资交叉分析一张图讲清市场需求from pyecharts.charts import Bar from pyecharts import options as opts city_salary df.groupby(city)[salary_mid].mean().sort_values(ascendingFalse) bar Bar() bar.add_xaxis(city_salary.index[:10].tolist()) bar.add_yaxis( series_name平均薪酬(元/月), y_axiscity_salary.round(0).values[:10].tolist(), label_optsopts.LabelOpts(formatter{c}) ) bar.set_global_opts( title_optsopts.TitleOpts(title招聘城市平均薪酬 Top10), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate15)), ) bar.render(city_salary_bar.html)代码逻辑说明groupby(city)[salary_mid].mean()按城市聚合薪资中位数的平均值算的是每个城市所有样本的平均水平sort_values(ascendingFalse)先整体排序再取前 10否则切片结果是原始顺序图表会失真rotate15让城市名在横轴上旋转 15 度避免顶部城市名密集叠压formatter{c}把具体数值直接显示在柱子上方评审现场不需要盯着坐标轴估算4.3 学历与薪资关系分析分组柱状图才是主菜学历和薪资的关系是招聘分析里几乎必被追问的维度。这里用 pyecharts 的Bar配合add_yaxis多次添加系列就能实现分组柱状图。edu_order [学历不限, 大专, 本科, 硕士, 博士] edu_salary df.groupby(education)[salary_mid].mean() bar_edu Bar() bar_edu.add_xaxis([edu for edu in edu_order if edu in edu_salary.index]) for level in [大专, 本科, 硕士]: if level in edu_salary.index: bar_edu.add_yaxis(series_namelevel, y_axis[edu_salary[level]]) bar_edu.set_global_opts(title_optsopts.TitleOpts(title不同学历平均薪资对比)) bar_edu.render(edu_salary_bar.html)这里需要注意edu_order的作用是固定学历展示顺序因为 pandas 的groupby默认按字母排序“博士”会排在“大专”前面这在中文语境里看起来非常别扭。遍历时加了if x in edu_salary.index判断避免部分样本量过小的学历等级不存在时抛出 KeyError。4.4 生成可离线查看的 HTML 报告与本地看板from pyecharts.components import Table table Table() headers [城市, 岗位数, 平均薪资(元/月), 最高薪资(元/月)] rows [] for city, group in df.groupby(city): rows.append([ city, len(group), int(group[salary_mid].mean()), int(group[salary_high].max()), ]) table.add(headers, rows).render(summary_table.html)用Table组件把聚合结果渲染成离线 HTML 表格和前面生成的柱状图放在同一个目录就是一套完整的数据分析可视化报告。如果想在答辩时一页页翻看可以用 pyecharts 的Page组件把多个图组合到单个 HTML 文件里翻页效果比逐个打开文件自然得多。注意最终输出的 HTML 文件依赖本地 echarts.min.js换电脑展示的时候要把整个目录一起拷走不能只带 HTML 文件。5. 部署排错与低成本扩展从跑通到能接住真实需求系统能在本地跑通只是第一步。这里分享三个实际使用中高频出现的操作技巧覆盖校验、周期性采集和排错路径。数据校验方面建议在清洗后增加一个校验脚本核对前后行数差、薪资空值数量和城市覆盖数。这三个数值分别对应采集是否完整、清洗是否合理、字段是否统一。实际做法是定义check.py读取jobs_cleaned.csv打印上述指标发现问题时优先回头检查清洗函数而不是重新采集。周期性采集可以把主流程包成main()函数再配合系统定时任务每天执行一次输出文件名带上日期例如jobs_2025-06-01.csv。积累两周后这批带时间戳的历史数据就能支撑岗位需求趋势变化分析比一次性抓几百条样本的结论要有说服力得多这也是从“期末作业”向“课程设计”质量靠拢的关键差异。扩展方向上不建议直接上多线程并发。招聘类站点对单 IP 的请求频率敏感多线程很容易触发风控阈值。更稳妥的方式是一台机器跑一个关键词分时段错开启动产物按关键词分目录存放最后再合并这就是最轻量的爬虫并发设计思路。至于分布式爬虫在这个项目体量下完全用不到引入只会增加部署复杂度。最后排错技巧。做完可视化发现城市分布始终只有两三个城市大概率不是采集没跑全而是清洗时城市字段的命名规则没有覆盖到行政区划变更后的新名称。手动查看df[city].unique()的取值用replace把“北京市”和“北京”这类同义写法统一再重新生成图表比重新采集数据快得多。本文还有配套的精品资源点击获取