十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

BOSS直聘岗位数据爬虫分析可视化:从requests到pyecharts的完整实战

BOSS直聘岗位数据爬虫分析可视化:从requests到pyecharts的完整实战 简介这是一套面向计算机专业期末大作业和课程设计的招聘岗位数据爬虫分析可视化项目适合需要完整可运行样例作参考或实战练习的学生。项目经导师指导并获九十八分评价核心覆盖岗位数据抓取、清洗、可视化展示涉及爬虫框架调度、管道、中间件等模块同时附有全国热门城市岗位数据文件。压缩包共三十九个文件大小仅三百三十九KB包含十三份Python脚本、十二份JavaScript交互脚本、网页样式、效果图、配置文件与说明文档另含一个可参考的新项目压缩包目录清晰便于对照和二次开发。目前已有二百九十九人学习浏览代码均通过本地调试难度适中拿到后可直接运行观察效果也可结合说明文档梳理爬虫逻辑与可视化流程对完成课程设计或项目实战很有帮助。1. 为什么期末大作业都爱选题 BOSS 直聘岗位数据爬取与分析BOSS 直聘这类招聘平台的岗位数据结构规整、字段丰富天然适合拿来练爬虫、清洗和分析是 Python 数据分析方向期末大作业里出现频率最高的题目之一。但把这道题做好难点从来不在“爬下来”而在后续环节字段怎么映射、薪资怎么归一化、城市和学历怎么对齐以及最终的可视化能不能讲出一个能让老师点头的故事。这篇博文就用“BOSS 直聘岗位数据爬虫分析可视化”这个课题把一套完整可复现的实现路径讲清楚覆盖 requests 数据采集、Selenium 兜底、Pandas 清洗、pyecharts 可视化和工程打包这几段正好对应你说的“含全部资料”里的整套交付物。新手能按步骤跑通老手可以重点关注第 2 章的并发设计取舍和第 5 章的交付物组织方式。2. 先搞定数据采集requests 主力 Selenium 兜底的反爬方案写爬虫的第一件事不是写代码是把目标站点的接口和数据流梳理清楚。BOSS 直聘的岗位列表走的是 JSON 接口返回结构里含 jobId、岗位名称、薪资区间、经验学历要求、城市商圈等关键字段比从 HTML 里用 XPath 提取要稳得多。我一般先用浏览器开发者工具切到 Network 面板筛选 XHR 请求找到返回岗位列表的那个接口观察它的 query 参数和响应结构再决定用 requests 直接模拟请求还是上 Selenium 兜底。2.1 用 requests 打列表接口先写最小请求验证连通性这里先给一个最小可用的 requests 实现目标是把指定城市的 Python 岗位列表抓回来并落成 JSON跑通这一步再谈并发。import requests import json import time session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: application/json, text/plain, */*, Referer: https://www.zhipin.com/web/geek/job, }) def fetch_job_list(city_code, queryPython, page1): url https://www.zhipin.com/wapi/zpgeek/search/joblist.json params { scene: 1, query: query, city: city_code, page: page, pageSize: 30, } resp session.get(url, paramsparams, timeout10) if resp.status_code ! 200: print(fHTTP {resp.status_code}, 可能被风控) return None data resp.json() if data.get(code) ! 0: print(f业务码异常: {data.get(code)}, 消息: {data.get(message)}) return None return data[zpData][jobList] if __name__ __main__: jobs fetch_job_list(city_code101010100, queryPython, page1) if jobs: with open(raw_jobs_page1.json, w, encodingutf-8) as f: json.dump(jobs, f, ensure_asciiFalse, indent2) print(f抓取 {len(jobs)} 条岗位)这段代码有几个关键点要说明。请求头里的Referer在 BOSS 直聘这种站点上通常比 UA 更敏感不带 Referer 直接请求接口很容易被拦截这是我踩过几次坑之后固定下来的写法。返回结构里code为 0 表示业务成功非 0 一般是风控或参数错误要单独打日志而不是直接报异常。city参数用的是城市编码101010100 对应北京这个编码可以从 BOSS 直聘的前端静态资源里扒出来也可以自己维护一份映射表。pageSize一般建议保持默认值改大容易触发异常。2.2 列表接口不稳定时的兜底Selenium 渲染抓包接口有风控时的兜底思路是切换请求源。BOSS 直聘的岗位列表页是服务端渲染加部分异步加载直接用 Selenium 打开列表页滚动几次后从driver.page_source里用正则或 XPath 抽取岗位卡片信息绕开 JSON 接口。这个方案慢但连通性最稳。from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By import time def fetch_jobs_by_selenium(city_code101010100, queryPython, pages3): options Options() options.add_argument(--headless) options.add_argument(--no-sandbox) options.add_argument(--disable-gpu) options.add_experimental_option(excludeSwitches, [enable-automation]) driver webdriver.Chrome(optionsoptions) jobs [] try: for page in range(1, pages 1): url fhttps://www.zhipin.com/web/geek/job?query{query}city{city_code}page{page} driver.get(url) time.sleep(3) for _ in range(3): driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) time.sleep(2) cards driver.find_elements(By.CSS_SELECTOR, .job-card-wrapper) for card in cards: item { title: card.find_element(By.CSS_SELECTOR, .job-name).text.strip(), salary: card.find_element(By.CSS_SELECTOR, .salary).text.strip(), company: card.find_element(By.CSS_SELECTOR, .company-name).text.strip(), } jobs.append(item) finally: driver.quit() return jobsSelenium 方案的要点在等待策略。XPATH 定位卡片在 BOSS 列表页改版后不够稳定CSS 选择器更可靠。excludeSwitches去掉自动化标记能降低被识别概率但不能完全避免headless 模式本身也会增加特征暴露所以这版兜底脚本只用于接口被封时的补充采集不作为主力。数据量不大时单线程逐个页面跑配合随机 sleep 2 到 4 秒比并发更安全。2.3 并发与去重的工程取舍动辄并发几十上百的爬虫项目在 Boss 直聘这种强风控站点上效果并不好。这里的瓶颈不是带宽是账号和 IP 的信用分。常见做法是控制并发在 4 到 8 个线程以内配合一个简单的请求信号量同时把抓到的 jobId 去重后落库避免重复采集。pip install tenacityfrom concurrent.futures import ThreadPoolExecutor, as_completed from threading import BoundedSemaphore import requests, json, time SEM BoundedSemaphore(4) def safe_fetch(city_code, query, page): with SEM: # 复用 2.1 里定义的 fetch_job_list加一个指数退避重试 for attempt in range(3): try: return fetch_job_list(city_code, query, page) except requests.RequestException as e: time.sleep(2 ** attempt) return None with ThreadPoolExecutor(max_workers8) as executor: futures { executor.submit(safe_fetch, 101010100, Python, page): page for page in range(1, 10) } for future in as_completed(futures): jobs future.result() if jobs: for job in jobs: print(job.get(jobId), job.get(jobName))BoundedSemaphore(4)限制的是同一时刻在途请求数ThreadPoolExecutor(max_workers8)限制的是线程池大小两者配合能保证请求频率不失控。重试用指数退避而非固定间隔是避免风控升级的关键策略。BOSS 直聘单页 30 条10 页 300 条已经足够一个期末大作业做分析用了不需要贪多。合规边界必须划清只请求公开页面、控制频率、不碰登录后的私有数据、不用于商业目的这是所有反爬方案的前提。注意跑爬虫前先看一眼 robots.txt 和网站的服务条款请求频率控制在 1 QPS 以内数据量级在千条上下用于教学演示是安全的。一旦触发验证码立即停止当前 IP 的请求不要尝试绕过验证码机制。3. 数据清洗与存储薪资归一化和字段对齐是分析质量的分水岭爬下来的 JSON 不能直接做分析。BOSS 直聘的原始字段里薪资是“15-25K·14薪”或“200-300元/天”这样的字符串经验是“3-5年”学历是“本科”城市是“北京·海淀区”带商圈后缀。这些字段全部要拆、要映射、要归一化否则后面任何一张图都是错的。我一般会用 pandas 做清洗输出一个干净的 CSV再决定要不要导入 MySQL。3.1 用 pandas 做薪资区间拆解与中位数映射import pandas as pd import re df pd.read_json(raw_jobs.json) def parse_salary(s): if pd.isna(s): return None, None, None s str(s).strip() # 处理 15-25K·14薪 这种带薪月数的 match re.search(r(\d{2,3})-(\d{2,3})K, s) if not match: # 处理 200-300元/天 这类日薪 match re.search(r(\d)-(\d)元/天, s) if match: low, high int(match.group(1)), int(match.group(2)) return low, high, 日薪 return None, None, None low, high int(match.group(1)), int(match.group(2)) month re.search(r(\d)薪, s) if month: month_num int(month.group(1)) return low * month_num / 12, high * month_num / 12, 月薪 return low, high, 月薪 df[[salary_low, salary_high, salary_type]] df[salary].apply( lambda x: pd.Series(parse_salary(x)) ) df[salary_mid] (df[salary_low] df[salary_high]) / 2 # 经验字段映射为有序数值 exp_map { 经验不限: 0, 1年以内: 1, 1-3年: 2, 3-5年: 3, 5-10年: 4, 10年以上: 5 } df[exp_level] df[experience].map(exp_map) # 学历字段设成有序类别 degree_order [学历不限, 初中及以下, 中专/中技, 高中, 大专, 本科, 硕士, 博士] df[degree_level] pd.Categorical(df[degree], categoriesdegree_order, orderedTrue) df.to_csv(jobs_clean.csv, indexFalse, encodingutf-8-sig) print(df[[jobName, salary, salary_mid, city, degree_level]].head())salary_mid是后续所有薪资分析的核心指标。把“15-25K·14薪”转成中位数 20K再把 14 薪的 mid 乘 14 再除 12算成“月均等效薪资”这样过年薪或月薪的岗位才能放进同一张图里比较。日薪岗位单独打标不混进月薪分析否则分布图会被拖偏。utf-8-sig编码是为了让 Excel 打开 CSV 不乱码。3.2 城市字段拆分与地点聚合BOSS 直聘的city字段是“北京·海淀区·上地”这种三段式分析“城市分布”时需要取第一段。热门城市可以做成城市编码映射表但更通用的办法是直接按·切分。df[city_clean] df[city].str.split(·).str[0] df[district] df[city].str.split(·).str[1] if · in str(df[city].iloc[0]) else None city_salary df.groupby(city_clean).agg( job_count(jobId, count), salary_median(salary_mid, median), exp_mean(exp_level, mean), ).reset_index().sort_values(job_count, ascendingFalse)job_count反映岗位需求量salary_median反映薪资水平exp_mean反映行业资历门槛。这三个指标放在一张表里后边做可视化的数据底座就打好了。注意exp_mean是有序数值的均值解释时要表述为“平均经验要求等级”不能直接说“平均经验年数”。3.3 SQLite 为主、MySQL 可选的存储选型期末大作业的项目打包和分发场景SQLite 比 MySQL 更合适。SQLite 是单文件数据库不需要安装服务老师拿到源码后直接跑 Python 脚本就能建表查数省去配置数据库的环节。如果题目要求“使用数据库”SQLite 也完全满足。想体现 MySQL 技能的话可以把清洗后的 CSV 用pandas.to_sql导入 MySQL但这样交付时需要附带建表 SQL 文件和连接配置说明。import sqlite3 conn sqlite3.connect(boss_jobs.db) df.to_sql(jobs, conn, if_existsreplace, indexFalse) conn.execute(CREATE INDEX IF NOT EXISTS idx_city ON jobs(city_clean)) conn.execute(CREATE INDEX IF NOT EXISTS idx_salary ON jobs(salary_mid)) conn.commit() # 验证入库行数和字段完整性 cur conn.execute(SELECT COUNT(*), COUNT(DISTINCT jobId) FROM jobs) print(总行数, 去重岗位数:, cur.fetchone())if_existsreplace适合重复跑流程覆盖旧数据CREATE INDEX在数据量只有几千条时不是必须的但写上能体现工程规范。验证行数时同时看总行数和去重岗位数两者不一致说明爬虫阶段去重没做干净。注意爬虫入库前一定要按 jobId 去重否则同一岗位在多个分页出现时会重复计数直接影响“岗位需求量”图表的准确性。4. 分析与可视化用 pyecharts 讲清“岗位在哪儿、薪资差多少”分析环节要把清洗后的数据变成老师能一眼看懂的故事。一个完整的大作业可视化至少应该覆盖三张图城市岗位需求量分布、城市薪资与经验中位数对照、学历与薪资箱线图。用 pyecharts 的好处是输出 HTML 文件双击就能打开不需要额外搭 Web 服务也支持在 Jupyter Notebook 中直接渲染。4.1 城市岗位量与薪资气泡图的参数调整from pyecharts.charts import Bar, Map, Boxplot from pyecharts import options as opts city_count city_salary.head(10) bar ( Bar() .add_xaxis(city_count[city_clean].tolist()) .add_yaxis( 岗位数量, city_count[job_count].tolist(), category_gap30%, ) .set_global_opts( title_optsopts.TitleOpts(titlePython 岗位城市分布 TOP10), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate30)), datazoom_opts[opts.DataZoomOpts()], ) ) bar.render(city_bar.html)category_gap30%控制柱间宽度比例城市名长度超过 4 个字时rotate30能避免标签重叠。datazoom_opts单独对 x 轴加滑块当城市超过 15 个时看图的人可以拖着看这个细节在答辩演示时很加分。如果想要更有“数据分析”观感的效果可以把柱状图换成地图。map_chart ( Map() .add( 岗位数, [list(z) for z in zip(city_salary[city_clean], city_salary[job_count])], maptypechina, ) .set_global_opts( title_optsopts.TitleOpts(title全国 Python 岗位需求分布), visualmap_optsopts.VisualMapOpts(max_int(city_salary[job_count].max())), ) ) map_chart.render(city_map.html)地图的maptypechina依赖 pyecharts 内置地图数据首次使用会自动下载地图 JSON离线环境会失败这是期末演示现场最容易翻车的点。建议在联网环境下提前确认地图渲染成功并把生成的 HTML 一并放进交付物里。visualmap_opts的max_不设置的话颜色映射的基准会乱图例颜色和实际数值对不上。4.2 学历与薪资箱线图看清“读研值不值”盒须图是分析学历与薪资关系的最佳工具它能同时展示中位数、四分位数和离群值比柱状图加误差线更直观。df df[df[degree].isin([大专, 本科, 硕士, 博士])] degree_order [大专, 本科, 硕士, 博士] df[degree] pd.Categorical(df[degree], categoriesdegree_order, orderedTrue) salary_by_degree [ df[df[degree] deg][salary_mid].dropna().tolist() for deg in degree_order ] box ( Boxplot() .add_xaxis(degree_order) .add_yaxis(薪资, box.prepare_data(salary_by_degree)) .set_global_opts( title_optsopts.TitleOpts(title学历与月均薪资分布), yaxis_optsopts.AxisOpts(name月均薪资 (K)), legend_optsopts.LegendOpts(is_showFalse), ) ) box.render(degree_box.html)Boxplot.prepare_data()是 pyecharts 提供的静态方法输入是“每个类别的数值列表组成的列表”输出才是盒须图要求的五元组格式。单独把学历字段转成有序 categorical能保证 x 轴的顺序是“大专、本科、硕士、博士”而不是 Python 默认的字母序。做过这个图的数据后会发现本硕薪资中位数差距通常在 5K 到 8K 之间但离群值差异更大这个结论作为大作业的分析亮点很合适。4.3 词云与技能需求分析的隐藏加分项岗位描述jobDescription字段是很多人忽略的宝藏。把 JD 文本做 jieba 分词过滤停用词后生成词云能直观回答“市场最看重什么技能”。这里给一个极简实现import jieba from collections import Counter import re all_text .join(df[jobDetail].dropna().tolist()) all_text re.sub(r[^\u4e00-\u9fa5a-zA-Z#], , all_text) words [w for w in jieba.cut(all_text) if len(w.strip()) 1] stopwords {岗位, 职责, 任职, 要求, 负责, 工作, 相关, 经验, 能力} words [w for w in words if w not in stopwords] counter Counter(words).most_common(30) print(counter)jieba 分词对“Python”“C”“数据分析”这类词默认切分不一定准建议往jieba.suggest_freq里手动加词。词云图建议用stylecloud或wordcloud出图配色选和主图表一致的蓝绿系提交进报告里整体感会好很多。这一步是纯加分项投入二十分钟值得。5. 期末大作业的交付工程化目录结构、关键代码注释和答辩验证清单“含全部资料”这个词落到实际交付上意味着老师拿到的是一个能直接运行的完整项目而不是散落一地的 py 文件和图片。这个环节的整理规范度往往比技术实现更能影响期末成绩。一个好的交付目录应该让老师 10 分钟内跑通全流程并且不需要读代码就能明白每个文件是干什么的。5.1 一套可复用的项目目录模板boss_job_analysis/ ├── README.md ├── requirements.txt ├── config.py ├── data/ │ ├── raw/ # 爬虫原始 JSON │ ├── clean/ # 清洗后 CSV │ └── db/ # SQLite 数据库文件 ├── src/ │ ├── crawler.py # 页面采集 │ ├── cleaner.py # 数据清洗 │ ├── analysis.py # 统计分析 │ └── visualizer.py # 图形生成 ├── output/ │ ├── city_bar.html │ ├── degree_box.html │ └── wordcloud.png └── 报告/ └── 期末报告.docxrequirements.txt中的版本号建议全部锁定我常用的版本组合是requests2.31.0、pandas2.0.3、pyecharts2.0.3、jieba0.42.1、selenium4.15.0。不锁版本的话半年后 pandas 升级 API 变化源码直接跑不起来这是所有 Python 项目交付后的头号风险。config.py里集中放城市编码、请求头、目标关键词等可调参数避免老师在源码里翻找硬编码。5.2 README 里必须写的三件事README 是大作业的“地图”很多同学只写一句“这是爬虫项目”就交上去了非常浪费。我觉得 README 里必须写三件事环境怎么搭、运行按什么顺序、图往哪看。环境搭建写pip install -r requirements.txt运行顺序写成三步清晰的命令pip install -r requirements.txt python src/crawler.py --city 101010100 --query Python --pages 10 python src/cleaner.py --input data/raw --output data/clean python src/visualizer.py --input data/clean --output output每条命令后面用一行字说明它的作用。crawler 控制采集的城市、关键词和页数cleaner 负责把 raw 下所有 JSON 合并清洗输出一张总表visualizer 读取总表生成全部图表。如果老师机器上没装 ChromeREADME 里还要标注 Selenium 方案需要额外安装chromedriver并配好路径。5.3 答辩演示时的判断指标清单答辩和演示环节最怕的是现场出图失败或数据对不上。我建议准备一个自查清单第一重新冷启动一次全流程确认pip install到生成output/目录不超过 15 分钟第二检查清洗后的总表行数是否与爬取日志中“成功抓取 N 条”的数字一致如果不一致大概率是去重或解析丢数据第三打开所有 HTML 图表确认地图渲染正常pyecharts 地图 JSON 需要联网加载提前把output/整个目录拷贝到演示机上不要现场生成第四准备好“如果反爬突然升级导致抓不到数据”的逃生方案——使用data/raw里已经抓好的离线 JSON直接跳过 crawler 步骤进入分析和可视化。这最后一条极其重要能让演示流程在任何网络环境下都立得住。把离线数据也提交到工程包里是这个项目“含全部资料”的完整含义所在。本文还有配套的精品资源点击获取
返回列表