
简介一份基于 Python 的 Boss 直聘数据可视化分析系统源码面向正在准备期末大作业、毕业设计或想提升数据综合能力的高校学生解决招聘岗位数据从爬取、清洗到分析、展示的完整链路问题。压缩包仅 426KB共 26 个文件以 12 个 HTML 图表页面、2 个 Jupyter Notebook、4 个 CSV 数据集和爬虫.py 为主配套 .gitignore、.iml 等工程文件目录结构清晰能快速定位脚本、数据与图表。目前已有 787 人学习下载。内含岗位爬虫脚本、岗位与公司 CSV 原始数据、数据清洗和关联分析 Notebook以及学历要求、经验要求、招聘城市、工资待遇、大厂招聘比例等多维度可视化 HTML 页面覆盖从数据采集到结论呈现的全流程项目经过严格调试评审得分 95 分以上可直接运行适合期末大作业参考也能作为 Python 数据分析综合项目演练。1. 基于python的boss直聘数据可视化分析系统到底能做什么基于python的boss直聘数据可视化分析系统听起来像是个爬虫项目真正卡人的地方反倒在数据底座和可视化表达。它做的事是一条完整链路用python采集boss直聘上的岗位数据存进本地数据库用pyecharts把薪资、学历、经验、城市画成能讲出结论的图最后用flask串成一个答辩现场能打开的演示页面。它解决的是期末作业里最常见的两类问题——只有代码没有结果图或者有图却讲不出数据含义。适合python课设、数据可视化课设以及想拿真实数据练手的入门学习者。只想练爬虫的不必做可视化只想做可视化的又缺一份能自圆其说的数据来源这个标题恰好把两边接上。2. 数据底盘这么做字段清单、SQLite 存储与混合采集方案采集代码可以往后放字段和存储方案要先定。原因很简单可视化阶段所有图表的观感都取决于字段设计。字段抓少了后期补采要重新登录、重新翻页成本比一开始把数据字典列全高得多。我一般先把字段清单和数据格式定死再决定用什么库、怎么采集。这个顺序不要反过来否则返工的时候会发现清洗代码、建表语句、图表脚本全要跟着改。2.1 boss直聘岗位数据里值得入库的十个字段boss直聘网页版把岗位数据以内嵌JSON的形式放在页面源码里抓取前先明确每个字段将来用在哪个图里。字段建得杂没关系但每个字段都得有去处。建议优先入库下面这一组字段名统一用小写下划线方便pandas直接操作。存库字段含义可视化用途job_id岗位唯一ID去重、断点续采job_name岗位名称查看招聘方向细分salary薪资原始字符串解析成低/高/中位数值city城市名城市岗位量与薪资对比experience经验要求经验门槛分布degree学历要求学历门槛分布brand_name公司名称头部雇主统计industry公司行业行业分布skill_tags技能标签列表词云、技能要求统计pub_time发布时间过滤过期岗位字段选定之后心里要有一条完整的分析链job_name告诉你市场在往哪个方向招人salary告诉你行情区间degree和experience告诉你门槛industry和skill_tags告诉你技能方向。每抓一个字段都是往这条链上补一块。反过来抓到一堆没想好怎么用的字段最后只会堆出一面没法讲解的图表墙。这里多说一句网上流传的各种“boss直聘字段表”版本很旧页面改版后很多字段对不上。判断标准只有一个——以你自己浏览器里返回的JSON为准。打开开发者工具看一眼真实结构比抄任何模板都靠谱。2.2 为什么选SQLite建表语句与两条存储纪律期末作业的数据量在几百到几千条SQLite一个单文件就能装下pandas可以直接读写不需要装MySQL也不用处理端口占用、root密码过期这些环境问题。只有两种情况我才会换MySQL多人协作写同一个项目或者数据量到了几十万条。期末作业基本碰不到。import sqlite3 from pathlib import Path db_path Path(jobs.db) conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS job_post ( job_id TEXT PRIMARY KEY, job_name TEXT, salary TEXT, city TEXT, experience TEXT, degree TEXT, brand_name TEXT, industry TEXT, skill_tags TEXT, pub_time TEXT, crawl_date TEXT ) ) conn.commit() conn.close()两条存储纪律必须遵守。第一salary字段存原始字符串解析出的数值列放另外的字段。解析规则可能写错原始串还在就能重新解析不用重新爬。第二skill_tags是列表序列化成JSON字符串再入库可视化时loads回来即可。直接存成python列表会得到一行带方括号的文本后期SQL查询会很难受。crawl_date字段记录采集日期后面想验证“不同时间段薪资有没有变化”时直接group by就能查。2.3 混合采集方案为什么纯接口方案对新手不友好boss直聘网页版对未登录的requests请求基本返回登录跳转页接口参数里还混着加密token纯接口方式对新手不友好。常见做法是混合采集先用playwright弹出一个真实浏览器扫码登录一次把cookie落盘随后把cookie喂给requests批量抓列表页。这样兼顾了稳定性和速度周末跑一两个晚上数据就够了。采集规模要提前控制。三五个关键词、两三个城市、每个3到5页就能拿到几百到上千条数据出图足够。页码从1开始每页10条翻到20页以后重复率上升频率稍高还会触发风控。对期末作业来说数据量不是越大越好能讲清楚一套分析链更重要。项目目录建议固定成下面这个结构后面所有脚本按这个约定落位boss_job_analysis/ ├── login_get_cookie.py # 登录一次保存 cookie ├── crawler.py # 列表页采集 ├── clean.py # 清洗与入库 ├── charts.py # pyecharts 出图 ├── app.py # flask 演示页 ├── jobs.db # 数据文件 ├── zp_cookies.json # cookie 文件 ├── charts/ # 图表输出目录 └── templates/ # flask 模板目录3. 从爬虫到入库建立岗位数据集的完整链路字段、存储和采集方案定了之后剩下的就是按步骤把数据从页面搬到SQLite。下面这四步是完整链路登录拿cookie、抓列表页、清洗、入库。每一步的坑我都标在代码后面。第一次跑通的时候不要把期望放在一次成功上页面结构、字段名、cookie有效期都可能是变量debug的思路比代码本身更重要。3.1 用playwright登录一次把cookie落盘# login_get_cookie.py import json from playwright.sync_api import sync_playwright def save_cookie(): with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context( user_agent( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 ) ) page context.new_page() page.goto( https://www.zhipin.com/web/geek/job, wait_untildomcontentloaded, ) input(请在弹出的浏览器里扫码登录完成后回到终端按回车...) cookies context.cookies() with open(zp_cookies.json, w, encodingutf-8) as f: json.dump(cookies, f, ensure_asciiFalse) browser.close() if __name__ __main__: save_cookie()playwright需要先pip install playwright再执行playwright install chromium。headlessFalse不能省headless模式下看不到窗口也没法在手机上确认登录。input()是暂停点作用是把脚本卡在登录完成之后、cookie还没失效之前扫码完成回终端按回车再把cookie写盘。cookie落盘格式保持playwright列表原样每项含name、value、domain、expires。requests只需要name和value但保留完整结构便于排查过期时间。cookie有效期通常几小时到几天不要提前很久存着等答辩用现场失效会很尴尬。3.2 用requests批量抓列表页解析内嵌JSON# crawler.py import json import random import re import time import requests def load_cookie(pathzp_cookies.json): with open(path, r, encodingutf-8) as f: cookie_list json.load(f) # playwright 导出的列表转成 requests 需要的 dict return {c[name]: c[value] for c in cookie_list} def fetch_job_list(keywordpython, city_code101010100, pages5): cookie_dict load_cookie() headers { User-Agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 ), Referer: https://www.zhipin.com/web/geek/job, Accept-Language: zh-CN,zh;q0.9, } all_jobs [] for page in range(1, pages 1): url https://www.zhipin.com/web/geek/job params {query: keyword, city: city_code, page: page} resp requests.get( url, paramsparams, headersheaders, cookiescookie_dict, timeout15, ) resp.raise_for_status() # 首屏数据内嵌在 script 标签里 match re.search( rscriptwindow\.__INITIAL_STATE__(.*?)/script, resp.text, re.S, ) if not match: print(f第 {page} 页没解析到 INITIAL_STATE可能被跳转或风控) break state json.loads(match.group(1)) job_list state.get(jobList) or state.get(jobCardList) or [] all_jobs.extend(job_list) print(f第 {page} 页抓到 {len(job_list)} 条累计 {len(all_jobs)} 条) time.sleep(random.uniform(3, 8)) return all_jobsparams传中文关键词时让requests自己处理URL编码不要手工拼URL。手工拼出来的中文URL经常被服务端判定为非法请求。分页从page1开始每页10条pages控制在5以内。city_code常见值北京101010100上海101020100深圳101280600其他城市打开搜索页看URL里的city参数以实际为准。匹配INITIAL_STATE用re.search不用等页面渲染完成这是混合采集比纯selenium快的原因。每页抓完sleep 3到8秒随机固定间隔更容易被识别成脚本行为。sleep低于3秒翻到第3页很容易触发验证码。state里优先取jobList取不到就试jobCardList这是给页面结构变化留的兼容。两边都没有就break别继续翻页浪费请求。3.3 数据清洗薪资区间转数值、学历经验归一# clean.py import re import pandas as pd def split_salary(s): # 输入形如20-40K、20-40K·15薪、面议 if not isinstance(s, str): return None, None, None s s.strip() if s in (面议, —, ): return None, None, None m re.match(r(\d(?:\.\d)?)\s*[-~—]\s*(\d(?:\.\d)?), s) if not m: return None, None, None low float(m.group(1)) high float(m.group(2)) return low, high, (low high) / 2 def clean_jobs(raw_jobs): df pd.DataFrame(raw_jobs) df[[salary_low, salary_high, salary_mid]] df[salary].apply( lambda x: pd.Series(split_salary(x)) ) df df.dropna(subset[job_id]).drop_duplicates(subset[job_id]) # 学历、经验做顺序化处理后面图表排序用 df[degree_std] df[degree].fillna(其他) df[experience_std] df[experience].fillna(不限) return df薪资必须用正则解析不能replace(K,)之后split(-)。“20-40K·15薪”这类带薪资结构的字符串会让float(15薪)直接报错。正则只取数字区间不匹配就返回None让整行薪资变成缺失值而不是中断整个采集流程。面议统一返回None导入SQLite时表现为NULL可视化阶段过滤掉。这里不能填0否则薪资分布图左端会多出一根异常柱子答辩时很难解释。job_id做drop_duplicates是去重底线。页面里对应字段通常是加密字符串如果缺失就用job_name加city加brand_name拼一个业务主键。学历和经验字段先做标准化学历里“学历不限”改成“不限”经验里“经验不限”改成“不限”否则图表图例会出现同一含义的两种叫法。3.4 入库与断点续采只插入不存在的job_id# save.py import sqlite3 import pandas as pd def save_to_sqlite(df, db_pathjobs.db): conn sqlite3.connect(db_path) existing pd.read_sql(SELECT job_id FROM job_post, conn) new_df df[~df[job_id].isin(existing[job_id])] new_df.to_sql(job_post, conn, if_existsappend, indexFalse) conn.close() print(f新增 {len(new_df)} 条跳过 {len(df) - len(new_df)} 条已存在)先查已存在的job_id集合再插入这是断点续采的关键。采集中断后重新运行不会产生重复行。to_sql的if_existsappend表示表的创建交给2.2的建表语句后续只追加。不要在每次运行时先drop表再重建否则断点续采就没有意义。跑完这一步jobs.db里就是一份可查询、可反复使用的岗位数据集后续所有可视化都从这张表读。4. 用pyecharts把数据变成能回答问题的四张图数据入库只是第一步可视化才是期末作业的得分点。这里说的可视化不是把图堆起来而是每一张图都对应一个明确的业务问题。答辩时最怕的状态是图很多但每张都说不清反过来带着问题选图每一张都能当证据用。我列了四个问题薪资给到多少学历卡在哪一档经验要求集不集中城市之间差多少下面四张图分别回答这些问题。4.1 画图之前先写一份问题清单很多作业翻车不是爬虫没写对而是图表的分析目标不明确。拿到数据第一件事不是render而是把结论假设写在纸上。比如“我猜测python岗位大多在北京薪资中位数在20到30K之间学历以本科为主”然后画图去验证。这样每张图都有一个可以讲出来的结论老师问起来你直接说这张图证明了什么和对着图现场编原因完全是两个分数。这个问题清单不需要写进代码写进答辩讲稿就行但它的价值比多画五张图更高。4.2 薪资分布把20-40K变成可统计的数值再分桶# charts_salary.py import sqlite3 import pandas as pd from pyecharts import options as opts from pyecharts.charts import Bar def load_data(db_pathjobs.db): conn sqlite3.connect(db_path) df pd.read_sql(SELECT * FROM job_post, conn) conn.close() df df[df[salary_mid].notna()] return df def salary_dist(df): bins [0, 10, 15, 20, 25, 30, 40, 50, 100] labels [ 0-10K, 10-15K, 15-20K, 20-25K, 25-30K, 30-40K, 40-50K, 50K以上, ] df[salary_bin] pd.cut(df[salary_mid], binsbins, labelslabels, rightFalse) dist df[salary_bin].value_counts().sort_index() bar ( Bar() .add_xaxis(dist.index.tolist()) .add_yaxis(岗位数, dist.values.tolist()) .set_global_opts( title_optsopts.TitleOpts(titlepython岗位薪资分布), xaxis_optsopts.AxisOpts(name薪资区间), yaxis_optsopts.AxisOpts(name岗位数), ) ) bar.render(charts/salary_dist.html)bins不是随手定的。0到10K一档容纳实习和初级岗10到30K之间每5K一档体现20-25K和25-30K这种关键差异50K以上合并因为高薪样本少细分会让柱状图尾部稀碎。pd.cut的rightFalse让10K归到10-15K而不是0-10K整数边界才不会错位。labels必须和bins产生的区间数一致9个边界产生8个区间这里少写一个labels就会直接报错。value_counts之后要sort_index让横坐标按薪资从低到高排列而不是按数量从高到低。这个排序决定读者第一眼能不能看出分布形状。薪资分布图是所有图表里信息量最大的一张因为它直接回答“市场给多少钱”这个问题。4.3 学历与经验门槛两张环形图看准入条件# charts_degree.py import pandas as pd from pyecharts import options as opts from pyecharts.charts import Pie def degree_dist(df): degree_order [不限, 大专, 本科, 硕士, 博士, 其他] df[degree_std] pd.Categorical( df[degree_std], categoriesdegree_order, orderedTrue ) counts df[degree_std].value_counts().sort_index() pie ( Pie() .add( series_name学历要求, data_pair[list(z) for z in zip(counts.index, counts.values)], radius[40%, 68%], label_optsopts.LabelOpts(formatter{b}: {d}%), ) .set_global_opts( title_optsopts.TitleOpts(titlepython岗位学历要求分布), legend_optsopts.LegendOpts(orientvertical, pos_top12%), ) ) pie.render(charts/degree_pie.html)学历不是数值默认value_counts按数量排会把“本科”排到最前面。用pd.Categorical固定顺序图例才会按不限、大专、本科、硕士、博士排列这才像门槛从低到高。环形图用radius[40%,68%]实现内圈留白视觉上比实心饼图清爽很多。formatter里的{d}%是占比模板直接在扇区旁边标百分比答辩时不用凑近数格子。经验分布图用同样结构categories换成[不限,1年以内,1-3年,3-5年,5-10年,10年以上]把experience_std替换degree_std即可不重复贴代码。4.4 城市与行业对比横向条形图避免数值打架# charts_city.py import pandas as pd from pyecharts import options as opts from pyecharts.charts import Bar def city_compare(df): city_stats ( df.groupby(city) .agg(job_count(job_id, count), salary_median(salary_mid, median)) .sort_values(job_count) ) bar ( Bar() .add_xaxis(city_stats.index.tolist()) .add_yaxis(岗位数, city_stats[job_count].tolist()) .reversal_axis() .set_series_opts(label_optsopts.LabelOpts(positionright)) .set_global_opts( title_optsopts.TitleOpts(title各城市python岗位数量对比) ) ) bar.render(charts/city_count.html)city字段在清洗阶段要做标准化“北京”和“北京市”并成“北京”否则同一个城市会裂成两根柱子。岗位数和薪资中位数量纲差十倍不要塞进同一张图分两张摆放对比更清楚。sort_values(job_count)默认升序配合reversal_axis之后岗位数最多的城市会出现在条形图最上面这个细节不处理好图表方向就是反的。行业分布可以用同样方式做Top10横向条形图也可以把skill_tags聚合成词云。词云更适合展示“需要会什么”而不是“哪些行业在招人”如果要用词云注意pyecharts的WordCloud需要额外扩展包支持网络不稳定时安装容易失败准备一个普通柱状图做备选方案。4.5 用flask把图表串成一个演示页# app.py from flask import Flask, render_template from pyecharts.render import render_embed app Flask(__name__) app.route(/) def dashboard(): df load_data() charts [ render_embed(salary_dist(df)), render_embed(degree_dist(df)), render_embed(city_compare(df)), ] return render_template(dashboard.html, chartscharts) if __name__ __main__: app.run(host127.0.0.1, port8000, debugFalse)!-- templates/dashboard.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 titleboss直聘数据可视化分析/title /head body {% for chart in charts %} {{ chart|safe }} {% endfor %} /body /htmlrender_embed把pyecharts图表输出成完整的div和script模板里必须加|safe防止Jinja2把脚本转义成字符串。漏掉safe页面会显示一串转义字符而不是图表。debugFalse这个细节要在答辩前就设好开着debug模式时页面一旦报错会把完整堆栈暴露出来演示现场很尴尬。访问地址用127.0.0.1:8000不要用localhost。部分机器localhost会走IPv6解析导致连接失败127.0.0.1是写死的回环地址兼容性最好。跑起来之后浏览器里就是几个可以滚动的图表页面这就是flask和echarts数据可视化结合的标准形态。5. 绕开采集与展示的五个坑现象、原因、解决采集和分析跑通一遍之后把最常踩的坑按出现频率排一遍。前三条集中在采集和清洗期后两条集中在展示期。每条都按“现象、原因、解决”的结构写方便你对照排查。5.1 采集到第3页突然遇到验证码页现象脚本还在正常翻页某一页的resp.text里突然出现大量“验证”相关字样jobList解析为空程序break退出。原因访问频率超过风控阈值或者cookie被判定为异常环境。固定间隔的sleep比随机间隔更容易触发风控连续几个请求间隔完全一样是明显的脚本特征。解决先停5到10分钟再继续不要换个UA就硬刚那解决不了问题。把sleep下限从3秒提高到5秒上限保持8秒。恢复之后不要并发开多个终端同时跑同一个cookie。触发一次验证码后这个cookie的寿命会明显缩短最好回3.1重新扫码登录一次。5.2 薪资字段带·15薪float转换直接报错现象split_salary处理“20-40K”正常跑到“20-40K·15薪”的时候float转换报错整个清洗流程中断。原因薪资字符串后面还带薪资结构说明replace(K,)再split(-)只能取到前半段后半段“40K·15薪”里的“15薪”没法转成数字。解决用正则只匹配第一段数字区间代码就是3.3里的split_salary。重点是不匹配时统一返回None让这一行薪资变成缺失值而不是让异常中断整个采集。面议、薪酬范围缺失、格式异常全部走同一条路可视化阶段过滤缺失值即可。5.3 jobList字段突然KeyError页面返回结构变了现象re匹配到了INITIAL_STATE但json.loads之后state[jobList]报KeyError代码崩溃。原因页面结构更新过或者当前返回的其实是登录跳转页state里根本没有岗位列表字段。网上流传的解析代码大多是老版本的字段名和层级对不上。解决先print(resp.url)确认请求还停在搜索URL上再看resp.text前500个字符确认不是登录页。字段真实名称以浏览器开发者工具Network面板里接口返回的JSON为准。解析时一律用state.get(jobList) or state.get(jobCardList)或者None兜底取不到就break并打印调试信息别在不确定结构的情况下继续翻页。5.4 图表白屏把echarts资源从CDN换成本地文件现象pyecharts生成的HTML用浏览器打开是空白控制台提示echarts.min.js加载失败。原因pyecharts默认从公共CDN加载ECharts资源在只开放内网、外网不稳定的演示环境里CDN资源加载不出来图表自然白屏。解决先把echarts.min.js下载到项目static目录然后在生成图表之前设置本地资源地址。from pyecharts.globals import CurrentConfig CurrentConfig.ONLINE_HOST http://127.0.0.1:8000/static/设置之后pyecharts生成的HTML里所有脚本引用都会指向本地。答辩现场跑flask时图表资源全部走127.0.0.1完全不依赖外网。这个配置要放在import图表组件之前执行放在render之后不生效。5.5 数据有了图也画了却讲不出结论现象数据入库了图也render出来了老师问“这张图想说明什么”支支吾吾答不上来。原因先画图后想结论图表只是把数据倒出来没有回答任何业务问题。图表本身不会替你说话分析结论需要自己组织。解决画图前先写问题清单每张图配一句话结论。薪资分布图的结论是“python岗位中位数集中在20-30K档”学历环形图的结论是“本科是绝对门槛”城市对比图的结论是“北京上海岗位量领先”。答辩按结论倒着讲先抛结论再指图图的证据价值自然就出来了。下面这张表是采集期的快速排查指引结合前面几条看可以省掉不少debug时间。现场现象优先排查顺序列表页出现验证码停一会再试必要时重新登录取cookiejobList KeyError看resp.url和resp.text前500字符清洗时float报错打印原始salary字符串检查是否带额外文字图表白屏用本地echarts.min.js替代CDNflask页面打不开改用127.0.0.1访问不要用localhost6. 期末答辩前的最后三步把演示做成加分项6.1 演示开场先交数据字典答辩前几分钟老师还没进入状态这时候给一张数据字典是建立信任最快的方式。我一般会打一页纸写明采集关键词、城市范围、有效记录数、清洗规则。例关键词python、java、数据分析覆盖北京上海深圳有效记录800条薪资面议剔除按job_id去重。这一页纸比任何模板都管用它把作业从“代码堆”变成“一份可审计的小型数据报告”。项取值采集关键词python / java / 数据分析覆盖城市北京、上海、深圳有效岗位数800清洗规则job_id去重、面议剔除、薪资转中位数采集时间2024-12-016.2 按结论-证据讲图不要按图讲图演示顺序我固定为市场需求概览、薪资分布、学历经验门槛、城市对比。每张图只留一句结论。比如先讲“招聘需求集中在20-30K薪资段本科以上占比接近七成”然后把图切出来。先结论后证据听的人不需要自己看图猜重点。顺序讲完大概六分钟剩下的时间留给老师提问比对着图从标题讲到图例要高效得多。6.3 两个必被追问的问题怎么答追问一为什么用中位数不用均值。薪资是右偏分布极少数高薪岗位会把均值拉高中位数代表普通求职者能拿到的典型水平。回答完可以补一句图上每一档占比都能复现清洗代码和jobs.db都在项目里随时可以重新算。追问二八百条数据有代表性吗。先承认局限再拉回方法论这是课程设计规模的数据集验证的是从采集、清洗到可视化的完整链路要支撑更强结论可以扩城市、扩关键词、拉长采集周期。硬说“有代表性”反而容易被追着问细节说明样本边界才是更稳的回答。我做这类期末大作业吃过最大的亏是把图堆满整页老师一问这张图说明什么我盯着图看了十几秒说不出话。后来每张图先写结论再画图演示按结论倒着讲效果完全不一样。希望帮到你。本文还有配套的精品资源点击获取