简介:基于python的boss直聘数据可视化分析系统是面向数据采集与可视化课程设计的高分项目源码,适合高校学生、爬虫入门者及需要完成期末大作业的开发者参考。资源共26个文件,包括爬虫脚本、2个数据分析笔记本、4个CSV数据文件与12个HTML图表页面,压缩包仅426KB,目录结构清晰。爬虫脚本负责采集Boss直聘招聘信息,清洗与分析过程完成数据预处理和指标统计,各学历、经验、城市工资及大厂招聘占比等图表可直接渲染复用,涵盖招聘大数据分析的完整链路,并包含词云展示等可视化模块。项目调试通过,评审95分以上,可直接运行,已有787人学习浏览;内置前端展示页与工程配置,便于二次开发或课程设计参考,对理解招聘数据分析流程和降重均有实际帮助。
1. 基于Python的Boss直聘数据可视化分析系统到底是什么:从期末作业到可复用的数据链路
大四下学期被期末作业压着,又要准备找实习,很多同学最后都选了这个方向:用Python爬一份招聘数据,再做成可视化看板。这个题目确实讨巧——爬虫是现成的热点,ECharts出图又好看,答辩时PPT一放,老师觉得你有工程能力,自己也觉得没白学。但说句实在话,大部分人交上去的东西只是把几张图表堆在网页里,点开一看,数据是假的、图表是凑的——这种作业拿不了高分,放进简历更是减分项。
真正值钱的,从来不是那几张柱状图和饼图,而是从Boss直聘采集岗位数据、用pandas做清洗归并、再通过Flask接口把数据喂给ECharts这一整条链路。它把一个Web项目拆成了爬虫、数据处理、后端接口、前端可视化四段,每一段都是招聘市场里真会问的技能点。这篇就按这条链路展开,从采集字段怎么定、清洗规则怎么写、图表怎么配置到反爬踩坑,按一个能拿去答辩、也能写进项目的标准来做。
2. 数据采集怎么做:用爬虫拿到Boss直聘岗位数据的最小方案
2.1 为什么选requests + 解析库,而不是Selenium或Scrapy
先说结论:选requests加解析库(BeautifulSoup或json解析)是这类大作业最稳的组合,不是因为它最强,而是因为它刚好够用且好控制。Selenium能模拟浏览器行为,对付复杂渲染很好使,但它启动一个Chromedriver至少几百毫秒,连续爬几十页会非常慢,而且无头浏览器的特征很容易被反爬识别,触发验证码后整个流程会卡死。Scrapy是专业级框架,但它的异步调度、中间件、Item Pipeline对期末作业来说太重,学一遍也要花不少时间,用它写出来的代码反而不好在答辩时讲清楚“每一步在干什么”。
Boss直聘的岗位列表页,核心字段(岗位名称、薪资、城市、经验、学历、公司名)是直接写在页面里的,也就是服务端渲染,不需要等JS异步加载。这类页面用requests拿HTML再用解析库提取就够了。少数字段如果页面里没有,再考虑补一个浏览器渲染的接口,但那是后话。大作业的核心是把链路跑通,而不是把工具用得多高级。
2.2 一个能跑的采集脚本:请求头、分页参数与字段定位
下面这个脚本是采集岗位列表页的通用写法,注释里标了每个参数的位置。分页参数在不同站点叫法不同,Boss直聘用的是page和query,注意看URL变化。
import requests import time import random from bs4 import BeautifulSoup HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", "Referer": "https://www.zhipin.com/", } def fetch_job_page(city_code="101010100", keyword="python", page=1): params = { "query": keyword, "city": city_code, "page": page, } # 这里的URL是列表页入口,实际运行时以你自己抓到的为准 resp = requests.get( "https://www.zhipin.com/web/geek/job", params=params, headers=HEADERS, timeout=10, ) resp.raise_for_status() # 部分页面返回gbk编码,用apparent_encoding做兜底 if resp.encoding and resp.encoding.lower() != "utf-8": resp.encoding = resp.apparent_encoding return resp.text html = fetch_job_page(page=1) print("页面长度:", len(html))这段代码的核心是两件事:构造请求参数和确认编码。query是搜索关键词,city是城市编码,page是页码。城市编码可以在Boss直聘的页面里找到,比如北京是101010100,上海是101020100,它是一个形如九位数字的字符串,别硬编码成城市名,否则请求会出错。
timeout=10是必须加的,如果目标站点响应慢或连接被重置,requests会抛出异常,不至于让脚本卡几分钟。发现页面长度正常后再往下写解析逻辑,这是爬虫的基本习惯——先确认“我能拿到数据”,再确认“我能读懂数据”。
解析部分建议直接定位岗位卡片节点,避免在整页大杂烩里找字段。Boss直聘的列表项是包在一个带class的div里的,每个卡片里的岗位名称、薪资、公司名结构相对固定。用BeautifulSoup的select方法按CSS路径取,翻车概率低:
from bs4 import BeautifulSoup def parse_job_list(html): soup = BeautifulSoup(html, "html.parser") jobs = [] # 实际class名请以抓到的页面为准,不要照抄网上的旧教程 card_list = soup.select("div.job-card-wrapper, ul.job-list-box > li") for card in card_list: title_tag = card.select_one("span.job-name, a.job-name") salary_tag = card.select_one("span.salary") company_tag = card.select_one("span.company-name, h3.company-name") if not title_tag or not salary_tag: continue jobs.append({ "岗位名称": title_tag.text.strip(), "薪资": salary_tag.text.strip(), "公司": company_tag.text.strip() if company_tag else "", }) return jobs这里故意保留了两种选择器写法,原因是不同时期页面的class名会变。如果你在本地跑出来是空的,不要在解析逻辑里死磕,先打开网页按F12确认当前页面的DOM结构,把选择器换成实际值。选择器定位时优先用“父级容器 + 关键词字段”,比精确到单个span更抗页面小改动。
2.3 数据落盘:CSV还是MySQL,期末作业怎么选
数据存哪里是个被低估的问题。MySQL听起来正式,很多教程也默认爬虫连数据库,但期末作业的场景里,我建议用CSV加pandas的to_csv就够了。原因有三点:第一,答辩时老师可能直接问“数据哪来的”,CSV就在你项目目录下,打开就能看,说服力比“我在本地建了个库”强;第二,清洗和可视化阶段用pandas读CSV,代码量比连数据库少一个量级;第三,CSV不会出现编码、权限、服务没启动这类环境问题,少一个翻车点。
存CSV时有一个容易被坑的地方——中文列名和内容写进文件后,Excel打开是乱码,但直接用pandas读又是好的。这是因为CSV默认用utf-8存储,而Excel按GBK读取。解决方法是存储时加encoding参数:df.to_csv("jobs.csv", index=False, encoding="utf-8-sig"),utf-8-sig会在文件头写入BOM标记,Excel就能正确识别。这个细节在答辩演示时特别加分,因为很多同学的CSV打开全是乱码,你的能直接看,这就是差别。
字段设计按“够用”原则来,不要一上来想抓十几个字段。大作业的成熟度不取决于字段数量,而取决于每个字段是否真的用上了。推荐最少收集这6个:岗位名称、薪资、城市、经验要求、学历要求、公司名称。薪资后面要拆区间,经验和学历要做归一化映射,这些在下一步清洗时处理。
3. 数据清洗与预处理:把抓回来的脏数据变成可分析的结构
3.1 清洗什么:薪资区间、经验学历与空值的归并规则
爬下来的数据第一眼是能看的,细看全是问题。薪资字段是“15-20K·14薪”“面议”“25-35K·13薪”这种混合格式;经验要求有“1-3年”“3-5年”“经验不限”“在校/应届”等多种写法;学历要求看起来规整一些,但也有“本科及以上”“大专”和“学历不限”的差异。如果直接把这些字符串丢给可视化,图表轴标签会排出一串乱码一样的文本,所以必须先做归并。
清洗规则要提前定清楚,不要边写边改。我的做法是:薪资拆出最低值、最高值两个数值列,取中间值算平均薪资;面议置为空值,不参与统计。经验按“不限/应届/1-3年/3-5年/5-10年/10年以上”六档归并;学历按“不限/大专/本科/硕士/博士”五档归并,带“及以上”的取基准档。岗位名称也值得处理一下,比如“python开发工程师”“Python后端开发”和“python工程师”其实是同一类岗位,可以用 keyword提取主语言做粗分类,这在后面做岗位分布统计时能省很多事。
3.2 pandas清洗脚本:字段拆分与空值处理的一次跑通
清洗脚本建议单独建一个clean.py,和爬虫分开,这样每跑一次爬虫不用重复清洗逻辑,答辩时也可以说“采集和清洗是解耦的两个模块”。
import pandas as pd import re df = pd.read_csv("jobs.csv", encoding="utf-8-sig") # 1) 薪资字段拆分成最低/最高两列,面议置为NaN def split_salary(s): if not isinstance(s, str): return pd.Series([None, None]) # 匹配 15-20K 或部分地区 15-20千 等格式 m = re.search(r"(\d+(?:\.\d+)?)\s*[-—~到]\s*(\d+(?:\.\d+)?)\s*[Kk千]", s) if m: return pd.Series([float(m.group(1)), float(m.group(2))]) return pd.Series([None, None]) df[["最低薪资", "最高薪资"]] = df["薪资"].apply(split_salary) df["平均薪资"] = (df["最低薪资"] + df["最高薪资"]) / 2 # 2) 经验要求归并为固定六档 def map_experience(v): if not isinstance(v, str): return "不限" if "应届" in v or "在校" in v: return "应届/在校" if "10年以上" in v: return "10年以上" m = re.search(r"(\d+)-(\d+)年", v) if m: low = int(m.group(1)) return f"{low}年以上" # 简单归并,答辩演示够用 if "不限" in v: return "不限" return "其他" df["经验归并"] = df["经验要求"].apply(map_experience) # 3) 学历归一 df["学历归一"] = df["学历要求"].replace({ "学历不限": "不限", "中专/中技": "大专", "本科及以上": "本科", "硕士及以上": "硕士", "博士及以上": "博士", }).fillna("不限") # 4) 空值处理:没有拆出薪资区间的记录,不参与平均薪资统计 df_valid = df.dropna(subset=["平均薪资"]) df.to_csv("jobs_clean.csv", index=False, encoding="utf-8-sig") print("清洗完成,有效薪资记录数:", len(df_valid)) print("各学历区间占比:\n", df_valid["学历归一"].value_counts(normalize=True))代码里的关键点在薪资拆分。regex里的[-—~到]是一个字符组,覆盖了常见的短横线、中文破折号、波浪线和中文字“到”,这是从实际数据里总结出来的坑——招聘方写工资的间隔符五花八门,只匹配连字符会漏掉一批数据。匹配后乘以K的单位换算成以千为单位即可。
经验归并这里用了简单规则,不是最严谨的,但目的是给图表提供有序的x轴标签。如果你希望图表x轴按时间顺序排(1-3年在3-5年前),就需要在归并后把它转成Categorical类型并指定顺序,否则pandas按字符串排序,“本科”会排在“大专”前面,图表显示就不符合直觉。清洗完之后,建议把value_counts的结果打出来看一眼,确认每个字段的分布都合理再进入可视化。
4. 可视化与分析:用Flask+ECharts搭出能答辩的看板
4.1 技术选型:为什么是Flask+ECharts而不是pyecharts
看到“数据可视化”,很多同学第一反应是pyecharts,因为它几行代码就能生成HTML文件,用浏览器打开就是一个像模像样的图表。但放在这个项目里,我明确推荐Flask+ECharts的纯前端方案,理由有三个。第一,pyecharts生成的HTML是静态独立文件,图表之间没法做联动交互,答辩时演示不流畅,遇到“你点这个柱子看看”这种问题时只能干站着;第二,Flask接口返回JSON、前端用Ajax拉数据再渲染,这就让项目有了前后端分离的痕迹,简历里可以写“开发了基于Flask的可视化分析服务”;第三,ECharts的配置项是无数企业在用的实际技能,学一遍以后做别的项目直接复用。
这个选型同时也是对“企业级数据可视化”方向的一个靠拢。不是说大作业要做到企业级,而是技术栈要往主流方向靠,Flask做数据服务、ECharts做渲染,是当前比较常见的组合之一。
4.2 系统目录结构与数据接口设计
项目的目录结构不需要复杂,但要有清晰边界。我一般这样组织:
boss_analysis/ ├── app.py # Flask入口,所有接口都在这里 ├── spider/ │ ├── crawler.py # 采集脚本 │ └── clean.py # 清洗脚本 ├── data/ │ ├── jobs.csv # 原始数据 │ └── jobs_clean.csv # 清洗后数据 ├── templates/ │ └── index.html # 看板页面 └── static/ ├── echarts.min.js # ECharts本地文件 ├── jquery.min.js # 用于Ajax请求,可选 └── style.csstemplates和static是Flask的默认约定,放对位置后render_template才能找到页面。ECharts不要用CDN链接,答辩现场往往没网或者网络慢,本地放一个echarts.min.js是最稳的。用什么版本的ECharts文件不重要,能在本地加载就行。
数据接口建议按图表需求来设计,不要让前端直接读CSV。一个接口对应一个图表,前端代码好写,后端逻辑也清晰。比如“按学历分布统计”就返回[{name: "本科", value: 42}]这种结构。
from flask import Flask, render_template, jsonify import pandas as pd app = Flask(__name__) df = pd.read_csv("jobs_clean.csv", encoding="utf-8-sig") @app.route("/") def index(): return render_template("index.html") @app.route("/api/salary_by_degree") def salary_by_degree(): # 按学历分组,算平均薪资 result = ( df.groupby("学历归一")["平均薪资"] .mean() .round(1) .reset_index() ) data = [ {"name": row["学历归一"], "value": row["平均薪资"]} for _, row in result.iterrows() ] return jsonify(data) if __name__ == "__main__": app.run(debug=True, port=5000)接口逻辑很短,但很多同学写到这里就慌了,因为groupby的结果不知道用什么结构返回。用reset_index()把索引变成列,再遍历转成字典列表,是最直观的做法。round(1)是让薪资保留一位小数,避免图表tooltip里出现一堆小数位。debug=True方便本地调试,但演示时建议关掉。
4.3 核心图表:岗位分布、薪资区间、经验要求的三张图
看板不需要七八个图表,三到四张核心图就够了。我通常会做这几张:按岗位关键词统计的岗位数量TOP10柱状图、按学历分组的平均薪资条形图、经验要求的饼图,再加一张薪资区间分布散点或箱线图。四张图正好是一屏的布局,填得满又不拥挤。
ECharts的配置是最容易出视觉效果也最容易出bug的地方。下面是一个柱状图的完整示例,option写在script标签里,数据通过Ajax从Flask接口读:
<div id="salaryChart" style="height: 400px;"></div> <script src="/static/echarts.min.js"></script> <script src="/static/jquery.min.js"></script> <script> var chart = echarts.init(document.getElementById("salaryChart")); $.get("/api/salary_by_degree", function(data) { chart.setOption({ title: { text: "不同学历的平均薪资", left: "center" }, tooltip: { trigger: "axis" }, grid: { left: 80, bottom: 30 }, xAxis: { type: "category", data: data.map(function(item) { return item.name; }) }, yAxis: { type: "value", name: "平均薪资(K)" }, series: [{ type: "bar", data: data.map(function(item) { return item.value; }), itemStyle: { color: "#3b82f6" } }] }); }); </script>这里几个配置项是答辩时会被问到的,提前记住:tooltip的trigger设为axis,鼠标滑过柱子时会显示整根柱子的数据(也就是同一学历下所有岗位的平均);grid的left和bottom是给y轴的名称和x轴的标签留空间,不设的话文字会被截断;itemStyle的color是柱子颜色,统一用企业蓝,比默认的随机色看着专业很多。如果想要柱子带圆角,加itemStyle.borderRadius: [4, 4, 0, 0]即可。
需要注意的是$.get的回调里,Flask返回的JSON对象已经是被解析过的,data直接就是一个数组。map函数在回调里遍历出name数组和value数组,正好对应ECharts的xAxis.data和series.data。如果接口返回结构不对,最常见的错误就是忘了写map,直接把整个数组塞给data,图表会一片空白,打开浏览器开发者工具能看到数据格式错误。
饼图的配置思路类似,只是把series.type改成"pie"并加上radius。饼图有一个特殊配置label.formatter,可以控制标签显示为“本科 35%”而不是只显示“本科”,在小屏幕上不占地方,也更直观。formatter写成function(params) { return params.name + " " + params.percent.toFixed(0) + "%"; },对前端来说不算复杂,但视觉效果提升很大。
5. Boss直聘数据采集的避坑指南:反爬、编码与字段缺失的血泪经验
5.1 请求太频繁被限制
现象:爬了十几页之后,页面返回的不是岗位列表,而是一个滑块验证页面或“访问异常”提示,脚本解析不到任何数据。
原因:采集频率太高,同一IP的请求特征太明显。requests请求带上浏览器UA还不够,如果每秒发十几个请求,任何一个站点都会警觉。
解决:每页请求之间加随机延时,2到4秒的区间比较稳妥。代码里用time.sleep(random.uniform(2, 4)),这个random不是随便写的,固定延时3秒反而会被识别为机器行为。还有一点,用requests.Session()保持会话,让服务器看到的是同一个会话的连续请求,比每次新建连接更像正常用户。
提示:任何爬虫都要控制频率,大作业只需要演示环境跑通,不要为了刷数据量去挑战反爬。
5.2 薪资字段解析错位
现象:有的岗位薪资是“15-20K·14薪”,拆出来最高、最低没问题;有的写“面议”,直接变成空值丢弃了;还有“20-30K·16薪”这种带固定薪的,正则匹配到30K后面还有字符。
原因:薪资字段不是严格统一的格式,用单一正则去匹配一定会有漏网之鱼。
解决:正则要从宽不从紧,先匹配数字区间,再忽略后面的“·14薪”后缀。r"(\d+(?:\.\d+)?)\s*[-—~到]\s*(\d+(?:\.\d+)?)\s*[Kk千]"就能忽略后缀,因为只匹配到K为止。面议数据不要硬算,记为空值,在清洗后统计时注意不能把它当0处理,否则会把平均薪资拉到很低。dropna(subset=["平均薪资"])就是在做这个过滤。
5.3 城市数据大面积丢失
现象:采集了多个城市的数据,清洗后发现城市字段大部分为空,图表没法按城市统计。
原因:列表页的城市信息可能不在岗位卡片里,或者页面需要登录才展示。这是一个常见的字段缺失场景,不是你的选择器写错了。
解决:在城市选择时就把城市写进采集结果里,而不是等爬完再补。采集时city_code是你自己传的参数,爬虫脚本里可以直接把这个参数值转成城市名写入每条记录。还有一种方案是用日志记录每次请求的城市参数,后续再回填。无论如何,不要在清洗阶段才想起来城市字段没采集,那就只能重爬了。
5.4 页面编码判定错误导致乱码
现象:parse出来的岗位名称是“pythonå¼åå·ç¨‹å¸ˆ”这种乱码。
原因:Boss直聘的页面编码不是稳定的utf-8,有时候是gbk或gb2312,直接用requests的text属性会按默认编码解码。
解决:在拿到响应后,先检查resp.encoding,如果是gbk相关就强制转换,更稳的是用resp.apparent_encoding。这个属性是根据页面meta信息推测的编码,准确率较高。这个方法对任何中文站点都适用,不只是Boss直聘。爬虫写多了你会发现,编码问题占踩坑量的三成。
5.5 翻页截断导致只爬到前五页
现象:总共应该能爬到50页数据,实际只爬到第5页就停止了,后面几页的内容跟前面重复。
原因:站点对未登录用户的分页深度有限制,翻到一定页码后返回的是同一页内容或者重定向到登录页。
解决:采集时记录每一页返回的记录条数,如果连续两页条数相同且和上一页完全相同,就停止翻页。另外,爬到最后一页时通常会有页码边界,超过真实页码后URL会重定向,所以用resp.history判断是否有重定向也能提前结束。这个判断逻辑放在爬虫里比事后看数据更省事。
6. 把期末作业做出加分效果:数据真实性验证与看板交互的两个技巧
第一个加分点是数据真实性验证。答辩时老师最常问的问题是“这数据是真的吗”,很多同学支支吾吾,因为自己也没验证过。做法很简单:爬虫跑完后,把jobs_clean.csv里某个关键词、某个城市的平均薪资算出来,跟招聘市场的公开信息对比,能对上说明数据可信,对不上就分析原因。这个步骤在答辩时直接说“我对比了拉勾/猎聘同类岗位平均薪资,误差在10%以内”,比任何美化图表都有说服力。记得把对比数据截图存到答辩PPT里。
第二个加分点是图表联动。上面四张图如果各自独立,演示时会显得干巴,但加一个点击联动很容易——在柱状图的点击事件里获取当前岗位名称,再触发饼图的setOption更新数据,就实现了“点某个学历,看它对应的经验分布”。代码量不大:
salaryChart.on("click", function(params) { var degree = params.name; $.get("/api/experience_by_degree?degree=" + degree, function(data) { pieChart.setOption({ series: [{ data: data }] }); }); });对应的Flask接口需要接request.args.get("degree")来接收参数,这个交互做了,项目就从“能看”变成了“能分析”。面试官基本不会追问交互怎么实现,因为一看就是自己写的。
第三个加分点是答辩演示的动线设计。演示时不要从爬虫开始讲,那是最枯燥的一段。先打开看板页面,让老师看整体视觉效果,然后点几个图表做联动,最后再展示清洗前后数据对比。讲爬虫时重点说反爬策略和频率控制,讲可视化时说技术选型理由,这两段是老师最可能追问的部分。
做这类大作业,我自己的习惯是先把CSV打开看一遍再决定做什么图表——数据长什么样,就做什么图,不要为了图好看而做图。数据清晰,图表自然清楚,答辩也就顺利。这个道理同样适用于以后做数据分析项目的选图判断。希望帮到你。
本文还有配套的精品资源,点击获取