Boss直聘爬虫与可视化:反爬突破、数据清洗到薪资分析

发布时间:2026/10/29 7:32:45
Boss直聘爬虫与可视化:反爬突破、数据清洗到薪资分析 简介一套面向Python期末大作业与课程设计的Boss直聘在线爬虫及数据分析可视化系统完整源码附带详细文档说明适合计算机专业学生及爬虫、数据分析入门者。项目基于Django与Django REST framework搭建后端集成requests爬虫、pandas数据处理及matplotlib、seaborn可视化覆盖数据采集、清洗、存储、API接口和图表展示全流程。资源包共665个文件压缩后约7.29MB主要包含25个Python脚本、107个HTML页面模板、229个JavaScript交互文件、61个CSS样式、9个JSON配置以及部分数据文件类型分工明确按后端逻辑、页面模板和爬虫模块分目录存储方便按需检索。已有96人学习下载。附赠使用手册与教学辅助内容包含环境配置说明、启动服务的详细步骤及后端API访问方法源码注释清晰能够帮助使用者快速跑通完整项目完成期末展示或毕业设计参考。1. Boss直聘在线爬虫与可视化系统它到底解决了什么问题做招聘数据采集的人十有八九都在Boss直聘上碰过壁。这个站的反爬强度在招聘类目里属于第一梯队Cookie有效期短、接口加密、请求频率稍微快一点就直接给你弹验证码。但另一边岗位薪资、公司规模、技能要求这些数据又是做职场行情分析最值钱的原材料。把「Boss直聘在线爬虫及数据分析可视化系统」这个标题拆开看它讲的就是三件事一是用可控的方式把Boss直聘上的岗位数据持续抓下来二是把脏乱的招聘信息清洗成能建模的结构化数据三是用可视化大屏把这些数据的价值呈现出来。这个方案适合谁我的判断是有Python基础、想做数据分析作品集或行业调研的人以及公司里需要定期看竞品岗位布局的HR和产品团队。它最大的价值不是爬虫本身而是把采集、清洗、入库、展示串成一条可复现的链路。下面我按自己做过的方案从数据模型讲到代码落地再讲坑和可视化收尾全程不依赖任何商业爬虫框架。2. 先看反爬和数据建模字段怎么设请求频率怎么控2.1 Boss直聘的登录态与反爬边界为什么Cookie是一等公民Boss直聘对未登录用户的访问限制极严大部分岗位列表页直接302到登录页。做在线爬虫第一步就是把登录态管理好。常见做法是用手动扫码登录拿Cookie然后把Cookie持久化到本地文件脚本每次启动都优先加载它。这个方案比账密登录稳妥得多因为模拟账密登录很容易触发设备风控。为什么Cookie是一等公民观察请求特征就能发现Boss直聘的职位搜索接口对请求头里的Cookie、User-Agent、Referer都有校验缺少任何一个字段都可能返回空数据或跳到安全验证页。实际落地时我一般会把Cookie、UA、Referer统一封装成一个请求头字典每次请求都会随机加一个轻微的偏移。请求间隔也很关键不要低于8秒线上跑了一个月后我的结论是短时间高并发是封号的最大诱因宁可慢不能停。2.2 字段模型与存储结构用MySQL还是SQLite数据建模决定了后续分析能不能顺畅进行。岗位数据最核心的字段是岗位名称、公司名称、城市、薪资下限、薪资上限、经验要求、学历要求、岗位标签、技能关键词、发布日期、数据抓取日期。这些字段分布在列表页和详情页两个层级列表页能拿到大部分技能关键词和完整岗位描述需要进详情页。字段类型说明job_idvarchar(32)岗位唯一标识去重用job_namevarchar(64)岗位名称company_namevarchar(128)公司名称cityvarchar(32)工作城市salary_min / salary_maxint解析后存入单位千元/月experiencevarchar(32)经验要求educationvarchar(32)学历要求tagstext岗位标签用逗号分隔skillstext从描述里抽取的技能词fetch_datedate抓取日期增量字段存储我选MySQL不选SQLite。原因很简单在线爬虫是多线程或定时调度跑的SQLite并发写入容易锁库MySQL在并发和后续可视化查询上都更稳。表设计时把fetch_date和job_id建联合索引做“某天新增多少岗位”这类查询会快很多这个细节在数据量过十万后区别非常明显。2.3 requestsxpath为什么够用什么时候才需要上selenium技术选型上requests配合lxml的xpath解析就足够。Boss直聘的列表页HTML结构相对规整岗位卡片都挂在同一个class节点下xpath可以直接定位。很多人一上来就写selenium其实没必要selenium的浏览器指纹反而更容易被风控识别而且资源占用高不适合长时间在线跑。真正需要selenium的场景只有一种接口参数看不懂、拿不到数据时退回到浏览器渲染去观察网络面板。也就是说selenium是排查工具不是主力采集工具。我一般用requests拿HTML用xpath解析用pymysql入库四个库打天下。这套组合的灵活度足够源码结构也更清晰别人拿到手能快速看懂。3. 在线爬虫的最小可运行实现从登录态到增量调度3.1 登录态封装手动扫码一次长期复用Cookie在线爬虫的第一步是把登录态做成可复用资源。第一次运行时用浏览器手动登录Boss直聘登录成功后从浏览器开发者工具里复制Cookie字符串存到项目根目录的cookie.txt。脚本启动时读取这个文件构造请求头。import os import requests def load_cookie(pathcookie.txt): 从本地文件加载登录Cookie if not os.path.exists(path): raise FileNotFoundError(请先把浏览器里的Cookie粘贴到cookie.txt) with open(path, r, encodingutf-8) as f: cookie f.read().strip() return cookie def build_headers(cookie): 构造带登录态的请求头UA和Referer不能省 return { Cookie: cookie, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.zhipin.com/web/geek/job?querypythoncity101010100, } cookie load_cookie() headers build_headers(cookie) session requests.Session() session.headers.update(headers)这段代码有三个关键点。第一load_cookie做了文件存在性检查避免首次运行直接报KeyError第二请求头里的Referer必须跟岗位搜索页保持一致否则请求会异常第三用requests.Session而不是每次新建requests.getSession会帮我们维护底层的TCP连接和部分状态在连续请求场景下表现更好。3.2 采集主流程请求、解析、入库一次跑通主循环的逻辑是翻页请求列表页解析出岗位卡片拿到岗位详情页链接再请求详情页补全技能和描述字段最后入库。这里我把列表页解析和详情页解析分开写好处是某一个阶段挂了可以单独重跑。from lxml import etree import pymysql import time def parse_job_list(html): 从列表页HTML中提取岗位卡片信息 root etree.HTML(html) items [] # 每条岗位卡片都挂在 .job-card-wrapper 下 for card in root.xpath(//*[classjob-card-wrapper]): job {} job[job_id] card.xpath(.//div[classjob-title clearfix]/a/data-jid)[0] job[job_name] card.xpath(.//span[classjob-name]/text())[0].strip() job[company_name] card.xpath(.//span[classcompany-name]/text())[0].strip() job[salary] card.xpath(.//span[classsalary]/text())[0].strip() job[city] card.xpath(.//span[classjob-area]/text())[0].strip() # 详情链接在岗位名的a标签上 detail_url card.xpath(.//div[classjob-title clearfix]/a/href)[0] job[detail_url] https://www.zhipin.com detail_url items.append(job) return items def save_jobs(items, conn): 把列表页数据写入数据库主键冲突时跳过 cursor conn.cursor() sql INSERT IGNORE INTO job_info (job_id, job_name, company_name, city, salary, detail_url) VALUES (%s,%s,%s,%s,%s,%s) for job in items: cursor.execute(sql, (job[job_id], job[job_name], job[company_name], job[city], job[salary], job[detail_url])) conn.commit()注意parse_job_list里我用了.//而非//这是xpath里的细节.表示从当前节点开始搜索避免匹配到页面其他区域的同名class。如果不用.某些页面会把推荐职位和搜索职位混在一起造成重复数据。INSERT IGNORE是去重的最简单手段前提是job_id必须建唯一索引。接下来是详情页解析。详情页能拿到岗位描述、经验、学历要求这些字段列表页不完整。详情页请求时要把job_id拼在URL里解析完直接回写同一行数据。def parse_detail(html): 解析岗位详情页抽取经验、学历、技能标签 root etree.HTML(html) detail {} info_list root.xpath(//*[classjob-sec-content]/div[contains(class, job-primary)]//div[classinfo-primary]//p//text()) # 经验学历通常长这样[1-3年, 本科, 深圳南山区] if len(info_list) 2: detail[experience] info_list[0].strip() detail[education] info_list[1].strip() desc_list root.xpath(//*[classjob-sec-content]//div[classjob-detail]//text()) desc_text .join([t.strip() for t in desc_list]) # 从描述中匹配技能关键词 skill_keywords [Python, Java, MySQL, Redis, Kafka, Docker, K8s] matched [k for k in skill_keywords if k.lower() in desc_text.lower()] detail[skills] ,.join(matched) return detail经验学历的解析有个坑不同岗位的信息顺序不完全一致有的岗位写的“在校/应届”有的写“经验不限”。我在源码里做了顺序假设——第一个是经验、第二个是学历实际跑数据时发现有大概5%的脏数据会颠倒。这个不影响最终统计但会让“学历分布”图表出现少量噪音后面在清洗环节统一处理。3.3 增量与调度怎么做到只抓新岗位增量采集的核心是记住上次抓到了哪一页、哪些岗位已经入库。最简单可靠的方案是用数据库里的fetch_date字段做判断——当天抓过的不再抓跨天则全量重抓。这样避免了维护游标文件的复杂性。def fetch_today_jobs(session, city_code101010100, queryPython): 按城市和关键词抓取当天新增岗位只跑前5页 all_items [] for page in range(1, 6): url fhttps://www.zhipin.com/web/geek/job?query{query}city{city_code}page{page} resp session.get(url, timeout10) if resp.status_code 200 and job-card-wrapper in resp.text: items parse_job_list(resp.text) all_items.extend(items) print(f第{page}页解析出{len(items)}条) else: print(请求异常可能触发验证码停止本轮) break time.sleep(12) # 每个请求之间固定休眠 return all_items参数说明city_code是Boss直聘的城市编码北京是101010100上海是101020100全国是100010000page最大翻到5页是安全线超过之后触发验证码的概率会直线上升time.sleep(12)是我调试了很久之后选出来的值这个间隔既能让一晚上抓完一个城市的核心岗位又不至于触发频率检测。如果你抓的岗位数不需要太多可以进一步降到15秒。调度的方案我推荐系统crontab而不是在Python里写定时循环。用crontab的好处是进程挂了系统会自动拉起不需要在源码里维护进程守护。一个标准的定时任务长这样0 2 * * * cd /opt/boss_jobs /usr/bin/python3 run_crawler.py logs/crawler.log 21这段cron表达式表示每天凌晨两点执行一次采集任务。选凌晨跑是因为这个时间段站点活跃度低反爬压力小而且早上起来正好能看前一天的增量数据报表。4. 数据清洗与指标口径薪资怎么解析哪些字段能信4.1 薪资解析K、千/月、14薪的归一化处理Boss直聘的薪资字段长什么样常见的是15-30K、20-30K·14薪、200-300元/天、30-40K·15薪。这些形态不统一直接入库后完全没法做均值分析。我的方案是写一个解析函数把文字拆成下限、上限、薪资类型、发薪月数四个字段。import re def parse_salary(salary_text): 把Boss直聘的薪资文本解析成结构化字段 if isinstance(salary_text, float) or not salary_text: return {min_k: 0, max_k: 0, type: , months: 12} text salary_text.strip() months 12 m re.search(r(\d)薪, text) if m: months int(m.group(1)) if 元/天 in text: nums re.findall(r\d, text) min_k int(nums[0]) * 30 / 1000 max_k int(nums[1]) * 30 / 1000 if len(nums) 1 else min_k return {min_k: round(min_k, 1), max_k: round(max_k, 1), type: day, months: 1} if K in text or k in text: nums re.findall(r(\d(?:\.\d)?), text.replace(K, ).replace(k, )) if len(nums) 2: return {min_k: float(nums[0]), max_k: float(nums[1]), type: month, months: months} elif len(nums) 1: return {min_k: float(nums[0]), max_k: float(nums[0]), type: month, months: months} return {min_k: 0, max_k: 0, type: unknown, months: 12}拆出来之后怎么算口径我用的指标是(min_k max_k) / 2 * months / 12也就是说把14薪折算成年包再还原成月均。这样不同发薪月数的岗位才能放在同一张图上比较。这里有个很多人会踩的坑直接把15-30K·14薪当15-30K处理导致该岗位的实际月收入被低估约17%。4.2 岗位分类与技能标签让分词替代人工打标原始数据里岗位名称千奇百怪Python开发、高级Python工程师、Python后台开发其实都是Python岗但直接按名字分组会裂成几十个类。我的做法是用关键词映射做一个粗分类Python、Java、前端、算法、测试、运维这几个词先匹配匹配不到的就归入其他。技能标签的提取更简单先从岗位描述里匹配预置的技术词列表匹配不到的再用jieba分词后做词频统计取Top3作为补充。这个方案比起纯机器学习分类胜在可解释性强每一行数据的分类依据都能说清楚代码也容易维护。不要一上来就整BERT模型这个场景下性价比极低。4.3 指标设计月薪均值、供需比、经验分布怎么算可视化系统需要有稳定口径的指标我一般会维护一张日汇总表每天采集完自动算一次。关键指标如下指标名计算逻辑用途平均月薪全部岗位折算月薪的算术平均值城市薪酬基准薪资中位数排序取中受极端值影响小更真实的薪酬水平供需比岗位数量 / 平均每天新增岗位数判断岗位增长速度学历分布按本科、硕士、大专分组计数门槛可视化技能频次skills字段的拆词计数热门技能趋势这五个指标足以支撑一张职场行情看板。平均月薪看趋势中位数看真实供需比看冷热学历和技能看门槛。再复杂的模型不是当前阶段该做的事先把口径固定下来后面做时间序列才有基准。5. 避坑Boss直聘爬虫的五个典型翻车现场5.1 翻车现场一Cookie明明是对的却一直302跳登录现象首次运行正常第二天再跑同一个Cookie发现每次请求都被302到登录页。原因Boss直聘的Cookie有效期很短尤其是__zp_stoken__这个字段几乎每天都会失效。失效后Session不会自动重登请求就一直在登录页打转。解决在采集主逻辑里加Cookie有效性检查每次请求后判断响应URL是否包含/web/geek/job若包含则说明登录态正常若跳转到了/web/user/或登录页就停止任务并发送告警通知。养成每天手动刷新一次Cookie的习惯或者做一个Cookie失效自动通知机制。5.2 翻车现场二同一个岗位采集到几百条重复数据现象数据库里出现大量相同job_id的记录导致统计时岗位数量虚高。原因列表页请求成功后解析逻辑没有去重更隐蔽的原因是INSERT IGNORE依赖唯一索引但建表时漏掉了job_id的唯一约束。解决建表时必须给job_id加唯一索引不要靠应用层过滤。其次在解析函数末尾加一个seen set()来拦截本次请求内部重复的卡片。两层兜底能压住99%的重复数据。5.3 翻车现场三夜间定时任务跑了十分钟就挂现象crontab里设置了凌晨2点执行看日志发现每次只跑了几十个请求就退出日志末尾也没有报错信息。原因事件一触发安全验证脚本里的time.sleep(12)没有生效因为详情页请求里有几个没有走同一个Session导致请求间隔失效被风控命中直接断连。解决把详情页请求也统一挂到同一个requests.Session上并且在session.get外层包一个try...except requests.exceptions.ConnectionError遇到断连就退出而不是让堆栈信息淹没在日志里。再加一个进程退出码crontab看到非0退出码时发邮件。5.4 翻车现场四平均薪资算出来高得离谱现象城市平均月薪统计结果比招聘平台公开报告高出30%以上。原因薪资解析函数对15-30K·14薪处理正确了但对200-300元/天按30天折算月薪导致实习岗和兼职岗被放大成高薪全职岗。解决在统计层面对薪资类型做筛选只看type month的记录day和hour类型的岗单独统计。如果你的分析目标就是“全职岗位薪酬行情”这一步必须做。这也是我在4.1节里单独加type字段的原因。5.5 翻车现场五可视化图表的数字跟数据库对不上现象ECharts大屏上显示的岗位总数比数据库SQL查询结果少了几千。原因后端接口直连数据库但表里包含了全量历史数据而前端的筛选条件默认只查近30天。两边口径不一致一个看总量一个看增量。解决定一个数据接口标准——所有可视化接口必须至少接收start_date和end_date两个参数默认区间统一为最近30天同时在SQL查询里加fetch_date BETWEEN %s AND %s。口径统一之后图表和明细才能对得上。6. 可视化设计与持续迭代用ECharts做一张能看懂行情的大屏6.1 接口与图表选型数据服务先行图表是最后一公里可视化这块我用ECharts原因是在Python技术栈里它的生态最成熟。前端只需要四个图表城市薪资地图用map薪资区间分布用bar热门技能Top10用horizontal bar市场需求趋势用line。接口我用Flask写一个极简的JSON接口返回清洗后的聚合数据。from flask import Flask, jsonify, request import pymysql app Flask(__name__) def query_db(sql, paramsNone): conn pymysql.connect(hostlocalhost, userroot, password123456, databaseboss_jobs, charsetutf8mb4) cursor conn.cursor() cursor.execute(sql, params or ()) cols [col[0] for col in cursor.description] rows [dict(zip(cols, row)) for row in cursor.fetchall()] conn.close() return rows app.route(/api/salary_trend) def salary_trend(): 按周聚合的平均月薪趋势接口 days request.args.get(days, 30, typeint) sql SELECT DATE_FORMAT(fetch_date, %%Y-%%m-%%d) AS day, AVG((salary_min salary_max) / 2) AS avg_salary FROM job_info WHERE fetch_date DATE_SUB(CURDATE(), INTERVAL %s DAY) GROUP BY day ORDER BY day data query_db(sql, (days,)) return jsonify(data) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)这个接口就是大屏的数据底座。注意SQL里的%%Y-%%m-%%d写法在Python的格式化场景下%需要转义不转义会直接报SQL语法错误。参数days默认30前端传多少就用多少后续想看季度趋势直接改参数即可。6.2 数据质量监控与迭代方向可视化不只是把图做出来更要能让人在图表里一眼发现异常。我习惯在接口层加一个简单的数据质量检查如果今天的采集量比昨天下降超过50%接口返回的JSON里带上anomaly: true前端拿到这个标记就在大屏角落展示一个黄色警告条。这一步能帮你及时察觉到账号风控或者采集进程异常。如果你要持续运行这套系统我建议的迭代路线是先做到每天稳定抓一个城市、一个岗位方向的数据跑满一个月形成时间序列再扩展到五个核心城市最后再把岗位描述文本做成词云或主题模型。不要一开始就追求大而全数据的稳定性远大于数据的广度。我自己的教训是第一版系统 пытался抓了六个城市和几十个岗位方向结果维护成本极高每周都在排查脏数据后来狠心砍到只抓一个核心维度数据质量立刻上来了。希望这套方案能帮你在Boss直聘的数据采集和可视化分析上少走几个月的弯路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询