简介:这是一份基于Python与Scrapy框架的Boss直聘岗位数据采集与分析可视化项目,面向爬虫开发者、数据分析学习者以及求职人群,帮助快速掌握从招聘网页数据抓取、清洗、存储到统计分析与可视化展示的完整流程。项目聚焦全国热门城市中大数据、数据分析、数据挖掘、机器学习、人工智能等岗位的招聘信息,围绕薪资水平、学历要求、区域分布、行业需求及岗位技能多个维度展开对比,能够辅助求职者看清人才市场趋势,也方便招聘方了解岗位薪酬与技能分布。压缩包共三十八个文件,其中十三个Python源码文件承担爬虫主程序、数据管道、中间件、配置等核心模块,另含岗位数据表、可视化图表、爬虫配置及项目说明文档,整体包体仅二百三十七KB,目录结构紧凑清晰。读者可获得可直接运行的完整工程与已采集数据,便于复现分析过程并在此基础上继续扩展;目前已有五百八十二人浏览学习,适合需要完整项目参考、开展招聘数据调研或准备毕业设计的读者下载使用。
1. Boss直聘岗位数据采集及分析可视化:一套能直接跑通的Python实现
2023年之后的就业市场里,岗位数据成了比简历更硬核的分析标的。与其手动翻Boss直聘的搜索页逐条截图,不如让Python把岗位数据采集、清洗和分析可视化串成一条流水线。这份资源正好把这条流水线完整打包了:源代码负责从Boss直聘按关键词、城市抓取岗位信息,数据文件是落库后的样例,项目文档写清楚了环境配置和运行顺序。适合两类人:想拿真实数据做薪资与岗位分布分析的从业者;刚学完爬虫、需要一个像样实战项目的开发新手。它解决的是采集链路里最磨人的几个问题——反爬校验、字段口径不统一、可视化图表反复返工,这些坑源码里基本都趟平了。
2. 拆开资源包先看结构:源码、数据与文档三条线怎么配合
拿到压缩包的第一件事不是运行脚本,而是把目录结构摸清楚。这类采集项目最忌讳上来就python main.py,跑起来看似正常,等你去查数据的时候才发现字段漏了、编码错了、入库逻辑跟文档对不上。我拆过不少同类资源包,处理流程基本都是固定的:先读文档确认设计意图,再检查依赖和数据表结构,最后才碰采集脚本本身。
2.1 压缩包里的文件构成与运行前置条件
这份资源包内部大致分四类内容,第一类是项目说明文档,第二类是采集相关源码,第三类是清洗与可视化相关源码,第四类是数据样例和落库文件。它们之间的依赖关系是单向的:采集把网页数据写成数据库或CSV,清洗与可视化再从库里读数据出图。这样分层的设计好处在于,采集挂了不会影响后续分析,分析改图也不会反过来破坏采集结果。
| 资源内容 | 大概作用 | 运行顺序 |
|---|---|---|
| 项目说明文档 | 环境要求、运行步骤、表结构说明 | 第 1 步读 |
| 采集模块 | 请求参数、Cookie 维护、分页抓取 | 第 2 步跑 |
| 清洗与入库模块 | 字段归一化、薪资换算、去重入库 | 第 3 步跑 |
| 可视化模块 | 读库生成 HTML 图表与报表 | 第 4 步跑 |
| data 目录 | 样例数据、SQLite / CSV 落库文件 | 产出物 |
| docs 目录 | 设计文档与踩坑记录 | 查阅 |
运行前置条件不复杂,Python 3.9 以上就够用。依赖库集中在requirements.txt里,我一般习惯先手动装一遍核心依赖,避免 pip 批量安装时某些包装不上导致后面排查困难:
pip install requests pandas openpyxl pyechartsrequests负责发 HTTP 请求,pandas做数据清洗与聚合,openpyxl是用来导出 Excel 报告的,pyecharts生成交互式图表。这套组合是 Python 数据采集项目的常见标配,源码里如果还引用了fake_useragent之类的辅助库,按文档说明补齐即可。装完之后验证一下版本,pandas 和 pyecharts 这两个库版本差异可能会带来部分 API 不兼容,建议先跑一个导入测试,报错就按文档锁版本。
2.2 表结构:为什么岗位和公司要拆成两张表
看文档时重点看数据库设计。样例数据如果落在 SQLite 里,直接打开库文件看表结构比读文档更直观。合理的做法是把岗位和公司拆成两张表,岗位表记录每次抓到的职位信息,公司表记录公司侧的基础档案,中间用company_id关联。这样做的直接收益是,同一家公司发多个岗位时,公司信息不需要重复存储,统计“哪些行业岗位量最多”时也只需要关联一次。
我按源码里常见的字段设计整理了一份建表参考:
CREATE TABLE IF NOT EXISTS job ( id INTEGER PRIMARY KEY AUTOINCREMENT, job_id TEXT UNIQUE, job_name TEXT, salary_bottom REAL, salary_top REAL, salary_text TEXT, experience TEXT, education TEXT, city TEXT, area TEXT, company_id TEXT, pushed_at TEXT ); CREATE TABLE IF NOT EXISTS company ( company_id TEXT PRIMARY KEY, company_name TEXT, industry TEXT, size TEXT, finance_stage TEXT );salary_bottom和salary_top是最关键的字段。原始文本如“20-40K·14薪”不能直接参与数值计算,源码里通常会在入库前把薪资拆成上下限两个数值列,单位统一成月薪。job_id加唯一约束的目的很明确,增量抓取时用INSERT OR IGNORE幂等写入,同一个岗位被抓两次也不会翻倍。
这里要特别提醒一点:城市字段不要只存中文名,尽量把城市的编码一起存下来。因为重新采集时需要通过城市编码去拼请求参数,如果库里只存了“北京”两个字,下次扩展城市时还得重新做一次中文名到编码的映射,字段设计时就保留编码能省掉这批重复工作。
3. 采集端实现:请求头、Cookie 与分页参数是三个命门
Boss直聘的岗位列表页本身有足够多的反爬校验,采集模块的参数配置基本决定了脚本寿命。我把采集端的核心拆成两部分讲,一部分是“请求前怎么伪装”,另一部分是“响应回来怎么解析”。前者决定能不能拿到数据,后者决定拿到之后是不是干净数据。
3.1 Cookie 管理、User-Agent 轮换与延时控制
源码的采集模块里,配置信息通常集中在文件头部或独立配置文件里。常见的做法是维护一个HEADERS字典和一个CITIES映射表,Cookie 单独存成常量或从文件读取。这里我一般建议把请求头里的User-Agent拆成列表,每次请求随机取一个,避免高频请求里同一个 UA 被服务端记录特征:
# config.py 采集配置集中管理 HEADERS = { "Accept": "application/json", "Referer": "https://www.zhipin.com/web/geek/job", } UA_POOL = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", "Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36", ] COOKIES = "从浏览器复制粘贴的登录Cookie字符串" KEYWORDS = ["python", "数据分析", "爬虫"] CITIES = {"北京": "101010100", "上海": "101020100", "深圳": "101280600"} PAGE_SIZE = 30 DELAY_RANGE = (1.5, 4.0)逻辑说明:COOKIES是访问搜索接口的敲门砖,不带 Cookie 直接请求大概率被重定向到安全验证页。CITIES里的编码是城市 ID,与网页端城市切换按钮点击后 URL 里的city参数一一对应。DELAY_RANGE表示两次请求之间随机休眠 1.5 到 4 秒,这个区间是我常用的起点值,既能压低请求频率,又不会让采集过程慢得难以接受。
参数调整方面,关键词列表决定采集范围,想分析 Python 岗位就把KEYWORDS换成["python"],想分析 Java 就换成["java"]。城市编码可以在浏览器里切换城市后从地址栏直接复制,不需要手动记忆。Cookie 有有效期,一般几天内有效,失效后脚本会连续报错或返回验证页,重新登录浏览器并复制新 Cookie 即可。
3.2 岗位列表解析与断点续抓的代码骨架
Boss直聘搜索页返回的内容里,岗位列表是 JSON 结构,每条岗位记录包含职位名、薪资文案、经验要求、学历、公司信息等字段。采集脚本的内部逻辑通常是三层:组装请求参数、解析 JSON 列表、把结果写入 SQLite。其中最容易出错的是分页参数——搜索接口的页码是从 0 开始计的,第一页对应page=0,第二页才是page=1,源码里写错一位就会导致漏采第一页或重复采最后一页。
我在实际项目里维护这样一个抓取函数,带重试和断点续抓能力:
import requests import random import time def fetch_job_list(query, city_code, page): params = { "query": query, "city": city_code, "page": page, "pageSize": PAGE_SIZE, } seen_ids = load_existing_ids() for attempt in range(3): try: headers = dict(HEADERS) headers["User-Agent"] = random.choice(UA_POOL) resp = requests.get( JOB_URL, headers=headers, cookies={"zp_stoken": COOKIES}, params=params, timeout=10, ) data = resp.json() items = data.get("zpData", {}).get("jobList", []) results = [] for item in items: job_id = item.get("jobId") if job_id in seen_ids: continue results.append(parse_item(item)) return results except Exception: if attempt == 2: return [] time.sleep(2 ** attempt + random.random()) return []逻辑说明:请求先随机换 UA,再带着 Cookie 访问搜索接口。拿到 JSON 后从zpData.jobList里提取岗位数组,逐条判断jobId是否已在历史数据里。load_existing_ids()会把库里已有的任务 ID 加载成集合,这样断点续抓时已经入库的岗位直接跳过。请求失败时按指数退避重试,第一次失败等约 1 秒多,第二次等约 2 秒多,第三次直接放弃当前页。
参数说明:timeout=10是请求超时时间,网络波动时防止脚本无限卡住;pageSize=30是单页拉取条数,不建议调到更大的值,接口对单页数量的限制很敏感。重试机制只对临时网络错误有效,如果是 Cookie 失效或验证码拦截,重试再多也没用,这种情况要靠日志里的响应文本特征去识别。
采集完成后一定要看一眼日志里的条数总和与total字段。接口响应里通常有岗位总数,用总数除以每页条数判断循环边界,比写死页码更安全。如果日志显示“第 3 页开始出现空列表”,大概率是 Cookie 对应的搜索范围与关键词组合结果不足,而不是代码逻辑出了问题。
4. 清洗与可视化:薪资换算、字段聚合与图表输出
采集落库只是前半程,分析可视化才是这份资源的价值所在。岗位数据里最需要清洗的字段就是薪资文本,其次是公司规模、学历要求这类枚举型字段。清洗的思路要围绕“让文本变成可计算的数值”展开,可视化再把清洗后的结果转成几张能直接解读的图。
4.1 薪资文本拆分:K、元/天、14薪如何统一换算成月薪
Boss直聘岗位页的薪资文案格式大致分三种:“20-40K·14薪”是常见社招格式,“150-200元/天”是实习或兼职格式,“面议”是信息缺失格式。如果直接拿这些文本做统计,等于拿着字符串分析数字,什么结论都得不到。源码里的清洗模块通常会用正则表达式把数字部分抽取出来,再按单位折算成统一的月度区间。
我给出一段在实际项目里用过的解析函数,覆盖三种格式:
import re def parse_salary(salary_text): if not salary_text or "面议" in salary_text: return None, None nums = re.findall(r"[\d.]+", salary_text) if len(nums) < 2: return None, None bottom, top = float(nums[0]), float(nums[1]) if "元/天" in salary_text: return bottom * 21.75, top * 21.75 if "K" in salary_text.upper(): months = re.search(r"(\d+)薪", salary_text) if months: months_num = int(months.group(1)) bottom = bottom * months_num / 12 top = top * months_num / 12 return bottom, top return None, None逻辑说明:re.findall先把文本里的数字全部抽出来,前两个数字当作薪资上下限。元/天按每月 21.75 天折算成月薪,这是国内工资核算常用的月平均工作天数。K结尾的文本先按月薪处理,再通过(\d+)薪提取“14薪”“13薪”这类倍薪表述,把年薪折算回月均。
边界情况也要考虑清楚:文本里如果只有“20K”没有区间,nums长度为 1,函数直接返回空值。有些岗位写的是“25-45K·15薪”,这里的正则能正确提出 25 和 45,然后乘以 15 再除以 12。清洗完字段后,salary_bottom和salary_top才是真正能进聚合计算的列。
4.2 城市岗位量、薪资中位数与技能词云:pyecharts 出图套路
可视化模块的常规流程是:从库里读取 DataFrame,按维度聚合,再把聚合结果喂给 pyecharts。分析视角集中在三张图上:各城市岗位数量柱状图、各经验要求下的薪资中位数箱线图、岗位描述文本里的技能词云图。柱状图负责回答“岗位集中在哪”,薪资图回答“哪类人值多少钱”,词云回答“市场需要什么技能”。
以城市岗位量为例,代码骨架如下:
from pyecharts.charts import Bar, WordCloud from pyecharts import options as opts city_counts = df.groupby("city").size().reset_index(name="cnt") city_counts = city_counts.sort_values("cnt", ascending=False) bar = Bar() bar.add_xaxis(city_counts["city"].tolist()) bar.add_yaxis("岗位数", city_counts["cnt"].tolist()) bar.set_global_opts( title_opts=opts.TitleOpts(title="各城市岗位数量分布"), xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=45)), ) bar.render("city_jobs.html") text_blob = " ".join(df["job_name"].tolist()) wordcloud = WordCloud() wordcloud.add("", [(w, 1) for w in text_blob.split()]) wordcloud.render("skill_wordcloud.html")逻辑说明:groupby("city").size()是 pandas 里最快得到分组数量的方式,排序后再传给柱状图。axislabel_opts里的rotate=45是防城市名过长时标签重叠,中文标签尤其需要这一项。词云部分把岗位名称合并成一个长文本再分词,英文岗位名如Python、Java会直接成为词项,中文技能词需要配合jieba分词处理。
图表生成后是独立 HTML 文件,浏览器直接打开就能交互。如果源码里用的是 Matplotlib,核心思路也一样,只是把render("xx.html")换成savefig("xx.png"),适合做静态报告插入 Word 文档。pyecharts 的优势在交互性,鼠标悬停能看到每个城市的具体数值,做分析演示时比静态图更有说服力。
5. 避坑记录:反爬校验、漏采、乱码与增量重叠的五类现场
采集项目从来不缺坑,我整理五类实际场景里高概率遇到的问题,每条按现象、原因、解决三个层次记录。这些坑在源码和文档里可能已经隐含处理过一部分,但自己动手改参数时大概率会重新踩一遍。
5.1 请求返回验证页面,JSON 解析直接报错
现象:脚本跑了几百条后突然抛异常,打印响应文本发现返回的不是岗位列表,而是一段只有几百字节的 HTML 验证页。原因:请求频率太高,同一 IP 的访问特征被服务端记录,触发了访问校验。解决:把请求间的延时区间从 1 到 2 秒调整成 3 到 5 秒,并检查日志里是否出现连续失败。验证页出现后立刻停止爬取,静置 5 到 10 分钟再继续,本质上是让频率特征降下来。还有个细节要注意,日志里发现wappass或verify关键字时要主动休眠,这不是靠重试能解决的问题。
5.2 分页漏采:接口页面显示 30 条,抓回来只有 20 条
现象:页面里能看到的岗位数明显多于脚本入库记录数,而且漏采的范围集中在末尾几页。原因:搜索接口的岗位总数计算与页面渲染使用了不同的数据源,或者岗位列表里有部分推荐位字段不在统一列表接口里。解决:用接口返回的total字段除以每页条数计算循环次数,而不是在代码里写死页码;同时把推荐位单独解析,与普通列表分开入库。这样虽然会多几个字段判断,但总数据量能对齐页面显示数。
5.3 薪资统计出现异常值:日薪被当成月薪算了
现象:统计结果里出现一批月薪不到 2000 的极端值,或者出现月薪 50 万以上的离谱值。原因:清洗时只判断了K单位,没判断元/天和元/小时,导致 “200-300元/天” 这类文本的数字直接被当作月薪入库。解决:先判断单位再计算,日薪按月平均工作天数折算,时薪再乘以 8 小时折算。清洗后加一道异常值检测,把salary_top < 3000或salary_top > 200000的数据单独筛出来复查。
5.4 SQLite 中文乱码:连接参数少了一个
现象:数据成功写入数据库,但用工具打开后中文全部变成乱码,英文和数字正常。原因:Python 连接到 SQLite 时没有指定编码,写入的字符串本身已经以 UTF-8 编码,但读取端以其他编码解析。解决:连接后立刻执行PRAGMA encoding = "UTF-8";,并统一脚本文件的头注释为# -*- coding: utf-8 -*-。写入前对文本字段做一次strip(),避免字段首尾混入不可见字符。
5.5 增量抓取第二天跑完,数据直接翻倍
现象:第一天入库 500 条,第二天增量采集后总数据量变成 1000 条,页面里很多是重复岗位。原因:job_id字段没有建立唯一索引,入库时使用普通的INSERT,重复任务 ID 被当新数据写入。解决:建表时给job_id加UNIQUE约束,写入语句改成INSERT OR IGNORE。同时给每次采集增加一个批次时间戳,统计时按批次过滤,这样增量部分和全量部分随时可以分开计算。
6. 把采集脚本跑成习惯:每日定时任务与完整性巡检
当脚本本地跑通后,下一步有价值的事情是把采集变成定时任务。这个场景在 Windows 上可以直接用任务计划程序调用 Python,Linux 或 macOS 上用 crontab,核心命令差别不大。常见做法是写一个入口脚本,把采集、清洗、巡检串成一条调用链,每次运行后生成一份简短的巡检报告。
巡检报告的任务很简单,检查三件事:昨日新增条数是否大于 0、去重率是否低于 90%、失败请求次数是否超标。这三个数字能快速判断采集状态是否健康。推送渠道用企业微信机器人或钉钉机器人都行,本质就是往 Webhook 发一段 JSON:
def push_report(added, dedup_rate, failed): text = f"新增 {added} 条,去重率 {dedup_rate}%,失败 {failed} 次" data = {"msgtype": "text", "text": {"content": text}} requests.post(WEBHOOK_URL, json=data)逻辑说明:added来自本次采集批次写入的新记录数,dedup_rate等于 1 减去重复写入失败的比例,failed是请求重试仍失败的次数。这三个值汇总后推给机器人,每天早晨打开手机就能看到前一天的采集状态。如果某天通知显示新增 0 条,说明 Cookie 可能过期或关键词结果为空,及时处理远好过攒了一周才发现数据断了。从那以后,我每次跑采集脚本都强制走一遍巡检,先看总条数和去重率这两个数字,确认没涨歪才继续用数据。这个习惯帮我避开过好几次静默失效的尴尬,希望你用这份资源跑通自己的岗位数据采集时,也能少走这些弯路,希望帮到你。
本文还有配套的精品资源,点击获取