Python岗位数据采集与分析可视化:从接口抓包到定时跑批的完整实践

发布时间:2026/11/3 16:55:40
Python岗位数据采集与分析可视化:从接口抓包到定时跑批的完整实践 简介一套面向求职者、招聘者与数据分析学习者的Boss直聘岗位数据采集及分析可视化项目资源基于Scrapy框架爬取全国热门城市大数据、数据分析、数据挖掘、机器学习等岗位信息并提供源代码、数据文件与项目文档。资源压缩包共38个文件整体仅237KB包含13个Python脚本、12个JavaScript交互文件、4个XML配置以及CSV数据表、PNG图表、HTML页面和Markdown说明文档等结构清晰便于按模块学习复用。目前已有582人学习下载。借助这套资源读者可查看采集好的全国热门城市岗位数据复用爬虫与数据清洗脚本并结合前端可视化代码搭建岗位薪资、学历要求、技能需求的分析看板项目文档同步提供运行指导与实现思路适合希望掌握数据采集到分析展示全流程的入门及进阶开发者。1. 基于Python做Boss直聘岗位数据采集与分析可视化先把整个项目拆成能落地的三块如果你做过招聘数据分析一定遇到过这种尴尬想统计某座城市Python岗位的真实薪资区间、学历门槛和技能要求却发现Boss直聘网页版只能一页一页手动翻既不能批量导出也没法按条件把几千条数据一次性拉下来。这时候用Python做岗位数据采集与分析可视化就是一条非常务实的路子。这个项目的本质是三条线采集层用requests/selenium维持登录态并抓取搜索接口存储层把JSON清洗成结构化数据分析层再对薪资、城市、技能做聚合统计和图表展示。适合的人群很明确想验证某个城市岗位供需的求职者、做行业调研的产品运营以及刚入门Python爬虫和数据可视化、需要一份完整可复现项目来练手的开发者。下面从接口设计讲到落地代码把坑和参数一次说清。2. Boss直聘采集方案设计为什么不能直接requests GET然后碰运气2.1 数据从哪来先抓包看清接口再谈怎么写代码做这个项目的第一个动作不是写代码而是打开浏览器开发者工具手动搜索一次“Python”岗位观察网络面板里到底发了哪些请求。Boss直聘的网页版搜索结果是异步渲染的HTML源码里根本没有岗位数据真正的数据藏在XHR请求里。常见做法是找到一个形如zhipin.com/wapi/zpgeek/search/joblist.json的接口GET参数带query、city、page、limit返回体是标准JSON岗位列表在zpData.jobList字段下。我一般会先用curl或者Postman把返回体整个看一遍确认字段名再动代码。一个最小请求长这样import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://www.zhipin.com/web/geek/job?querypythoncity100010000 } url https://www.zhipin.com/wapi/zpgeek/search/joblist.json params { query: python, city: 100010000, page: 1, limit: 15 } resp requests.get(url, headersheaders, paramsparams, timeout10) print(resp.status_code) print(resp.json().keys())这里最关键的是参数city它是一串城市编码比如北京100010000、上海100020000不是城市拼音。如果不带Referer部分请求头校验严格的接口会直接拒绝服务。这个脚本第一次跑大概率不会成功因为登录态还没带上。2.2 登录态与Cookie为什么第一次请求就进了“安全验证”如果你刚打开过Boss直聘首页通过扫码登录了账号再直接跑上面的代码很可能收到一个302跳转到登录页或者JSON里code不等于0消息写着“登录已过期”。原因是搜索接口要求请求携带有效的登录Cookie而requests本身不维护任何浏览器状态。常见做法是用requests.session()维持会话并手动把浏览器里登录后的Cookie塞进去。Cookie可以从开发者工具Network面板任意一个请求的Request Headers里复制以Cookie字段的完整文本粘贴到本地配置文件不要硬编码在源码里import requests import json with open(cookie.json, r, encodingutf-8) as f: cookie_str json.load(f)[cookie] session requests.session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://www.zhipin.com/web/geek/job?querypython }) # 逐行解包Cookie转成requests的CookieJar for item in cookie_str.split(;): item item.strip() if not item: continue if in item: key, value item.split(, 1) session.cookies.set(key, value)这里有个取舍为什么不直接headers[Cookie] cookie_str因为requests里手动构造Cookie字符串在重定向时容易出现丢失用session.cookies.set逐条写入更稳。Cookie的有效期通常是一周到一个月不等过期后重新扫码一次更新本地文件即可这也是项目文档里必须写清楚的维护项。2.3 反爬边界与合规用量一份个人分析项目的数据摄入量该控制在多少实现了登录态以后你可能会顺手把page从1试到100。这里要泼一盆冷水Boss直聘对请求频率有比较明显的风控连续高频翻页会触发滑块验证严重时账号会被限制搜索。个人的经验是这个项目定位应该是“低频个人分析”不要追求把全站岗位都抓下来。我通常把请求间隔控制在3到5秒之间单次任务最多抓10页、约150条数据一次跑5到8个关键词总量控制在1000条以内。频率上每天跑一次选在低峰时段。你可能会觉得这个量太少但做薪资分布和技能词云分析1000条样本已经足够看出趋势再多反而容易被风控盯上。控制频率的实现很简单在翻页循环里加随机等待即可import time import random time.sleep(random.uniform(3, 5))注意这里的间隔是“每次请求前”等待不是“每页等待”。如果加上解析时间一轮下来自然节奏更慢也更接近真人浏览的间隔。3. 登录态模拟与岗位搜索把“搜索Python岗位”跑通并拿到结构化数据3.1 用requests维持会话最小可用的登录态装配上一章的session装配已经解决了登录问题这里把它封装成一个函数方便后续所有请求复用。一个比较顺手的方式是单独建一个session_manager.py对外只暴露一个已经带好Cookie和Header的session对象。这样采集脚本里不用反复复制那串Cookie解析代码。# session_manager.py import requests import json def load_cookie(pathcookie.json): with open(path, r, encodingutf-8) as f: return json.load(f)[cookie] def build_session(cookie_pathcookie.json) - requests.Session: session requests.session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: application/json, text/plain, */*, Referer: https://www.zhipin.com/web/geek/job, }) cookie_str load_cookie(cookie_path) for item in cookie_str.split(;): item item.strip() if in item: key, value item.split(, 1) session.cookies.set(key, value) return session if __name__ __main__: s build_session() # 拿搜索接口做一次探测 url https://www.zhipin.com/wapi/zpgeek/search/joblist.json resp s.get(url, params{query: python, city: 100010000, page: 1, limit: 15}, timeout10) print(resp.status_code, resp.text[:200])这段代码里的build_session返回的是带着完整登录态的对象探测请求能直接打到接口层。一个值得注意的参数是Accept如果接口返回的是JSON这个头能避免部分情况下服务端返回HTML错误页。如果你在浏览器里复制Cookie时还看到了__zp_seo_uuid__这类特殊字段一并写进Cookie文件即可requests不会挑字段。3.2 解析岗位列表jobList里的三层嵌套与字段映射拿到搜索接口的返回体之后下一步就是把JSON里的岗位数据解析出来。jobList是一个数组每个元素是一个岗位对象常见字段包括jobId、jobName、salaryDesc、brandName、city、areaDistrict、businessDistrict、jobExperience、jobDegree和skills。其中salaryDesc是“10k-15k·13薪”这样的文本需要后期清洗。import json import pprint def parse_joblist(resp_json: dict): zp_data resp_json.get(zpData, {}) job_list zp_data.get(jobList, []) parsed [] for job in job_list: item { job_id: job.get(jobId), job_name: job.get(jobName), salary_desc: job.get(salaryDesc), brand: job.get(brandName), city: job.get(city), area: job.get(areaDistrict), biz_district: job.get(businessDistrict), experience: job.get(jobExperience), degree: job.get(jobDegree), skills: ,.join(job.get(skills, []) or []), link: job.get(jobDetail, {}).get(link) if job.get(jobDetail) else None, } parsed.append(item) return parsed # 调用示例 resp_json json.loads(resp.text) rows parse_joblist(resp_json) pprint.pprint(rows[:2])这里的jobId是整个项目最重要的字段后续去重和数据库主键都靠它。skills是一个列表在采集阶段先转成逗号分隔字符串存起来后面做词云时再拆开聚合。注意部分岗位对象没有jobDetail所以取link时要加个if job.get(jobDetail)保护否则会直接抛KeyError。这种字段缺失在实际数据里很常见解析时不能假设每个字段都存在。3.3 翻页循环与请求间隔把“搜1000条”变成可复现的采集任务单页解析跑通后写一个翻页循环把关键词和城市组合起来。一个完整的采集任务通常这样组织外层遍历关键词列表中层遍历城市列表内层遍历页码。参数page从1开始limit固定在15或30。import time import json import random import pandas as pd from session_manager import build_session SESSION build_session() def fetch_jobs(keyword: str, city_code: str, max_page: int 10): all_rows [] url https://www.zhipin.com/wapi/zpgeek/search/joblist.json for page in range(1, max_page 1): params { query: keyword, city: city_code, page: page, limit: 15 } try: resp SESSION.get(url, paramsparams, timeout10) if resp.status_code ! 200: print(f[跳过] {keyword} 第{page}页, HTTP {resp.status_code}) continue data resp.json() if data.get(code) ! 0: print(f[警告] {keyword} 第{page}页 code{data.get(code)}) break rows parse_joblist(data) if not rows: break all_rows.extend(rows) print(f[OK] {keyword} 第{page}页, 累计{len(all_rows)}条) except Exception as e: print(f[异常] {keyword} 第{page}页: {e}) time.sleep(10) time.sleep(random.uniform(3, 5)) return all_rows if __name__ __main__: result fetch_jobs(python, 100010000, max_page10) df pd.DataFrame(result) df.to_csv(raw_jobs.csv, indexFalse, encodingutf-8-sig)这里的code ! 0判断非常关键。接口返回code0才是正常数据返回其他值通常是登录态失效或触发了风控此时继续翻页没有意义应该跳出循环。我一般会在code非0后直接终止整个任务而不是只跳一页因为连续两页异常说明账号状态已经不可用了。CSV里的utf-8-sig是给Excel用户看的避免直接打开乱码。这段代码跑完之后你会得到一个原始岗位表接下来的活就是清洗。4. 数据清洗与落地存储把嵌套JSON整理成能直接做可视化的DataFrame4.1 把salaryDesc拆成数字区间正则处理“10k-15k·13薪”采集到的薪资字段长这样“10k-15k·13薪”、“8k-12k”、“面议”。做可视化时我们需要的是数字区间所以第一步是用正则从salaryDesc里提取下限和上限并算出中位数作为后续统计的基准值。这个环节看着简单实际上一碰数据就有各种写法比如“20-40K”“10k以上”“300-500元/天”等。import re def parse_salary(desc: str): if not desc or desc 面议: return None, None, None # 匹配数字字母k/k单位统一为k/月 pattern r(\d(?:\.\d)?)k?-(\d(?:\.\d)?)k match re.search(pattern, desc, flagsre.IGNORECASE) if match: low float(match.group(1)) high float(match.group(2)) return low, high, (low high) / 2 # 处理单值写法如 10k single re.search(r(\d(?:\.\d)?)k, desc, flagsre.IGNORECASE) if single: val float(single.group(1)) return val, val, val return None, None, None # 测试 for s in [10k-15k·13薪, 20-40K, 8k-12k, 面议, 300-500元/天]: print(s, -, parse_salary(s))正则(\d(?:\.\d)?)k?-(\d(?:\.\d)?)k里最需要注意的地方是第一个数字后面紧跟的k?是可选的因为部分岗位写的是“20-40K”而不是“20k-40k”直接把范围数字取出来即可。对于“300-500元/天”这种日薪岗位个人建议在采集阶段直接过滤掉或者单独标注不要和月薪混在一起算平均值否则出来的图表没有参考价值。4.2 城市与区域归一化为什么同一个城市会有三种写法Boss直聘接口里的city字段大多数是“北京”“上海”这种规范名但areaDistrict和businessDistrict偶尔会出现“朝阳区”“海淀区”和“CBD·国贸”“望京”这类混合写法。做城市维度的分析时city可以直接用但如果你要分析“杭州滨江区有多少Python岗”就需要对区域字段做一次清洗。def normalize_area(area: str): if not area: return 未知 # 去掉常见的后缀保留主区域名 area area.replace(区, ).strip() area re.sub(r[·|\-|/], _, area) return area这张清洗表不需要做得特别复杂把“区”字去掉把分隔符统一成下划线就够了。真正需要注意的反而是businessDistrict里的商圈名比如同一个“望京”有的岗位写“望京”有的写“望京·SOHO”分析时可以按前缀把商圈名归到街道这一级或者干脆不分析这个字段只看areaDistrict。4.3 落地到SQLite为什么我不用CSV直接做可视化采集脚本里用CSV做中间存储没问题但进入分析阶段我强烈建议换成SQLite。原因有三第一重复跑任务时CSV会累积重复行而SQLite可以给job_id建唯一约束第二SQLite支持SQL聚合后续按城市、关键词、经验字段做分组统计时一条SQL就能完成第三可视化工具直接读SQLite比读几千行CSV更干净。import sqlite3 import uuid def init_db(db_pathboss_jobs.db): conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, job_id TEXT UNIQUE, job_name TEXT, salary_desc TEXT, salary_low REAL, salary_high REAL, salary_mid REAL, brand TEXT, city TEXT, area TEXT, biz_district TEXT, experience TEXT, degree TEXT, skills TEXT, keyword TEXT, crawl_time TEXT ) ) conn.commit() return conn def batch_insert(conn, rows): sql INSERT OR IGNORE INTO jobs (job_id, job_name, salary_desc, salary_low, salary_high, salary_mid, brand, city, area, biz_district, experience, degree, skills, keyword, crawl_time) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) conn.executemany(sql, rows) conn.commit()INSERT OR IGNORE配合job_id UNIQUE约束是这里防重的核心。同一岗位在多次采集里都会出现用job_id做唯一键可以直接跳过重复记录这是CSV做不到的。crawl_time字段记录采集时间后续做时间维度分析时有用。如果同一天跑了两次任务第一次的1000条和第二次的1000条有600条重复最终库里只会有1400条而不是2000条数据可信度完全不一样。5. 避坑采集与可视化落地中的5个常见现场问题5.1 第一个请求就302跳转登录页现象运行采集脚本发现返回的resp.url已经变成了登录页地址而不是joblist.json状态码是200但内容是一段HTML。原因Cookie过期或没有正确写入session。最典型的错误是复制Cookie时把Cookie:这个前缀一起复制进了文件导致requests解析出名叫“Cookie:”的空字段。解决检查Cookie文件是否是纯JSON文本在build_session里给session加上一条探测请求输出resp.status_code和resp.url判断是否被重定向。我习惯在采集脚本里写一个check_login()函数跑任务前先探一次省得翻到第三页才发现登录态已经失效。5.2 翻到第三页突然出现滑块验证现象前两页正常到第三页开始接口返回的HTML里出现“安全验证”字样或者直接返回code4000之类非0错误。原因请求频率超过风控阈值不换IP的前提下只能降低速度。这个“阈值”并没有公开标准与账号历史行为有关但连续快速翻页一定是触发条件之一。解决把请求间隔从3至5秒放宽到8到12秒同时单次任务页数限制在10页以内。如果已经被限制了停掉任务半小时后再跑。这里我不会推荐换IP或者多账号绕过的方案低频、少量、个人使用是这类项目能持续跑下去的边界。5.3 薪资正则把“10k-15k·14薪”的薪数当成薪资取出来现象解析结果里出现salary_high15k但salary_mid29k这种明显不对的值。原因正则没有做边界约束部分写法里“14薪”中的“14”被当成了薪资范围的第二个数。解决上面的parse_salary函数里用了(\d(?:\.\d)?)k?-(\d(?:\.\d)?)k第二个数字后面必须紧跟k所以“14薪”不会匹配上。如果你拿到的数据里有“10k-15k·13薪”这种格式正则末尾的k正好拦住这个问题。优先用我前面给的解析函数不要自己随手写r(\d)-(\d)这种必须翻车。5.4 matplotlib画图时中文全部变成方块现象用matplotlib画薪资分布图标题和坐标轴上的中文全部显示为豆腐块。原因matplotlib默认字体不支持中文需要手动指定中文字体。解决在绘图脚本最前面加上字体设置并且要确认字体实际存在import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False这里第二行axes.unicode_minus是为了让坐标轴上的负号正常显示很多人在设完字体后漏了这行结果数字轴上的负号变成方块。5.5 SQLite里数据重复翻倍现象跑了两次任务后SELECT COUNT(*)显示的数量是第一次加第二次的总和而不是去重后的值。原因建表时没有写job_id UNIQUE或者插入用的是INSERT INTO而不是INSERT OR IGNORE。解决参考第4章的建表语句给job_id加UNIQUE约束插入语句改成INSERT OR IGNORE。如果已经建了错表直接删除数据库文件重新采集最快没必要写迁移脚本。6. 进阶把“能跑”变成“可信”——定时任务与抽样自检项目跑通之后下一步我建议做三件事给采集脚本加一个定时任务让数据每周自动更新一次加一段抽样自检逻辑确认库里的数据量和字段分布是合理的最后把可视化图表从“能出图”升级成“固定模板每周出一份”。定时任务在Windows上用计划任务在Linux上用crontab但两者的前提都是采集脚本本身能无人值守运行。我的习惯是把清洗和入库也纳入采集脚本保证一次执行完整个链路。一个简单的定时入口可以这样组织# run_weekly.py import subprocess import sqlite3 import datetime today datetime.date.today().isoformat() # 依次执行采集 - 清洗入库 - 生成报告 subprocess.run([python, fetch_jobs.py], checkTrue) subprocess.run([python, analyze_report.py, --date, today], checkTrue) print(f[完成] {today} 的数据已更新)自检逻辑里我一般会查三件事一是当天新增记录数如果为0说明登录态可能过期了二是每个关键词的岗位数量是否在合理范围如果某个关键词骤降到个位数多半是风控拦截了部分页三是薪资中位数的分布是否符合常识比如“北京Python岗中位数低于5k”这种结果大概率是数据清洗出了问题。自检通过后用matplotlib或pyecharts生成固定模板的图表包含薪资中位数Top10城市、岗位需求趋势、技能词云三个图。最终产物是一份带时间戳的HTML报告既方便自己回溯也能直接分享给团队。这套流程稳定运行两个月后你会慢慢积累出真正有参考价值的岗位数据而不是每次需要分析时临时抓一次拿到一份来源不明、质量不一的脏数据。希望这个从接口抓包到定时跑批的落地路径能帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询