Boss直聘爬虫与数据分析可视化系统搭建实战
发布时间:2026/10/7 3:40:21 作者:尧图编辑部 阅读量:1,286

简介面向计算机相关专业准备期末大作业、毕业设计或课程设计的学生以及希望提升爬虫与数据分析实战能力的学习者这是一套Boss直聘在线爬虫及数据分析可视化系统完整源码包代码经导师指导优化评审达99分可直接运行。项目基于Django与Django REST framework搭建后端服务结合pandas、matplotlib、seaborn实现数据清洗、分析与可视化覆盖爬取、存储、展示全流程。压缩包共665个文件、7.29MB包含Python源码、HTML页面、JavaScript脚本、CSS样式及图片图表等资源zbak备份文件便于还原工程状态。已有96人浏览学习。配套文档详细说明环境配置、依赖安装与服务启动方法另有附赠内容包、boss_drf爬虫模块、back后端代码和templates前端模板代码注释清晰可帮助理解接口设计与数据可视化等关键环节项目整体结构完整、运行可靠适合二次开发与学习参考。1. 这份 Boss直聘在线爬虫及数据分析可视化系统值得自己亲手搭一遍如果你正准备搭一个 Boss直聘在线爬虫及数据分析可视化系统最想解决的事往往不是“能不能爬到”而是“从平台推送给我的岗位列表里看不出真实行情”。Boss直聘上同一个 Python 岗位薪资能差出一倍学历、经验、公司规模全混在一起靠肉眼一条条刷既慢又容易漏。这套系统的价值就是把搜索、采集、清洗、入库、出图一条链路打通让“某城市某岗位的真实开价区间”变成一张随时可刷新的看板。它适合两类人一类是正在学 Python 爬虫和数据分析的开发者想拿一个真实项目练手另一类是有求职决策需求想量化岗位供给和市场行情的从业者。别再捧着别人发的截图做判断了数据自己抓一遍心里才踏实。2. 先定架构再写爬虫requests MySQL ECharts 的选型理由与数据模型2.1 为什么是 requests 串行爬取而不是一上来就 Scrapy 分布式很多新手一聊爬虫就直奔 Scrapy但这套系统我建议先用 requests 把单机串行跑通。原因很直白Boss直聘单个城市、单个关键词的搜索结果通常只有几十页按每页 15 条算总量也就几百到一千条出头。用 requests 配合随机延时串行爬一两个小时能完整跑完一轮完全够数据分析用了。Scrapy 的优势在规模化采集、去重调度、中间件扩展可这些能力在千条级数据量下全是过度设计。另一个容易被忽略的点是维护成本。Scrapy 的项目结构固定spider、item、pipeline 分层严格写完之后改字段名、调接口参数都得同时动多个文件。requests 写的采集脚本一个函数对应一个接口出问题时打开文件就能定位。对于这个场景代码可读性和改造成本远比框架的扩展性重要。分布式爬虫通常是团队作战、需要 Redis 队列和任务分发时才上的方案个人做岗位数据分析完全没必要。选型上我最后定的是 requests MySQL ECharts。requests 负责 HTTP 通信MySQL 负责结构化存储ECharts 负责把聚合结果渲染成大屏和报表。中间不插消息队列不插大数据组件。这套组合对“千级数据量、单机部署、定时刷新”的需求来说是性价比最高的路径。数据分析项目最怕的不是数据量大而是链路里每一环都要花时间学会用。能三个月跑完的事别拖成半年。2.2 岗位表怎么建字段设计、去重与薪资拆分后的存储数据库表结构决定了后续所有分析能不能顺畅写 SQL。我把字段分成三组职位基本信息、薪资解析结果、采集元信息。其中薪资不能只存原始字符串要拆成最低、最高、平均值和月薪倍数否则后面聚合分析时你会在字符串上做数学运算那是给自己埋雷。建表语句如下CREATE TABLE job_info ( id INT AUTO_INCREMENT PRIMARY KEY, job_id VARCHAR(64) NOT NULL COMMENT Boss直聘职位ID天然唯一键, job_name VARCHAR(128) NOT NULL COMMENT 职位名称, salary_min INT DEFAULT 0 COMMENT 薪资下限单位K, salary_max INT DEFAULT 0 COMMENT 薪资上限单位K, salary_avg INT DEFAULT 0 COMMENT 薪资均值单位K, salary_months INT DEFAULT 12 COMMENT 年薪月数如15薪, salary_raw VARCHAR(64) DEFAULT COMMENT 原始薪资文本, company_name VARCHAR(128) COMMENT 公司名称, company_size VARCHAR(32) COMMENT 公司规模, industry VARCHAR(64) COMMENT 行业, experience_required VARCHAR(32) COMMENT 经验要求, education_required VARCHAR(32) COMMENT 学历要求, city_code VARCHAR(16) COMMENT 城市编码, city_name VARCHAR(32) COMMENT 城市名, district VARCHAR(64) COMMENT 区域, skill_tags JSON COMMENT 技能标签JSON数组, publish_date DATETIME COMMENT 发布日期, detail_url VARCHAR(256) COMMENT 职位详情URL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_job_id (job_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明里最核心的是job_id这个唯一键。Boss直聘的职位 ID 是加密串同一个职位不管搜多少次ID 不变。把它设成唯一索引后用INSERT IGNORE或者ON DUPLICATE KEY UPDATE就能天然完成去重和更新完全不需要再写一道“判断职位是否存在”的逻辑。salary_raw字段必须留它是对照原文的“后悔药”清洗错了还能回头查原始值。skill_tags用 JSON 类型因为技能标签数量不定一个职位可能是 3 个也可能是 8 个塞 VARCHAR 里后续解析绕远路。2.3 项目目录与配置拆分让爬虫、清洗、可视化互不干扰这套系统里既有定时采集任务又有 Flask 数据服务还有静态页面。如果所有代码都堆在同一个文件里跑一天没问题跑一周就会后悔。我建议按职责拆目录每个模块只做一件事。boss_spider/ ├── config.py # 全局配置Cookie、关键词、城市、延迟区间 ├── db.py # MySQL 连接池与通用查询 ├── spider.py # 搜索页爬取解析 joblist.json ├── detail.py # 详情页爬取补充技能标签等字段 ├── clean.py # 薪资与字段清洗逻辑 ├── analysis.py # 聚合 SQL 封装供 Flask 调用 ├── app.py # Flask 服务提供 /api/xxx 数据接口 ├── templates/ │ └── dashboard.html # 可视化大屏页面 └── static/ └── echarts.min.js配置全部集中到config.py是为了让关键词、城市、Cookie 这些经常变的东西不用进代码逻辑。换城市只改cityCode换岗位只改query。不要把 Cookie 写死在爬虫函数里一旦登录态失效你要改的是一处配置而不是满文件找字符串。这个目录结构也方便你后续加新的分析维度比如想统计“各公司 Python 岗位数量”直接在analysis.py里加一个函数就好爬虫代码完全不用动。3. 拆解搜索接口登录态、请求参数与 JSON 清洗的落地写法3.1 登录态从哪来Cookie 的保存、读取与失效判断Boss直聘的职位搜索接口是带登录态鉴权的。最常见的做法是先打开浏览器登录自己的账号然后通过开发者工具复制接口请求里的 Cookie写进配置文件。这样做的优点是几乎零代码成本缺点是 Cookie 有时效过几天可能就失效了。更好的做法是把登录流程也脚本化但 B 站的反爬校验码不是那么容易自动通过个人项目没必要在这上面较劲。手动复制 Cookie 后把获取时间写进配置文件比如COOKIE_EXPIRE_DAYS 3超过三天主动提醒自己重新登录。爬虫启动前先发一个轻量请求探测登录态比跑一半才发现全被重定向强得多。# config.py COOKIE 你的完整Cookie字符串 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36, Referer: https://www.zhipin.com/web/geek/job, Accept-Language: zh-CN,zh;q0.9, Cookie: COOKIE, } SEARCH_URL https://www.zhipin.com/wapi/zpgeek/search/joblist.json CITY_CODE 101010100 # 北京 KEYWORD Python PAGE_SIZE 15 DELAY_RANGE (3, 6) # 每次翻页随机延时范围这里把 Cookie 合并进了 HEADERS后续所有请求直接复用requests.Session的默认头。CITY_CODE是 Boss直聘的城市编码北京是101010100上海是101020100深圳是101280600这个编码在网页端搜索框切换城市时能从 URL 里拿到。DELAY_RANGE是随机的比固定 sleep 更接近真人操作节奏。3.2 joblist.json 的请求参数query、cityCode、pageSize 的关键坑Boss直聘网页端搜索职位时浏览器发的是joblist.json这个接口。请求参数以 GET 形式拼接核心参数是query、cityCode、page、pageSize。pageSize看起来可以调大实际上后端会限制单页条数填 100 也不会全返回反而可能触发风控判断。老老实实用默认的 15翻页次数多点没关系访问节奏要压得住。# spider.py import time import random import requests from config import HEADERS, SEARCH_URL, DELAY_RANGE session requests.Session() session.headers.update(HEADERS) def fetch_job_list(keyword, city_code, page): params { scene: 1, query: keyword, cityCode: city_code, page: page, pageSize: 15, degree: 0, } resp session.get(SEARCH_URL, paramsparams, timeout10) data resp.json() # 关键防御zpData 可能为 None比如验证码页面或登录失效 zp_data data.get(zpData) or {} job_list zp_data.get(jobList) or [] return job_list def crawl_all_pages(keyword, city_code, max_page30): all_jobs [] for page in range(1, max_page 1): jobs fetch_job_list(keyword, city_code, page) if not jobs: break all_jobs.extend(jobs) time.sleep(random.uniform(*DELAY_RANGE)) return all_jobs这段代码的逻辑要重点看两处。第一处是data.get(zpData) or {}Boss直聘的正常响应是{ code: 1, zpData: { jobList: [...] } }但当你触发风控或者 Cookie 失效时接口也会返回 200HTTP 状态码正常JSON 里却可能没有zpData或者jobList为空。如果用裸的data[zpData][jobList]去取程序直接抛KeyError或者TypeError整轮爬取就中断了。第二处是循环里的resources判断遇到空页直接break避免多爬无意义的空请求。3.3 数据清洗把“20-40K·15薪”和“面议”清洗成可聚合的字段搜索接口返回的原始薪资字段长这样20-40K·15薪、30-60K·16薪、面议。如果直接存字符串后续 SQL 里没法比较、没法排序、没法求平均。清洗的目标是把字符串拆成四个可运算的数字字段同时保留原始文本用于追溯。# clean.py import re def parse_salary(salary_raw): 解析薪资字符串返回 (min, max, avg, months) if not salary_raw or salary_raw 面议: return 0, 0, 0, 12 match_range re.search(r(\d)-(\d)K, salary_raw) match_months re.search(r(\d)薪, salary_raw) if match_range: low int(match_range.group(1)) high int(match_range.group(2)) avg (low high) // 2 else: low high avg 0 months int(match_months.group(1)) if match_months else 12 return low, high, avg, months这个函数处理了两种情况匹配不到数字范围的文本统一归零避免面议这种值干扰聚合匹配不到月薪倍数的默认按 12 薪算因为大部分岗位没有标注就按普通月薪理解。清洗逻辑单独放一个文件不要在爬虫里顺手处理。原因是爬虫函数追求的是“快速抓取并入库”而清洗是“慢工出细活”两个职责混在一起时你改一行清洗正则往往会误伤已经跑通的爬取逻辑。除薪资外还有一个字段需要注意Boss直聘的jobList里职位详情字段并不全技能标签这种长文本通常在单独的详情接口里。常见做法是先用搜索接口拿到jobId和jobHref再对每条记录发详情请求补充字段。个人项目可以直接跳过详情采集技能标签留空也不影响薪资和学历维度的分析如果一定要采集记得对每条详情也做延时控制这个逻辑在detail.py里单独维护。4. 从 MySQL 到可视化大屏数据分析 SQL 与 ECharts 落地4.1 分析口径先定好薪资取中间值面议剔除时间窗统一拿到库里的数据直接画图是数据分析项目最容易翻车的地方。数据口径不一致图表数字对不上连你自己都不知道该信哪个。我这条链路里定了三条硬规则。第一薪资分析只看salary_avg 0的记录面议的岗位在薪资相关图表中剔除但保留在岗位数量统计里。第二所有聚合查询统一加时间窗条件created_at NOW() - INTERVAL 7 DAY避免大盘混入一个月前的过期岗位。第三平均值用AVG(salary_avg)而非AVG(salary_max)因为招聘方标的上限往往虚高中间值更接近真实谈判空间。-- 按学历要求统计岗位数量与平均薪资 SELECT education_required, COUNT(*) AS job_cnt, ROUND(AVG(salary_avg)) AS avg_salary FROM job_info WHERE salary_avg 0 AND created_at NOW() - INTERVAL 7 DAY GROUP BY education_required ORDER BY job_cnt DESC;这个 SQL 输出的表直接就能喂给 ECharts 饼图和柱状图。注意ROUND()的使用薪资平均值带小数在可视化大屏上很难看四舍五入到整数位更符合看板习惯。GROUP BY的字段是清洗过的枚举值比如“本科”“大专”“硕士”数据入库前已经在清洗层处理过SQL 里不需要再做字符串替换。4.2 聚合 SQL学历分布、经验要求、公司规模、城市对比分析看板需要支撑四类核心指标岗位供给量、薪资水平、经验要求分布、行业与公司规模分布。每一类都对应一个 SQL 查询统一封装在analysis.py里。# analysis.py QUERIES { education: SELECT education_required AS name, COUNT(*) AS value FROM job_info WHERE created_at NOW() - INTERVAL 7 DAY GROUP BY education_required , experience: SELECT experience_required AS name, COUNT(*) AS value FROM job_info WHERE created_at NOW() - INTERVAL 7 DAY GROUP BY experience_required , company_size: SELECT company_size AS name, COUNT(*) AS value FROM job_info WHERE created_at NOW() - INTERVAL 7 DAY GROUP BY company_size , industry_top: SELECT industry AS name, COUNT(*) AS value FROM job_info WHERE created_at NOW() - INTERVAL 7 DAY GROUP BY industry ORDER BY value DESC LIMIT 10 , }这里的设计意图是把查询集中管理Flask 接口层做的是按名字取 SQL 然后执行不需要每个接口写一遍 SQL。name和value的统一命名是为了让前端 ECharts 的数据格式完全一致饼图和柱状图共用同一套数据结构前端代码量大幅减少。4.3 Flask 提供数据接口ECharts 渲染大屏可视化部分我选了 Flask 做数据服务页面端直接用 ECharts 的 CDN 引入。Flask 在这里的角色很轻——从 MySQL 取数并转成 JSON 返回不涉及页面渲染和模板逻辑。这样做的优点是大屏页面可以做成静态 HTML后续要换模板引擎也不动后端。# app.py from flask import Flask, jsonify, render_template from db import query_all app Flask(__name__) app.route(/api/education) def api_education(): rows query_all(QUERIES[education]) return jsonify(rows) app.route(/api/experience) def api_experience(): rows query_all(QUERIES[experience]) return jsonify(rows) app.route(/) def dashboard(): return render_template(dashboard.html)前端拿数据只需要一段fetch然后setOption// dashboard.html 内联脚本 fetch(/api/education) .then(r r.json()) .then(rows { const chart echarts.init(document.getElementById(eduChart)); chart.setOption({ tooltip: { trigger: item }, series: [{ type: pie, data: rows.map(r ({ name: r.name, value: r.value })), label: { formatter: {b}: {c} } }] }); });ECharts 的图表类型选择和数据结构匹配是最容易踩的一个点。饼图要求data是{ name, value }数组柱状图要求xAxis.data和series.data分开两组字段名不一致时前端处理逻辑就得写两份。我在 SQL 层统一返回name和value前端一个map函数通吃这才是后端为前端妥协的正确姿势。大屏页面背景色用深色系backgroundColor: #0f1c2e图表的轴线、文字颜色整体调亮视觉上更像一个正经的可视化大屏。5. 避坑Boss直聘爬虫的 5 个典型翻车现场5.1 请求频繁触发风控验证码现象爬虫跑到第 30 页左右突然某次请求返回的 JSON 里zpData为None浏览器访问同样的页面开始出现滑块验证码。原因请求频率过高是最常见原因。即便单线程爬取若延时固定在 1 秒以内短时间几十次请求足够触发服务端的频率风控。解决把随机延时区间拉大到DELAY_RANGE (3, 6)也就是每次翻页间隔 3 到 6 秒随机。如果是初次跑这个接口延时低一点问题不大一旦触发过验证码后续 Cookie 即使重新复制风控标记也可能还在。这时候直接换一个网络出口再登录取新 Cookie比继续硬试体感好很多。5.2 登录态静默失效现象爬虫没有报任何异常日志里显示请求都成功了但入库条数为 0。手动打开接口地址看到的是“请先登录”。原因Boss直聘的登录态是服务端可主动失效的。同一账号在多个 IP 登录、长时间不活跃、或者异地登录都会触发重新认证。页面会正常跳转但接口层面返回的数据结构已经变了。解决爬虫启动时加一个登录态探测请求一个轻量接口看返回结构是否符合预期。不符合就直接日志告警并退出不要继续跑空请求。日志里记录当前 Cookie 的有效日期到期前主动提醒。5.3 单 IP 被限流返回 403现象某一天开始所有请求都返回 HTTP 403换一个城市编码也没用浏览器却能正常访问。原因单 IP 的短时间请求量超过了服务端的 IP 级限流阈值。注意这种封禁和登录态无关换 Cookie 也救不回来。解决最直接的办法是降低采集频率把每次请求的延时间隔提升到 10 秒以上每天的总请求量控制在几百次级别。个人数据分析项目完全够用。大规模采集需要分布式代理池支持但那是另一个量级的工程对岗位行情分析来说数据量过千条后边际价值已经很低了。5.4 接口 JSON 字段说变就变现象昨天跑得好好的今天突然报KeyError: jobList检查接口响应后发现字段名从jobList变成了job_list或被移到新的层级。原因招聘网站前端改版频率高接口字段结构和名称会随版本调整。这类变更没有通知只有程序跑挂了你才知道。解决代码里所有字段提取都用.get()方法加默认值解析逻辑集中在少数几个函数里不要散落在各处。字段变化时打开浏览器开发者工具看一遍接口返回改一个函数就能恢复而不是满文件搜。这也是之前强调的“清洗逻辑独立成文件”的另一个好处。5.5 薪资清洗把“面议”算成了 0现象薪资分析图里突然出现一大片 0 值拉低平均薪资图表数据明显失真。原因清洗函数里没用正则直接匹配(\d)-(\d)K遇到面议时返回None入库前没做空值兜底结果把None转化成了0存进salary_avg。解决清洗函数里对所有解析不到结果的情况统一返回0, 0, 0, 12同时 SQL 聚合时固定用WHERE salary_avg 0过滤。两条防线缺一不可——清洗层的兜底保证数据不报错查询层的过滤保证分析不污染。还有一条血泪经验薪资字符串里的·15薪中间的圆点可能是全角字符正则\d薪不受影响但如果你用split(·)做切分就会踩坑能用正则的地方尽量正则。6. 让系统长期自己跑增量更新、定时任务与数据验证6.1 增量爬取与旧数据失效标记爬虫做成一锤子买卖没意义岗位市场每天都在变看板要跟着更新。增量更新的核心是利用job_id唯一键重复的职位用INSERT ... ON DUPLICATE KEY UPDATE更新薪资和发布时间已消失的职位靠对比最近两天的数据做失效标记。我在库里加一个is_active TINYINT DEFAULT 1字段每天全量爬完后把没出现在当天结果里的旧记录置为 0查询时默认过滤这样既不删历史也能反映真实供给变化。6.2 crontab 定时调度与运行日志凌晨两点是采集的黄金时间段网站访问压力小风控相对宽松。Linux 下用 crontab 做定时任务一条命令解决。0 2 * * * cd /opt/boss_spider /usr/bin/python3 spider.py logs/spider.log 21重点看日志输出的写法标准输出和错误输出都追加到同一个日志文件出问题时不用翻终端直接tail -100 logs/spider.log就能定位。日志里每跑完一页打印一条带页码和条数的记录每天起床瞄一眼日志就知道夜里跑没跑成功。6.3 验证拿可视化结果和官网页面随机比对系统跑通后验证数据可信度比继续加图表更重要。我自己常用的验证方式是每次采集完跑一条抽样 SQL看今天新入库的岗位的薪资分布是否符合直觉比如 Python 岗位平均薪资跳到 60K 以上那大概率是清洗逻辑出了问题直接去官网抽查几条原始数据对比。还有一种做法是保留第一天的全量快照表每天跑完用一条 SQL 对比两个时间点的岗位总量和平均薪资看波动是否合理。这套系统的最终价值不在代码量而在你能不能持续从数据里发现变化某天某一线城市的 Python 岗位供给突然多了某类公司规模的薪资中位数在下移这些信号才是你每天打开看板的理由。我自己早期做爬虫时只追求速度把并发开到十几个线程结果还没爬到数据先被风控盯上连本带利赔进去好几个账号。从那以后我所有爬虫项目都把“访问节奏”当第一设计约束先限速后写解析再谈效率。这套 Boss直聘的采集项目也不例外宁可慢不可乱。希望帮到你。本文还有配套的精品资源点击获取