资讯详情

资讯详情

基于Python的Boss直聘岗位数据采集与可视化分析实践

简介面向计算机相关专业学生的Python综合实践项目资料聚焦Boss直聘岗位数据采集与可视化分析完整覆盖爬虫编写、数据清洗、统计分析和图表展示全流程。项目基于Scrapy框架构建评审得分99分可作为期末大作业、课程设计或毕业设计的高分参考。资源包共38个文件压缩包约246KB。核心为13个Python脚本包含爬虫蜘蛛、中间件、管道等标准工程模块12个JavaScript脚本与HTML/CSS配合实现可视化界面另有全国热门城市岗位数据CSV、Scrapy配置文件及文档说明便于对照理解数据从采集到呈现的每个环节。项目以模块化方式组织方便按需阅读与二次开发。目前已有162人学习下载。代码完整、目录清晰下载后可直接运行特别适合正在完成课程项目、希望掌握真实爬虫项目结构的学生以及需要项目实战练习的Python学习者快速上手。1. 这个项目解决了什么问题谁需要一份真实的岗位行情数据如果你曾经为了给朋友参谋“现在转行做数据分析还值不值”在招聘网站上一页页翻岗位、复制粘贴到表格里、再手工算平均薪资你应该能体会到这件事有多折磨人。基于Python实现Boss直聘岗位数据采集及分析可视化本质上是把这条链路自动化采集岗位列表与详情、清洗成规整的表格、再输出成看板和统计图让“这家公司给多少、这个城市要什么条件、这个岗位现在什么价”不再靠肉眼和手记。这个方案适合三类人正在求职的人用来校准自己的薪资预期和简历方向HR或者猎头用来做行业薪酬调研做课程的老师或者刚学完Python的同学把它当综合项目练手。门槛不在Python语法而在Boss直聘的反爬机制——绕过它后面全是标准的数据处理功夫。2. 采集层Boss直聘的反爬到底是什么水平以及怎么写第一版采集代码2.1 先弄清楚Boss直聘的反爬边界requests方案在哪些环节撑不住很多教程还在用requests直接请求Boss直聘的接口把Cookie硬编码到代码里。这个方案在2020年前后确实还能跑现在基本撑不过一个完整采集周期。原因有三个层面。第一层是登录态。Boss直聘的岗位列表和详情页要求登录才能看完整字段未登录状态只能拿到模糊的岗位标题。requests方案的登录态依赖Cookie而Cookie频繁失效失效后代码不会报错只会在后续解析时拿不到任何数据排查起来很费劲。第二层是接口签名。Boss直聘的Web端接口带有动态参数光靠修改请求头很难伪造出可信的请求。第三层是IP风控同一个IP在短时间内高频请求会触发滑块验证码甚至临时封禁封禁时不返回错误码而是返回一个空页面。所以现在的常见做法是放弃纯requests改用浏览器自动化方案。我一般用DrissionPage它本质上是通过Chrome DevTools Protocol控制真实浏览器和Selenium相比它不依赖webdriver这个会被网站特征识别的中间层识别难度明显低。如果你非要选Selenium也可以但要做好被拦的心理准备——这是这个项目里最玄学的部分同一套代码今天能跑明天翻车先有心理预期。2.2 用DrissionPage跑通岗位列表的最小采集代码先安装依赖Python版本建议3.9以上确保pip可用。如果你刚配好Python环境这一步跑完就说明环境没有问题。pip install DrissionPage pandas openpyxl安装完成后写第一版采集脚本。这里以采集上海地区“Python开发”岗位为例注意代码里的选择器和字段名是基于当时页面结构写的页面改版后需要按实际情况调整。from DrissionPage import ChromiumPage, ChromiumOptions import time # 打开浏览器并指定用户数据目录登录状态会被保留下次启动无需重新扫码 co ChromiumOptions() co.set_user_data_path(./boss_profile) co.set_argument(--window-size1920,1080) # 启动浏览器并访问Boss直聘搜索页city参数是城市编码 page ChromiumPage(co) page.get(https://www.zhipin.com/web/geek/job?queryPythoncity1010200) page.wait.load_start() time.sleep(3) # 定位岗位卡片列表Boss直聘每个岗位是一个job-card-wrapper cards page.eles(.job-card-wrapper) print(f本页找到 {len(cards)} 个岗位) for card in cards[:20]: try: title card.ele(.job-name).text.strip() company card.ele(.company-name).text.strip() salary card.ele(.salary).text.strip() tags card.ele(.job-card-tags).text.strip() print(title, company, salary, tags) except Exception as e: print(解析失败跳过一条, e)这段代码的逻辑分四步启动浏览器、访问搜索页、等待页面渲染、定位岗位卡片并逐个提取字段。set_user_data_path是最关键的一步它指定了一个本地目录来保存浏览器的Cookie和登录状态。第一次运行时你需要手动扫码登录之后每次启动都会自动带上登录态这比硬编码Cookie要可靠得多。city1010200是上海的城市编码其他城市可以在Boss直聘网页版切换城市后从URL里看到对应的数字参数建议先手动打开页面确认不要凭经验乱填。这里要特别提醒一点page.eles(.job-card-wrapper)返回的是元素列表但招聘网站页面结构经常调整如果你运行后len(cards)的结果是0先打开浏览器手动检查页面在开发者工具里查看岗位卡片实际使用的CSS类名。不要急着改代码先确认页面里到底有没有这个类。这是这个项目里最常见的“不是代码错而是页面变了”的坑。2.3 分页、详情页与字段抓取把采集从“一条”变成“一套”列表页能抓到基本信息之后接着要解决两个问题翻页和详情页字段。翻页的逻辑在Boss直聘上比较隐蔽它的Web端没有传统的页码条而是通过滚动加载或“下一页”按钮来实现。我一般用两种方式判断一是尝试在页面底部找“下一页”按钮二是滚动到页面底部后等待新卡片加载。下面是一段可复用的翻页逻辑。import time def turn_page(page, max_pages10): for i in range(max_pages): # 尝试点击下一页按钮如果没有则滚动到底部触发加载 try: next_btn page.ele(.ui-icon-arrow-right, timeout1) if next_btn: next_btn.click() time.sleep(2) else: page.scroll.to_bottom() time.sleep(3) except Exception: # 按钮找不到且滚动无新内容说明已经到最后一页 print(f第 {i1} 页翻页结束或已到末页) break翻页之后的隐藏需求是去重。Boss直聘的搜索页在翻页时可能会重复展示部分岗位你需要在保存数据时记录岗位的唯一标识一般可以从岗位详情页的链接里提取jobId之类的参数。如果不做去重采集3页数据里可能有10%是重复的后续分析时的统计数字会是错的。至于详情页建议按需采集。如果你只想统计薪资、学历、经验这三个维度列表页的标签字段基本够用。只有当你需要采集岗位描述、技能关键词、福利待遇等深度信息时才需要进入详情页。详情页的URL可以从列表页的岗位卡片链接里拿到常见的模式是在原URL后拼接?jobIdxxx不同城市和岗位类型可能结构略有不同进入页面后同样使用CSS类名定位正文内容。详情页采集的重点是控制频率每进入一个详情页建议至少等待1到2秒不是怕慢是怕封。3. 清洗与入库把“15-20K·本科·3-5年”拆成能分析的字段3.1 Boss直聘原始数据的“脏”来自三处采集到的原始数据和人眼看到的招聘页面基本一致但直接拿来分析是没法用的。脏数据主要来自三处。第一处是薪资字段。页面上显示的是“15-20K·13薪”或者“1.5万-2万·14薪”这需要拆出下限、上限和年终薪月份还要把“万”换算成“K”才能算出月均薪资。第二处是经验与学历混在一个字段里比如“3-5年·本科”是一条字符串实际分析时需要拆成“经验区间”和“学历要求”两列。第三处是城市字段Boss直聘显示的是“上海·浦东新区”需要拆出城市一级行政区和区域名后续做城市维度分析时才能按“上海”分组。如果不做清洗直接分析你会发现所有统计结果都是乱的按薪资排序时“1.5万”排在了“20K”的后面因为字符串排序“1”小于“2”按学历分析时“本科”和“经验不限·本科”是两个不匹配的值。清洗是这个项目里最脏最累但最值得花时间的环节。3.2 清洗脚本正则拆分薪资、经验、学历与城市下面是一段用pandas和正则表达式实现的清洗脚本适用于大部分“15-20K”“1.5万-2万”“100元/天”等格式的薪资字段。import pandas as pd import re def parse_salary(s): if not isinstance(s, str): return None, None, None, None # 处理“15-20K·13薪”格式 s s.replace(·, ) monthly None months 12 # 拆分出薪资主体和薪月份数 parts s.split( ) salary_part parts[0] if len(parts) 1 and 薪 in parts[1]: months int(re.search(r\d, parts[1]).group()) # 处理“K”和“万”两种单位 if K in salary_part.upper(): nums re.findall(r(\d\.?\d*), salary_part) if len(nums) 1: low high float(nums[0]) else: low, high float(nums[0]), float(nums[1]) low * 1000 high * 1000 elif 万 in salary_part: nums re.findall(r(\d\.?\d*), salary_part) if len(nums) 1: low high float(nums[0]) * 10000 else: low, high float(nums[0]) * 10000, float(nums[1]) * 10000 else: return None, None, None, None # 按12或13薪折算月均薪资 monthly_low low * 12 / months monthly_high high * 12 / months mid (monthly_low monthly_high) / 2 return low, high, mid, months # 示例用法 df pd.DataFrame({salary_raw: [15-20K·13薪, 1.5万-2万·14薪, 100元/天]}) df[[salary_low, salary_high, salary_mid, months]] df[salary_raw].apply( lambda x: pd.Series(parse_salary(x)) ) print(df)这里的parse_salary函数返回4个字段年薪折算下限、年薪折算上限、折算后月均薪资中位数、以及原始的年薪月份数。折算的逻辑是如果写“15-20K·13薪”说明一年发13个月工资那么把月薪乘以12再除以13就会得到一个比原月薪略低的实际月均收入。这样处理之后不同岗位之间的薪资对比才有意义否则“20K·12薪”和“15K·15薪”很难直接比大小。正则部分使用了re.findall提取所有数字以兼容“10-15K”和“10K以上”这类不对称写法。如果解析不出来就返回None后续在dropna时统一处理而不是在解析阶段强行填0或者中位数那样会污染统计结果。经验、学历和城市字段的拆分相对简单。def split_exp_edu(s): if not isinstance(s, str): return None, None parts s.split(·) if len(parts) 2: return parts[0].strip(), parts[1].strip() return s.strip(), None def split_city(s): if not isinstance(s, str): return None, None parts s.split(·) if len(parts) 2: return parts[0].strip(), parts[1].strip() return s.strip(), None df[exp] df[exp_edu_raw].apply(lambda x: split_exp_edu(x)[0]) df[edu] df[exp_edu_raw].apply(lambda x: split_exp_edu(x)[1]) df[city] df[city_raw].apply(lambda x: split_city(x)[0])这段逻辑的思想很简单Boss直聘的字段分隔符是“·”直接按分隔符切分取第一个元素。但有一个常见坑值注意部分岗位的学历和经验字段是分开显示的而不是合并字段清洗前先打印df.columns和几条样本确认数据来源的列名再动手。3.3 存储选型SQLite 与 CSV 怎么选清洗后的数据需要持久化。常见选项有CSV、Excel、SQLite、MySQL针对这类个人项目我在CSV和SQLite之间做选择一般不推荐Excel因为pandas读Excel依赖openpyxl之外的额外库且大数据量下打开卡顿。存储方式适合场景优点缺点CSV单次采集、快速交付用Excel打开方便、代码简单增量更新麻烦、字段类型丢失SQLite多次采集、增量管理支持SQL去重、类型固定、单文件学习SQL有门槛如果你是第一次做这个项目先用CSV把全流程跑通再迁移到SQLite。如果你已经确定要长期采集直接上SQLite下面的建表示例可以作为参考。CREATE TABLE jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, job_id TEXT UNIQUE, title TEXT, company TEXT, salary_low REAL, salary_high REAL, salary_mid REAL, exp TEXT, edu TEXT, city TEXT, area TEXT, crawled_at TEXT );job_id TEXT UNIQUE这一行是在数据库层面确保去重采集的时候先把job_id插入如果重复就自动忽略这样增量采集时不需要手动查重。crawled_at记录抓取时间做趋势分析时能知道每个岗位是在什么时间点被抓到的避免后续做“薪资随时间变化”分析时没有时间维度可看。4. 分析与可视化让岗位数据回答“现在值多少钱”4.1 先定分析指标再谈可视化很多人拿到数据第一件事就是画图画完才发现不知道要回答什么问题。正确顺序是先定分析指标再选择对应的图表类型。基于Boss直聘的岗位数据我一般会先做四个分析。第一个指标是“各城市平均薪资”评估岗位在不同城市的薪酬水平差异。第二个是“学历要求分布”看本科、大专、学历不限的岗位比例判断门槛是否在提高。第三个是“经验要求与薪资的关系”把工作经验和薪资做交叉分析看在哪个年限段涨薪最明显。第四个是“技能关键词词频”从岗位描述或标签里提取技能词统计哪些技术栈被提及最多。这四个指标覆盖了“值多少钱”“门槛多高”“趋势在哪”三个核心问题。这里有一个容易被忽略的点分析前先做一次去重。用df.drop_duplicates(subset[job_id])去掉重复岗位再用df.dropna(subset[salary_mid])把薪资解析失败的数据过滤掉。去重和过滤的顺序不要反先按岗位去重再过滤异常值这样每条数据代表一个真实岗位分析结果才有意义。4.2 用 pyecharts 生成岗位分析可视化大屏pyecharts是把ECharts的能力封装成Python接口生成HTML文件浏览器打开即看适合做数据采集及可视化项目的最终交付。下面是一个生成城市平均薪资柱状图和学历分布饼图的完整脚本。from pyecharts.charts import Bar, Pie from pyecharts import options as opts # 假设df是清洗且去重后的数据表 city_salary df.groupby(city)[salary_mid].mean().sort_values(ascendingFalse) bar Bar() bar.add_xaxis(city_salary.index.tolist()) bar.add_yaxis(平均月薪元, [round(v, 0) for v in city_salary.values]) bar.set_global_opts( title_optsopts.TitleOpts(title各城市平均月薪TOP10), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate45)), ) bar.render(city_salary.html) edu_ratio df[edu].value_counts(normalizeTrue) pie Pie() pie.add_xaxis(edu_ratio.index.tolist()) pie.add_yaxis(学历占比, [round(v * 100, 1) for v in edu_ratio.values]) pie.set_global_opts(title_optsopts.TitleOpts(title学历要求分布)) pie.render(edu_pie.html)groupby(city)[salary_mid].mean()是按城市分组求平均薪资sort_values(ascendingFalse)保证城市按薪资从高到低排列图表读起来符合直觉。柱状图的rotate45是防止城市名过长时互相遮挡。饼图的value_counts(normalizeTrue)把频次转成比例展示占比比展示原始数量更直观。生成的是HTML文件浏览器直接打开后续可以合并成一个页面做可视化大屏也可以导出为图片放进报告里。如果你想做可视化大屏效果不满足于简单的柱状图和饼图可以再加一个全国地图。但地图需要加载中国地图数据pyecharts默认不带完整地图文件需要先下载地图JSON并注册。这一步是这个项目里最烦人的环节很多人会在这里卡住。建议第一版先不要做地图用柱状图加城市排序代替信息密度足够也不依赖地图文件。4.3 图表之外的解读怎么判断结果的可用性图表只是结果展示真正值钱的是解读。分析时要注意三个容易误导自己的东西。第一个是样本量。如果你只采集了20条数据平均薪资是15000元和18000元之间的差异没有统计意义。我在写解读时一般会先看样本量少于30条的维度不写结论。第二个是极端值。有些岗位标称“100元/天”的实习岗会拉低平均薪资而某些“50K以上”的技术专家岗位会拉高平均值。如果中位数和平均值的差距超过30%说明数据里有明显的极端值建议在报告里同时展示中位数。第三个是时间窗口。Boss直聘上的岗位是动态下架和新增的今天采集的数据只能代表今天的情况如果你的报告写了“2024年Python岗位薪资分析”但没有注明采集日期这个分析是站不住脚的。我一般会在报告开头加一行“数据采集自Boss直聘采集日期2024年X月X日样本量N条分析结果仅代表采集时点的市场状况。”这句话看起来平淡实际上避免了报告被当成长期行情参考而误用。5. 采集避坑指南封控、验证码、字段缺失的5条实战记录5.1 爬到一半突然被要求扫码登录现象代码正常运行数据正常解析爬到第几十条时突然页面跳转到登录页之后所有字段解析失败。原因Boss直聘的登录态不是永久有效的长时间不间断请求会触发重新验证。另一个常见情况是浏览器用户数据目录里的Cookie过期而代码没有做登录态检测。解决在采集主循环里加一个页面状态检测如果发现URL跳转到了登录页就暂停采集并输出提示。手动扫码后继续。代码实现是每次翻页后检查page.url里是否包含login包含则等待人工处理。5.2 换一台电脑跑同样的代码验证码概率陡增现象在自己电脑上跑得好好的部署到服务器或者别的电脑上一启动就被要求弹验证码点完滑块还是反复弹。原因Boss直聘的风控系统会综合判断IP归属、浏览器指纹、访问频率等多个信号。服务器的IP往往是机房段被标记为高风险的机率远大于家用宽带。另外新电脑的浏览器指纹没有历史访问记录也会被当成可疑访问。解决如果你不是在服务器上跑而是本地换电脑建议直接复制之前的用户数据目录谷歌浏览器里叫“用户数据”DrissionPage里指定的boss_profile目录直接拷贝过去。如果复制后还在弹验证码就不要在同一IP下高频采集把采集频率降到每条间隔5秒以上。服务器场景没有太好的办法降低并发和最基础的伪装然后接受偶发验证码这是反爬的常态。5.3 列表页能打开详情页却拿不到任何字段现象列表页正常提取10个岗位点击进入详情页后card.ele(.xxx)返回空值整个流程只能采集到列表页的数据。原因详情页的字段结构和列表页完全不同选择器对不上。常见的错误是把列表页的类名直接用在详情页上比如列表页里公司名是.company-name详情页里的公司名可能是.name或者.company-info。解决先手动打开一个详情页在浏览器开发者工具里检查实际的DOM结构把详情页需要的字段选择器独立写一组函数不要和列表页混用一个解析函数。我习惯把列表页解析和详情页解析分成两个函数各自维护选择器页面改版时只需要改对应函数不用牵一发动全身。5.4 薪资解析后出现大量 NaN现象清洗后salary_mid列有30%以上的空值统计图表里这些数据全部消失了。原因薪资格式超出了正则匹配范围比如“面议”“3-4.5千”“8000-10000元/月”这类非标准写法。“3-4.5千”里的单位是“千”而不是“K”代码里只处理了K和万所以没有匹配成功。解决在解析函数里增加“千”和“元/月”两种格式的处理。还有个技巧是先将字符串统一替换小写k为大写K把“元/月”替换为“K”再正则这样大部分格式都能收敛到一个表达式里。遇到“面议”这类无法解析的字段统一填充None分析时过滤掉就好。不需要强行想一个特定规则去解析所有格式那是把自己的时间浪费在奇怪的边界情况上。5.5 可视化图表里中文全部变成方块现象生成的HTML图表在浏览器里打开横轴城市名和标签全部显示为小方块英文正常。原因pyecharts渲染的HTML文件引用的字体不支持中文字符集或者默认字体在系统里没有匹配到中文字形。解决在set_global_opts里指定字体为Microsoft YaHei或SimHei代码是opts.TitleOpts(title..., title_textstyle_optsopts.TextStyleOpts(font_familyMicrosoft YaHei))。如果你用在线模式引用ECharts的CDN字体问题可能出在CDN加载失败需要保持网络畅通。这个坑不常遇到但遇到一次就能让人多花半小时排查。更稳妥的做法是在生成HTML后用浏览器打开确认中文渲染正常再交付。6. 把单次采集变成增量管道去重、定时与结果校验6.1 增量采集用 job_id 做去重按关键词归档单次采集跑通了下一步是让脚本能持续运行而不用每次手动启动。增量采集的核心是去重去重的主键是岗位的唯一ID在采集时提取并写入数据库。import sqlite3 import datetime conn sqlite3.connect(jobs.db) cursor conn.cursor() today datetime.date.today().isoformat().replace(-, ) # 采集时先判断岗位是否已存在 def insert_job(job): cursor.execute( INSERT OR IGNORE INTO jobs (job_id, title, company, salary_mid, exp, edu, city, crawled_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?), (job[job_id], job[title], job[company], job[salary_mid], job[exp], job[edu], job[city], today) ) conn.commit()INSERT OR IGNORE利用前面建表时的job_id TEXT UNIQUE约束实现去重重复数据直接跳过不用先查询再判断逻辑简洁也高效。如果你用的是CSV存储增量更新的思路是完全一样的先读旧文件与新增数据拼接后drop_duplicates再写回。定时调度可以用操作系统的crontab也可以写一个简单的Python循环配合time.sleep每隔几小时跑一次。建议在采集脚本里按关键词归档比如“Python开发”“数据分析”“前端开发”各一个任务每个任务跑自己的关键词避免一次采集把接口查穿。日志也要单独存文件不然脚本跑崩了连在哪里崩的都找不到。6.2 结果校验抽样比对法与字段完整性检查自动化采集最大的风险不是采集不到而是采集到了错误的数据你却不知道。所以我有一套固定的校验流程每次跑完采集后执行。首先是随机抽样从数据库里随机抽出5到10条记录打开Boss直聘网页手动比对岗位标题、薪资、公司名是否一致。抽样比对能发现选择器错位、标签页切换失败等解析层的问题比检查代码更直接。其次是字段完整性检查看每一列的非空比例如果某列的空值率突然从5%涨到50%大概率是页面结构变了解析规则该更新了。最后是数量级检查对比今天采集的岗位数量和昨天是否在一个合理范围内。如果昨天200条、今天2000条或者反过来都说明翻页或者去重逻辑出了问题。这套流程看起来简单但它才是让采集结果可信的关键。我在这类采集项目上踩过最大的坑就是花了三个小时分析了一批数据最后发现薪资中位数被一个解析错误的“100元/天”实习岗拉低了几千块——从那以后我再也不敢跳过校验环节。希望帮到你也祝你第一次采集就顺利跑通。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →