ARTICLE DETAIL

资讯详情

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

Python招聘数据可视化:Boss直聘爬虫清洗与Flask+ECharts

Python招聘数据可视化:Boss直聘爬虫清洗与Flask+ECharts 简介基于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/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, Referer: https://www.zhipin.com/, } def fetch_job_page(city_code101010100, keywordpython, page1): params { query: keyword, city: city_code, page: page, } # 这里的URL是列表页入口实际运行时以你自己抓到的为准 resp requests.get( https://www.zhipin.com/web/geek/job, paramsparams, headersHEADERS, timeout10, ) 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(page1) print(页面长度:, len(html))这段代码的核心是两件事构造请求参数和确认编码。query是搜索关键词city是城市编码page是页码。城市编码可以在Boss直聘的页面里找到比如北京是101010100上海是101020100它是一个形如九位数字的字符串别硬编码成城市名否则请求会出错。timeout10是必须加的如果目标站点响应慢或连接被重置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, indexFalse, encodingutf-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, encodingutf-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, indexFalse, encodingutf-8-sig) print(清洗完成有效薪资记录数:, len(df_valid)) print(各学历区间占比:\n, df_valid[学历归一].value_counts(normalizeTrue))代码里的关键点在薪资拆分。regex里的[-—~到]是一个字符组覆盖了常见的短横线、中文破折号、波浪线和中文字“到”这是从实际数据里总结出来的坑——招聘方写工资的间隔符五花八门只匹配连字符会漏掉一批数据。匹配后乘以K的单位换算成以千为单位即可。经验归并这里用了简单规则不是最严谨的但目的是给图表提供有序的x轴标签。如果你希望图表x轴按时间顺序排1-3年在3-5年前就需要在归并后把它转成Categorical类型并指定顺序否则pandas按字符串排序“本科”会排在“大专”前面图表显示就不符合直觉。清洗完之后建议把value_counts的结果打出来看一眼确认每个字段的分布都合理再进入可视化。4. 可视化与分析用FlaskECharts搭出能答辩的看板4.1 技术选型为什么是FlaskECharts而不是pyecharts看到“数据可视化”很多同学第一反应是pyecharts因为它几行代码就能生成HTML文件用浏览器打开就是一个像模像样的图表。但放在这个项目里我明确推荐FlaskECharts的纯前端方案理由有三个。第一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, encodingutf-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(debugTrue, port5000)接口逻辑很短但很多同学写到这里就慌了因为groupby的结果不知道用什么结构返回。用reset_index()把索引变成列再遍历转成字典列表是最直观的做法。round(1)是让薪资保留一位小数避免图表tooltip里出现一堆小数位。debugTrue方便本地调试但演示时建议关掉。4.3 核心图表岗位分布、薪资区间、经验要求的三张图看板不需要七八个图表三到四张核心图就够了。我通常会做这几张按岗位关键词统计的岗位数量TOP10柱状图、按学历分组的平均薪资条形图、经验要求的饼图再加一张薪资区间分布散点或箱线图。四张图正好是一屏的布局填得满又不拥挤。ECharts的配置是最容易出视觉效果也最容易出bug的地方。下面是一个柱状图的完整示例option写在script标签里数据通过Ajax从Flask接口读div idsalaryChart styleheight: 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打开看一遍再决定做什么图表——数据长什么样就做什么图不要为了图好看而做图。数据清晰图表自然清楚答辩也就顺利。这个道理同样适用于以后做数据分析项目的选图判断。希望帮到你。本文还有配套的精品资源点击获取
返回列表