毕业生就业数据爬取与清洗分析实战:从抓取到可视化报告

发布时间:2026/10/13 0:39:01
毕业生就业数据爬取与清洗分析实战:从抓取到可视化报告
简介面向Python爬虫与数据分析学习者这是一份以重庆大学毕业生就业数据为实例的完整项目资源覆盖从Scrapy框架爬虫采集、Pandas/NumPy数据清洗到Matplotlib可视化展示的全流程适合作课程设计、毕业设计或就业数据研究参考。压缩包共23个文件以16个Python脚本为主体包含爬虫核心模块spiders、pipelines、settings等和多份可视化脚本宣讲会行业/性质饼图、校园招聘趋势线图、就业单位去向饼图等另附3张结果图片及README说明文档整体仅116KB轻量紧凑、便于快速定位代码与成果。已有634人学习浏览资源中展示的爬虫框架搭建思路、数据提取规则以及针对就业率、行业分布、起薪水平等指标的可视化方法可直接参考或复用帮助读者快速掌握从网页数据获取到业务洞察的完整工程实践。1. 毕业生就业数据这活儿水有多深前阵子有位读者问我能不能把学校就业网上的毕业生去向数据自动抓下来做成一份能直接汇报的可视化报告真正上手后会发现这个需求远比“爬虫画图”复杂——数据源口径不统一、专业名称分层混乱、薪资字段动不动就缺失最后清洗花掉的时间往往是爬虫的三倍。这篇文章就沿着“数据源选型 → 抓取 → 清洗 → 分析 → 可视化”这条主线给出一套在本地就能跑通、能应对真实脏数据的方案。适合想练手爬虫和数据分析的初级开发者也适合需要定期产出就业分析报告的从业者。2. 定数据源与字段设计先定落点再写爬虫2.1 数据源对比选择灵活但口径统一的数据入口常见做法是从学校就业信息网、招聘平台公开数据或政府公开数据平台抓取。对练手和校内场景建议优先选学校就业信息网的“毕业生去向”栏目页面结构相对简单。但有两点必须提前确认第一页面是否由前端渲染如果是前后端分离的页面直接请求 HTML 拿不到表格需要找背后的 JSON 接口第二字段是否稳定如果不同年份的字段名不一致后期清洗会很痛苦。我一般会先打开开发者工具的 Network 面板筛选 Fetch/XHR 请求找到返回数据的接口然后在写正式代码前先用一个最小化请求验证。这里以某高校就业信息网的接口为例import requests import pandas as pd # 先用最小请求确认接口是否能直接返回 JSON url https://job.example-school.edu.cn/api/graduation/stat params { year: 2024, page: 1, page_size: 50 } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(url, paramsparams, headersheaders, timeout10) print(resp.status_code) print(resp.text[:500])这段代码的思路是先看返回状态码和数据格式而不是一口气写完整个爬虫。如果返回的是 HTML则说明真实接口另有其处如果返回 JSON再进一步看字典层级。timeout10是必设参数避免某个请求卡死拖垮整个抓取流程。params里的page_size要根据接口文档调整一次性拉取过多容易被服务端拒绝。2.2 字段设计把“就业数据”拆成可分析的维度在写爬虫前先设计一张目标字段表这能避免反复返工。我习惯把字段分成四类字段类别示例字段用途身份维度学号、专业、学历、性别、毕业年份做分组和切片去向维度就业、升学、待就业、创业计算就业率和去向构成单位维度单位名称、单位性质、行业类别、单位所在地分析行业和地域流向收入维度税前月薪、年薪、签约薪资薪资分析常常缺失一个容易忽略的点是“专业”字段在不同年份可能写法不同例如“计算机科学与技术”在某一年可能被写成“计算机科学”。如果不在抓取阶段把映射关系处理好分析阶段就得回头补数据。更稳妥的做法是额外维护一张专业别名表字段值只保留统一后的专业名。下面是一个简化版存储和归一化方案import json def normalize_major(raw): mappings { 计算机科学: 计算机科学与技术, 计科: 计算机科学与技术, 软件工程: 软件工程, 软工: 软件工程, } return mappings.get(raw.strip(), raw.strip()) raw_data [] # 假设这是从接口收集到的原始记录 cleaned_rows [] for item in raw_data: row { year: item.get(毕业年度), major: normalize_major(item.get(专业)), degree: item.get(学历), monthly_salary: item.get(税前月薪), industry: item.get(行业类别), destination: item.get(去向), } cleaned_rows.append(row) df pd.DataFrame(cleaned_rows) print(df.info())这段代码把字段名统一成英文小写并把专业别名映射到标准名。normalize_major的设计思路是“白名单映射”无法匹配的值原样返回这样既保留原始数据又不阻断清洗流程。真正的爬虫主体应加入异常捕获和增量存储避免临时内存里积累太多脏记录。2.3 爬虫策略限速、重试与增量抓取三件套爬取网站时最怕的不是被拒而是写出的代码把对方服务器压垮。常见做法是给每次请求加随机延时同时用requests.Session维持会话状态。对需要登录后可见的数据还要在请求头里带Cookie但注意不要把 Cookie 硬编码进公开仓库。另一个值得一开始就做的设计是增量抓取把已经抓到的记录主键例如“学号毕业年份”存入本地抓取前先查重。这样后续重跑脚本不会重复请求已抓取页面。这里给出一个带重试和限速的抓取函数import time import random from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def build_session(): session requests.Session() retry Retry(total3, backoff_factor1, status_forcelist[500, 502, 503]) adapter HTTPAdapter(max_retriesretry) session.mount(http://, adapter) session.mount(https://, adapter) return session def fetch_json(url, params, session): try: resp session.get(url, paramsparams, timeout10) resp.raise_for_status() return resp.json() except Exception as e: print(f抓取失败: {url} | {e}) return None finally: # 限速随机停留 1~3 秒避免高频请求 time.sleep(random.uniform(1, 3))Retry里的backoff_factor1表示重试间隔按 1 秒、2 秒、4 秒递增status_forcelist只对服务端异常状态码做重试。这里没有对 404 重试因为 404 大概率是数据不存在重试只会浪费时间。限速放在finally块里可以保证每次请求都留出固定间隔。3. 清洗与薪资口径归一分析前的必修课3.1 缺失值与异常值先统计再决定取舍拿到数据后不要直接分析第一步永远是df.info()和df.describe(includeall)。缺失值处理要看字段性质就业去向缺失往往是因为学生未离校或未填报不能简单删行薪资缺失比较常见更适合单独归为一档进行分析。也就是说不把缺失值一律填充成 0否则会把“无数据”和“真实低收入”混在一起。我常用的缺失值处理框架如下def handle_missing(df): df_copy df.copy() # 去向字段按“未填报”填充 df_copy[destination] df_copy[destination].fillna(未填报) # 薪资字段单独标记 df_copy[salary_flag] df_copy[monthly_salary].isna().map(lambda x: 缺失 if x else 有值) df_copy[monthly_salary] df_copy[monthly_salary].fillna(-1) # 行业字段无法判断用“未知行业”填充 df_copy[industry] df_copy[industry].fillna(未知行业) # 去除极端值月薪低于 1000 或高于 100000 的记录视为异常 df_copy df_copy[(df_copy[monthly_salary] 1000) | (df_copy[monthly_salary] -1)] df_copy df_copy[(df_copy[monthly_salary] 100000) | (df_copy[monthly_salary] -1)] return df_copy这里的关键不是把缺失值填出一个“假数据”而是让缺失值在后续统计里可以被单独识别。把薪资缺失值填成 -1再通过salary_flag来区分比直接删除或用均值填充更诚实。异常值过滤的边界则要根据行业常识调整如果分析对象以一线城市研发岗为主100 万上限可能偏低如果对象以文职岗为主这个边界可能又太高。3.2 薪资口径归一月薪、年薪、实习薪资别混着算这是毕业生就业数据分析里最容易翻车的环节。不同数据源的薪资字段口径差异极大有的写“税前月薪”有的写“年薪”有的写“实习日薪”。如果不做归一化统计结果毫无意义。常见做法是统一折算成“税前月薪”年薪除以 12时薪乘以 160按每月工作 160 小时估算日薪乘以 21.75劳动法约定月计薪天数。这里给出一个可复用的处理函数def normalize_salary(row): raw row.get(salary_raw, ) salary_type row.get(salary_type, 月薪) try: if salary_type 年薪: return round(float(raw) / 12, 0) elif salary_type 日薪: return round(float(raw) * 21.75, 0) elif salary_type 月薪: return float(raw) else: return None except (TypeError, ValueError): return None使用这个函数前务必先确认原始数据的单位是“元”而不是“千元”或“万元”。实际项目里见过不少把“8K”直接当成 8000 处理的翻车现场也有把“w”后缀当成普通数字的情况。我的经验是抓取时连同“薪资单位”“薪资周期”一起存下来清洗阶段再统一换算不要在地层就丢掉原始信息。3.3 专业与行业字段的归类别把 Excel 的事留给 Python 硬扛专业归类可以交给别名映射表行业归类则更复杂。公开数据里的行业分类常常是“信息传输、软件和信息技术服务业”这类长名称直接作为可视化标签会非常拥挤。常见做法是建立“大类映射表”把细分行业归入若干大类。这个过程宁可手动维护 Excel 映射表也不要在代码里写一堆 if else否则每次新增数据都要改代码。INDUSTRY_GROUP { 信息传输、软件和信息技术服务业: IT/互联网, 互联网和相关服务: IT/互联网, 软件和信息技术服务业: IT/互联网, 教育: 教育, 高等教育: 教育, 制造业: 制造, 汽车制造业: 制造, 医药制造业: 制造, } def group_industry(industry_name): if not industry_name: return 未知 return INDUSTRY_GROUP.get(industry_name.strip(), 其他)归类时要注意字符串两端的空格和全半角差异“信息传输、软件和信息技术服务业”与“信息传输软件和信息技术服务业”是两个不同字符串。如果映射表里都没命中先归为“其他”不要直接报错中断整个流程避免新数据源出现时爬虫直接罢工。4. 分析模型与指标选择把“就业率”拆成能下决策的维度4.1 就业率与去向构成绕开单一指标陷阱毕业生就业分析里最常见的指标是“就业率”但单一就业率信息量很低——它无法回答“就业质量如何”“去了哪些行业”“薪资分布是否合理”这些问题。我一般会把指标拆成三层总体去向构成、专业去向差异、薪资分布形态。去向构成用分组计数即可重点在于可视化时如何避免误导性表达。dest_distribution df.groupby([year, destination]).size().reset_index(namecount) total_by_year df.groupby(year)[学号].count().reset_index(nametotal) merged pd.merge(dest_distribution, total_by_year, onyear) merged[percentage] merged[count] / merged[total] * 100 print(merged.head())groupby后算的是绝对值与总数合并后才有意义。这里没有直接用value_counts(normalizeTrue)原因是后续还要按年份、学院、专业多个维度钻取先保留明细再聚合更灵活。计算百分比时注意分母不要选错在就业去向分析中分母一般是有明确去向记录的人数而不是全体应届生否则“未填报”也会拉低就业率。4.2 薪资分析中位数比平均值更可靠分组也别只看均值薪资数据普遍右偏少数高薪岗位会把平均值拉得很高这时用中位数描述典型水平更合适。按“学历行业”分组后同时输出均值、中位数、样本量和缺失率缺失率过高时直接放弃该组的薪资分析。这里给出一段分组统计代码salary_stats ( df[df[salary_flag] 有值] .groupby([学历, industry]) .agg( 样本数(学号, count), 平均月薪(monthly_salary, mean), 中位月薪(monthly_salary, median), 最低月薪(monthly_salary, min), 最高月薪(monthly_salary, max), ) .reset_index() ) # 过滤掉样本数过少的组避免统计结论失真 salary_stats salary_stats[salary_stats[样本数] 5]筛选样本数至少为 5 是一个很保守的下限。如果某个学历行业组合只有 2 条记录算出来的均值没有任何代表性。实际项目中可以把阈值提高到 10 或 20。一个更合理的输出是同时显示“样本数”和“缺失率”如果缺失率超过 40%就应当在报告中标注“本组薪资数据参考价值有限”。4.3 地域流向与行业变化把可视化结论做成“可追问”的分析地域流向时常见做法是解析单位所在地字段中的城市信息然后按省份聚合。这里要特别注意“省份”和“城市”的层级混用问题有的记录写“广东省”有的写“深圳市”不能直接按字符串分组。常见做法是维护一份“城市→省份”映射表先用城市名匹配匹配不到再按省份名处理。行业年度变化分析更适合用透视表输出按年份查看各行业占比变化。实现如下industry_cross pd.pivot_table( df, values学号, indexindustry, columnsyear, aggfunccount, fill_value0 ) # 转成占比 industry_ratio industry_cross.div(industry_cross.sum(axis0), axis1) * 100 print(industry_ratio.round(1))div的方向容易写错industry_cross.sum(axis0)得到的是每年各行业的签约总人数按列对齐后得到“当年行业内占比”而不是“行业内部年份占比”这两者的解读完全相反。建议生成这样的表之后人工抽查一两个数字用原始明细验证避免透视表操作出错却不自知。5. 爬取与分析常见问题排查五个踩坑记录5.1 页面表格结构对不上改版后字段顺序变了现象某个月份抓回来的数据专业列和学历列串位分析时发现“学历”里出现专业名称。原因就业信息网站改版后调整了表格列顺序而爬虫还是按旧列索引取值。解决解析表格时不要死记列序号改用表头文本匹配列名找不到对应列时直接报警告并跳过该行。def extract_table_row(headers, cells): row {} header_index {h.strip(): i for i, h in enumerate(headers)} row[专业] cells[header_index.get(专业, -1)].text.strip() row[学历] cells[header_index.get(学历, -1)].text.strip() return row这段代码先建立“表头文本→列索引”的映射再取值。表头一旦变化使用get的默认值 -1 会出现cells[-1]这种错误反而更容易暴露问题。实际项目里应该加一条校验逻辑如果某个关键字段取到空值就把整行打印出来人工检查而不是默默填充。5.2 中文编码乱码requests 拿回来的是 GBK现象打印出来的响应文本是大量乱码但浏览器里显示正常。原因网站使用的是 GBK/GB2312 编码而requests默认按ISO-8859-1或utf-8猜测编码。解决从响应头或 HTML 的meta标签里确认编码并显式设置resp.encoding。resp requests.get(url, headersheaders, timeout10) # 手动指定编码避免乱码 resp.encoding gbk print(resp.text[:200])还有一种更稳的方式是使用apparent_encoding来自动检测但检测结果并非总是正确。尤其是页面本身混合了多种字符时直接指定通常比自动检测更可靠。读取 CSV 时也容易踩同样的坑建议在pd.read_csv里显式传encodingutf-8或encodinggbk。5.3 速率过快触发反爬请求间歇性失败现象前几百条数据抓取正常随后出现大量超时和 403重试也救不回来。原因请求频率超过了站点阈值IP 被临时限流。解决限速间隔从time.sleep(1)调整为随机1~3秒并观察响应头里是否有Retry-After字段如果使用多线程抓取不限制并发数也会导致同样的问题。A同学曾用ThreadPoolExecutor(max_workers20)并发抓取半小时后被网站封禁。后来改成单线程限速抓取虽然慢但再也没有丢数据。对这种内部页面抓取速度不重要稳定才是第一位。5.4 生成的图表中文全是方块现象用 matplotlib 画图时中文标签显示为方框。原因系统默认字体不包含中文字符。解决在绘图中显式指定中文字体例如 Windows 下的SimHei或 macOS 下的Arial Unicode MS。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False # 解决负号显示异常axes.unicode_minus这个参数很容易遗漏坐标轴出现负号时默认字体也显示不出来。换用其他中文字体时font.sans-serif需要提供一个列表matplotlib 会按顺序匹配第一个可用的字体。5.5 薪资字段出现“面议”类型转换直接把整列变成 NaN现象pd.to_numeric整列转换后薪资列原本有值的数据也变成 NaN。原因列里混入“面议”“年薪面议”等非数字文本errorsraise会直接报错errorscoerce则把非数字全部变成 NaN。解决转换前先识别非数字取值单独标记为“面议”再对数字部分做数值转换。import numpy as np df[salary_numeric] pd.to_numeric( df[salary_raw].astype(str).str.replace(r[元/月薪], , regexTrue), errorscoerce ) df.loc[df[salary_raw].astype(str).str.contains(面议), salary_numeric] np.nan df.loc[df[salary_numeric].isna(), salary_flag] 面议或缺失正则替换要小心不要把所有非数字字符都删掉比如“万元”中的“万”就包含单位语义。如果遇到“8万/年”astype(str)后应先处理成月薪再降为数值。常见顺序是先统一单位再做文本清洗最后转数值类型顺序颠倒就会出现虚高或虚低的数据。6. 多报表自动化一套代码跑通周报当数据清洗和分析流程稳定后下一步是模板化输出的周报。目标是一份独立可发布的 HTML 报告包含总就业率、分专业去向表、行业占比柱状图和薪资分布箱线图。这里分享一个模板化生成技巧比一次性写脚本更利于复用用pandas聚合数据写入模板变量再用str.replace替换 HTML 模板中的占位符。def render_report(df, template_path, output_path): # 汇总关键指标 total_count len(df) employment_rate ( df[df[destination].isin([就业, 升学])].shape[0] / total_count * 100 ) # 行业分布转为 JSON 供前端图表使用 industry_stats ( df.groupby(industry)[学号].count() .sort_values(ascendingFalse) .head(10) .to_json(force_asciiFalse) ) with open(template_path, r, encodingutf-8) as f: html_template f.read() html_content ( html_template .replace({{TOTAL_COUNT}}, str(total_count)) .replace({{EMPLOYMENT_RATE}}, f{employment_rate:.2f}%) .replace({{INDUSTRY_JSON}}, industry_stats) ) with open(output_path, w, encodingutf-8) as f: f.write(html_content)用模板字符串而不是在代码里拼接 HTML好处是页面结构调整时不用动 Python 逻辑让负责风格的人去改模板文件就行。to_json(force_asciiFalse)保证中文不被转成\uXXXX前端拿到后可以直接渲染。每周只要重新跑一遍数据抓取和清洗流程然后执行render_report一份新周报就完成了。实际运行的验证手段很直接抓取阶段检查每个年份的记录数与上年差异清洗阶段打印缺失率统计分析阶段抽查几个已知结论。比如上个月某专业就业率明显下降新跑的数据是否还是同样趋势如果趋势变了先怀疑清洗逻辑而不是数据本身。我的习惯是保留每个阶段的中间结果到文件里原始 JSON、清洗后 CSV、分析中间表任何一步出问题可以回溯不用重新抓一遍网页。这套流程的维护成本并不在代码而在数据源改版和新字段的持续迭代。把字段映射表、专业别名表、行业归类表做成外部配置文件而不是写死在代码里后续维护会轻松很多。希望这一整套做法能在你实际做就业数据分析时少走几步弯路帮到你少加班多产出。本文还有配套的精品资源点击获取