
简介这是一份基于Python实现的Boss直聘岗位数据采集与分析可视化项目面向计算机相关专业正在准备期末大作业、课程设计和毕业设计的学生也适合希望提升爬虫与数据分析实战能力的开发者。项目完整覆盖岗位数据抓取、清洗、对比分析与可视化展示流程采集方向包括大数据、数据分析、数据挖掘、机器学习、人工智能等热门岗位。压缩包共38个文件体积约245KB主要包括13个py源码文件、12个js脚本、4个xml配置文件和1个csv岗位数据文件py与cfg文件构成Scrapy爬虫项目核心js配合html/css实现前端图表与交互csv保存全国热门城市岗位数据md文档提供使用说明与设计思路。资源已有104人学习/下载代码通过本地编译并严格调试可直接运行。借助详细文档可快速复现从数据采集到分析薪资、学历、区域、行业与技能要求的全过程支撑课程报告和毕业设计答辩也能为后续二次开发提供基础。1. 招岗位数据不能只靠看应该把采集、分析和可视化做成一条流水线求职者看数据分析岗位的薪资通常只看几个招聘 App 的搜索结果看到的是平台推荐算法给你的局部样本想了解“哪个城市给钱多、什么经验门槛占比高、哪些技能出现频率最高”看目录页和分析报告都容易过时。更好的办法是用 Python 自己搭一条 Boss直聘岗位数据采集及分析可视化流水线先用爬虫采集岗位列表再清洗成结构化数据最后用图表把薪资分布、经验要求、技能标签呈现出来。这个项目最大的价值在于它能同时训练数据采集、pandas 清洗、可视化展示和工程化组织能力而且可以迁移到任何招聘、电商、资讯类网站。需要注意我不建议直接下载现成的源代码包就跑因为招聘网站的页面结构和反爬策略变化过快跑不通时你根本不知道改哪里从请求入口到字段解析再到图表输出每一层都要能自己改。2. 采集层设计定位 Boss直聘岗位数据接口与请求参数2.1 先确认边界数据采集的合规前提与反爬风控底线开发者的第一反应是“拿到 cookie 就能爬”但更重要的第一步是确认数据边界。公开页面上能看到的岗位名称、薪资区间、公司名称属于公开商业信息个人少量采集用于学习分析问题不大但如果要做成商业产品、批量转售、或采集用户联系方式就会涉及法律与平台条款风险。我的处理原则是只采集搜索结果列表不做登录绕过、不破解加密签名、不抓验证码控制请求频率不给对方服务器增加压力。开始写代码前可以先看下平台的 robots 约定import requests resp requests.get(https://www.zhipin.com/robots.txt, timeout10) print(resp.status_code) print(resp.text[:500])这段代码用 requests 请求 robots.txt输出前 500 个字符。它能让你快速了解该域名下哪些路径被允许或禁止爬取减少后续违规风险。需要注意robots.txt 是道德约束而非技术强制真正的边界还要结合平台服务条款来判断。2.2 确定采集入口URL 参数、城市编码与 keyword 关键词Boss直聘网页端的岗位搜索入口通常是一个带 query、city、page 参数的 GET 请求。以北京为例搜索“数据分析”第一页的 URL 大致长这样https://www.zhipin.com/web/geek/job?query数据分析city101010100page1query 表示搜索关键词city 是城市代码page 是页码。这个接口返回的是 HTML 页面包含岗位卡片、公司名称、薪资等公开字段。城市代码不要靠猜常用城市我一般维护成一张配置表城市城市代码 city请求示例北京101010100city101010100上海101020100city101020100深圳101280600city101280600广州101280100city101280100杭州101210100city101210100我一般会把这些城市代码放进 config.py而不是散落在主脚本里。因为后面做多城市对比时要频繁循环该配置集中管理可以避免改动时漏掉某一处。2.3 用 requests BeautifulSoup 解析岗位卡片字段在没有验证码、并手动从浏览器复制自己 cookie 的前提下常见做法是构造 headers 发起请求。Cookie 不要写死在代码里我放在环境变量中避免提交 Git 时泄露账号信息。import os import random import time import requests from bs4 import BeautifulSoup import pandas as pd HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36, Cookie: os.environ.get(BOSS_COOKIE, ), } def fetch_job_page(keyword, city_code, page): params { query: keyword, city: city_code, page: page, } resp requests.get( https://www.zhipin.com/web/geek/job, paramsparams, headersHEADERS, timeout15, ) resp.raise_for_status() return BeautifulSoup(resp.text, html.parser)这段代码做了三件事把 query、city、page 交给 params 让 requests 自动做 URL 编码设置 UA 和 Cookie用raise_for_status()在请求失败时快速抛异常。需要注意Cookie 必须是从已登录浏览器复制的未登录状态往往拿不到完整岗位列表而且频繁切换登录态容易触发风控。拿到 HTML 后用 BeautifulSoup 选择器定位岗位卡片。当前页面上岗位卡片的特征类名是li.job-card-wrapper但这类产品改版比较频繁下面代码里的选择器需要在你实际跑的时候优先确认。def parse_job_cards(soup): records [] for card in soup.select(li.job-card-wrapper): try: title card.select_one(.job-name).text.strip() salary card.select_one(.salary).text.strip() company card.select_one(.company-name).text.strip() area card.select_one(.job-area).text.strip() info_spans card.select(.job-info span) labels card.select_one(.job-labels) skills |.join([x.text for x in labels.select(span)]) if labels else records.append({ 岗位: title, 薪资: salary, 公司: company, 城市: area, 经验: info_spans[0].text.strip() if len(info_spans) 0 else , 学历: info_spans[1].text.strip() if len(info_spans) 1 else , 技能标签: skills, }) except AttributeError: continue return records这个函数用card.select_one()提取单个字段并统一放进字典。info_spans里通常按顺序存放经验、学历、工作年限等信息但它的索引顺序并不是百分之百固定所以代码里做了长度判断避免 IndexError。技能标签存在多个值时我使用竖线分隔因为后续做词频统计时需要把多个 tag 拆开取词。采集多页时必须控制访问节奏all_records [] for page in range(1, 6): soup fetch_job_page(数据分析, 101010100, page) all_records.extend(parse_job_cards(soup)) time.sleep(3 random.uniform(0, 2)) df pd.DataFrame(all_records) df.to_csv(data/raw_jobs.csv, indexFalse, encodingutf-8-sig)time.sleep(3 random.uniform(0, 2))让每次请求之间至少间隔 3 秒并加入随机浮动避免访问频率形成固定节律此处的 page 范围 1 到 5 是根据搜索结果总量设定的如果页面总数不足 5 页需要先解析总页数再截断否则会采集到空列表。最后以utf-8-sig编码存 CSV是因为用 Excel 打开包含中文的 CSV 时带 BOM 可以避免乱码。3. 数据清洗把 Boss直聘岗位数据变成可分析的结构化表3.1 薪资字段解析统一为月薪并保留特殊计薪方式采集到的薪资文本五花八门常见形式有15-30K、13K-20K·14薪、200-300/天。直接当作字符串没法做数值计算需要先解析出最低月薪和最高月薪。我使用正则表达式对文本做匹配import re import numpy as np import pandas as pd df pd.read_csv(data/raw_jobs.csv, encodingutf-8-sig) df df.drop_duplicates(subset[岗位, 公司, 薪资]) def parse_salary_full(value): value str(value).replace( , ) if 天 in value or 小时 in value: nums [float(x) for x in re.findall(r\d\.?\d*, value)] return nums[0] if nums else None, None, 非月薪 match re.search(r(\d\.?\d*)\s*[-~至]\s*(\d\.?\d*)K, value) if match: low float(match.group(1)) * 1000 high float(match.group(2)) * 1000 return low, high, 月薪 return None, None, 面议 df[月薪下限], df[月薪上限], df[薪资类型] zip(*df[薪资].apply(parse_salary_full)) df[平均月薪] (df[月薪下限] df[月薪上限]) / 2这段代码的关键是区分三种情况。第一种“非月薪”的日薪、时薪不适合混入月薪分布先用“非月薪”标记出来第二种标准区间15-30K提取上下界并乘以 1000第三种无法识别标记为“面议”后续分析中按缺失值处理。使用zip(*...)把函数返回的三列同时映射到 DataFrame 的三列比逐行 apply 更简洁。如果你确实想把日薪纳入统一比较一种折中方案是按 21.75 个工作日估算月薪但我不会把它和正式月薪放在同一张图里因为日薪岗位通常包含兼职和临时岗位薪资语义完全不同。3.2 经验、学历与技能标签的归一化采集文本里的经验值常常是“1-3年”“3-5年”“10年以上”直接排序时字符串顺序会错乱。处理经验字段时我使用 pandas 的类别类型exp_order [在校/应届, 1年以内, 1-3年, 3-5年, 5-10年, 10年以上] df[经验] pd.Categorical(df[经验], categoriesexp_order, orderedTrue) df[学历] df[学历].str.replace(及以上, , regexFalse)pd.Categorical设置有序类别后5-10年才能正确排在1-3年后面replace(及以上, )把“本科及以上”统一成“本科”减少聚合时的分组数量。这里要注意学历字段也可能出现“大专/本科”“学历不限”等复合描述字符串替换是粗粒度压缩更细的分支需要人工看数据分析结果再决定是否继续合并。技能标签字段是用竖线分隔的先把空值补成空字符串再拆分成独立的词df[技能标签] df[技能标签].fillna() df[技能列表] df[技能标签].str.split(|)3.3 数据质量检查与缺失处理策略清洗不是一股脑 dropna而是按字段语义决定处理方式。我习惯先跑一个缺失值统计missing pd.DataFrame({ 缺失数量: df.isna().sum(), 缺失比例: df.isna().mean().round(4), }) print(missing)针对不同字段处理策略差异很大。字段缺失原因处理方式岗位解析失败删除整行岗位缺失无法做任何分析薪资面议或非月薪保留到薪资类型不参与月薪均值计算经验不限经验填充为“经验不限”学历页面未展示填充为“学历不限”技能标签卡片未包含保留空列表不影响其他字段分析这种处理方式能最大程度保留样本量。如果直接把所有含空值的行删掉薪资类和学历类样本会大幅度减少最终图表上的结论容易失真。处理完成后我会把结果另存为 processed 文件方便后续分析重复读取df.to_csv(data/processed/jobs_clean.csv, indexFalse, encodingutf-8-sig)4. 分析可视化围绕岗位薪资、经验与技能词频生成图表4.1 先定指标城市、薪资、经验分布和技能词频清洗后的数据已经有城市、月薪上下限、经验、学历和技能标签接下来先构建最核心的三个分析维度。第一薪资分布用平均月薪的直方图观察整体水平第二不同经验和城市组合用分组柱状图比较中位数第三技能标签做词频统计看市场到底在要求什么。城市字段在采集时已经包含但不同页面写法可能不一致比如“北京”“北京·海淀区”“北京-昌平区”需要先统一取城市前缀df[城市] df[城市].str.split(·|-).str[0]这里用正则表达式风格不统一的分隔符把城市和区域拆开取第一个元素。如果城市名是“北京·海淀区”分割后第一个值是“北京”如果是“广州-天河”同样可以取到“广州”。4.2 绘制薪资与经验的分组柱状图使用 matplotlib 绘制柱状图时第一步是配置中文字体否则负号和中文会变成方块import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False然后做透视表以经验为行、城市为列统计平均月薪中位数pivot df.pivot_table( index经验, columns城市, values平均月薪, aggfuncmedian, sortTrue, )pivot_table的values指定聚合目标列aggfuncmedian比均值更能抵抗极端高薪异常值。绘制三类城市city_list [c for c in [北京, 上海, 深圳] if c in pivot.columns] pivot.loc[:, city_list].plot(kindbar, figsize(10, 5)) plt.title(不同经验要求下的岗位薪资中位数) plt.xlabel(经验要求) plt.ylabel(平均月薪中位数) plt.tight_layout() plt.savefig(output/salary_by_exp_city.png, dpi144)dpi144是适合高清显示屏插图的参数如果只是插入网页144 会占用较大体积可以降到 96。这张图能很快看出“3-5年”和“5-10年”在哪个城市溢价最高。再做一个按薪资区间分组的岗位数量条形图先对平均月薪分箱bins [0, 5000, 10000, 15000, 20000, 30000, 50000] labels [5K以下, 5-10K, 10-15K, 15-20K, 20-30K, 30-50K] df[薪资区间] pd.cut(df[平均月薪], binsbins, labelslabels) counts df[薪资区间].value_counts().reindex(labels)pd.cut把连续月薪映射到离散区间value_counts默认按数量降序排列所以要用reindex(labels)恢复原始区间顺序。4.3 技能标签词频统计与可视化表格先把技能列表展开成一维向量再用 collections.Counter 做词频统计from collections import Counter tags df[技能标签].dropna().str.cat(sep|) tag_list [t for t in tags.split(|) if len(t) 2] word_counter Counter(tag_list) top_word word_counter.most_common(15)在真实数据上这个统计结果中会大量出现“Python”“Excel”“SQL”“Tableau”这类词但它们常常是孤立字符串不能像自然语言文本那样直接分词。所以这里我并没有使用 jieba而是对已经结构化的标签直接做 Counter 统计结果更干净。如果想做词云就把 top_word 转换成词频字典交给 wordcloud 库。把这 15 个高频技能输出成表格可以直观判断岗位市场需求排名技能标签出现次数1Python13272SQL11823数据分析9764Excel8155数据可视化603这里的次数只代表在采集到的卡片技能标签中出现过多少次不代表岗位 JD 正文中的真实频次。要更精确地分析需要采集岗位详情页的职位描述代码逻辑类似只是请求路径要从卡片中的链接进入并单独控制采集速率。5. 项目落地源代码、数据与文档的三层组织方式5.1 项目目录结构与分层模块当采集脚本和图表脚本越来越多所有代码堆在一个文件里会让排错非常痛苦。我会把项目按采集、分析、数据、文档四个维度拆开boss_analytics/ ├── config.py # 关键词、城市、请求间隔、Cookie 读取 ├── main.py # 流程入口负责串联采集到可视化 ├── collector/ │ ├── fetcher.py # 请求模块requests 重试 │ ├── parser.py # 解析模块HTML 转字段 │ └── saver.py # 存储模块CSV / SQLite ├── analysis/ │ ├── clean.py # 数据清洗 │ └── charts.py # 图表生成 ├── data/ │ ├── raw/ # 原始采集结果 │ └── processed/ # 清洗后的数据 ├── docs/ │ └── README.md # 环境安装、运行步骤、字段说明 └── output/ # 图表输出目录config.py 里集中维护所有可配置项例如城市代码表、搜索关键词、每页采集最大页数、请求间隔。main.py 只做流程编排不写具体业务逻辑。这样结构调整时修改 parser.py 不影响 fetcher.py替换存储方式时也不影响分析模块。5.2 使用 SQLite 做增量采集与断点续跑CSV 适合单次采集分析但当你每天重复跑同一批关键词时CSV 会出现重复数据。更可靠的方式是使用 SQLite 存原始记录并对每条岗位生成唯一主键。我使用“岗位名公司名薪资”合成主键配合INSERT OR IGNORE实现天然去重import json import sqlite3 def save_to_sqlite(records, db_pathdata/jobs.db): conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS jobs ( job_id TEXT PRIMARY KEY, payload TEXT, collected_at TEXT DEFAULT (datetime(now)) ) ) for item in records: job_id f{item.get(岗位)}|{item.get(公司)}|{item.get(薪资)} conn.execute( INSERT OR IGNORE INTO jobs(job_id, payload) VALUES (?, ?), (job_id, json.dumps(item, ensure_asciiFalse)) ) conn.commit() conn.close()这里把整条岗位记录以 JSON 方式塞进 payload 字段是因为招聘网站的字段会随版本变化新增字段时不用频繁修改表结构。如果你需要按“岗位名称”做条件查询可以在建表时额外加一列或者在应用层用 pandas 读取后再过滤。5.3 偏向工程视角的自检表项目跑不通时按下面的对照表排查通常能在几分钟内定位问题现象可能原因处理方式请求返回 403请求频率过高或 Cookie 失效增大time.sleep间隔手动重新登录并更新 Cookie返回页面无岗位卡片页面结构改版选择器失效打开浏览器开发者工具重新检查li.job-card-wrapper是否存在岗位和薪资正常技能标签为空技能标签在不同页面使用不同 class打印卡片 HTML根据实际 HTML 结构调整 labels 选择器CSV 用 Excel 打开乱码写入编码不是带 BOM 的 UTF-8使用encodingutf-8-sig写入数据库数据反复重复主键字段不唯一或未使用 INSERT OR IGNORE检查主键是否包含足够字段再确认是否执行了去重 SQL排错时不要把代码从头到尾阅读而是从数据流向逐层观察先看原始 CSV 是否抓到了大量记录再看清洗后 DataFrame 的 shape 是否缩水最后看图表的 x 轴顺序是否被 pandas 自动排序打乱。每个环节加一行打印日志10 秒就能确认问题出在哪一层。本文还有配套的精品资源点击获取