
简介基于Python实现北京市大数据岗位招聘数据的全链路分析项目覆盖爬虫采集、数据清洗、统计分析与可视化展示适合希望从零入门网络爬虫与数据分析的开发者、学生以及需要把握招聘市场动态的求职者。项目以Boss直聘为目标站点讲解如何使用请求库与Scrapy框架配合BeautifulSoup完成网页解析与字段提取并通过pandas进行去重、缺失值处理和聚合统计最终用matplotlib等可视化工具生成岗位地域、薪资区间、经验要求等分布图表。资源共111个文件包含4个Python源代码、19个网页模板与样式、1个CSV数据集和1个数据库文件另有xls表格、示例图片以及前端展示所需的css/js/字体等辅助资源压缩包整体仅6.58MB。目前已有726人学习下载属于可直接运行的完整项目。项目结构清晰从爬虫模块到分析脚本再到前端展示均有对应目录。通过该项目能系统掌握招聘数据从采集到可视化的实现思路包括目录结构设计、数据持久化方式、图表渲染配置等关键环节便于二次开发或迁移到其他城市岗位分析。1. 用 Python 抓一遍北京市大数据岗位招聘数据再清洗成能画图的表格最后拼出可视化大屏这条链路本身并不复杂。多数人不是卡在爬虫被反爬拦截就是卡在薪资字段是20-40K·14薪这类混合文本没法直接做聚合统计。问题的关键从来不是某个环节多难而是每个环节都要处理真实数据里的噪音。这个项目实际上是一条完整的数据流水线requests 爬虫采集原始数据pandas 负责清洗和特征工程PyECharts 把分析结果渲染成交互式可视化图表。对正在准备大数据毕业设计、或者想验证自己数据分析实操能力的人来说它的价值在于每个环节都有可运行的代码和真实数据支撑而不是只给一张画好的效果图。分析结果还可以回答一个很实际的问题北京的大数据岗位到底要几年经验、什么学历、薪资中位数落在哪。我按从业者最常见的做法把这条链路拆成采集字段设计、薪资清洗与聚合分析、可视化大屏组装、结果校验四段。下面从爬虫端开始讲清楚每一步的参数选择和容易翻车的细节。2. requests 爬虫采集北京市大数据岗位招聘数据字段设计与反爬参数2.1 招聘数据源选择与字段建模岗位、薪资、学历、公司规模要做北京市大数据岗位的分析第一步不是写代码而是确定采集哪些字段。常见做法是选一个允许网页端搜索的招聘平台优先走它前端调用的 JSON 搜索接口而不是去解析 HTML 页面。JSON 接口的字段稳定、分页明确省掉了解析 DOM 的适配工作这也是 requests 爬虫最合适的形态。用 Python 做这类采集环境上只需要把 python 装好3.8 以上即可再装 requests、pandas没有额外的系统依赖。字段要按分析目标倒推不能抓到什么算什么。薪资要用来算中位数经验和学历要做分组对比公司规模用来判断岗位所在企业的类型岗位职责文本用来做热词统计。采集时保持原始文本不动清洗阶段再加工能避免爬虫端逻辑过重、中断后难续跑的问题。这套字段设计和 requests 爬虫的写法换到爬取竞品数据、行业报告等场景也能直接复用。字段名示例值用途落地类型job_id29571830去重主键字符串job_title大数据开发工程师岗位名称统计字符串salary_text20-40K·14薪薪资解析原始文本exp_text3-5年经验要求原始文本edu_text本科学历门槛原始文本company_name某科技有限公司公司维度聚合字符串company_size500-999人公司规模分组原始文本city北京地区过滤字符串pub_time2025-01-08时效性过滤datetimejob_desc岗位职责任职要求文本热词分析长文本这里有个容易踩的坑salary_text 里的14薪如果直接丢弃年终奖信息就损失了exp_text 里3-5年和3年以上格式不一致强类型转换必然报错。所以字段建模的要点是采集时保留原样把类型兜底做在采集函数里所有业务映射统一放到清洗阶段处理。2.2 最小可运行的招聘列表页爬虫请求参数与解析逻辑数据源确定后用 requests 实现一个抓取单页的函数。招聘搜索接口通常支持 query、city、page、pageSize 四个核心参数返回体是 JSON。下面是这种常见接口的最小实现换成目标平台的实际接口和解析字段即可跑通import requests import time def fetch_job_page(keyword大数据, city北京, page1, page_size20): url https://job-api.example.com/v1/search # 替换为目标平台的搜索接口 params { query: keyword, # 搜索关键词可传大数据或数据开发 city: city, # 城市编码或中文名 page: page, # 页码从 1 开始 pageSize: page_size, # 单页条数招聘接口常见 20 或 30 sortType: relevance, # 按相关度排序多次抓取顺序更稳定 } 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://job.example.com/beijing/data, Accept: application/json, text/plain, */*, } resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() # 非 2xx 直接抛异常交给重试逻辑 data resp.json() time.sleep(1) # 单页抓完强制休眠配合限速策略 return data参数说明query 决定搜索范围大数据能覆盖大数据开发、大数据平台等岗位但会漏掉数据仓库数据挖掘这类变体后续要在关键词池里补sortType 用相关度而不是发布时间是为了让列表结果在多次抓取时顺序稳定方便按 job_id 去重timeout 设 10 秒防止某个慢请求拖住整个采集任务。如果接口返回的字段嵌套在 data.list 这种结构中先打一层字典取值再拍平拼接到全局列表里全部页抓完后一次性转 DataFrame 落地比边抓边写文件更高效。2.3 反爬参数、重试退避与采集失败定位爬虫最常见的失败不是解析问题而是请求被限流或封禁。症状通常是前几页正常第 N 页开始返回 403 或一个验证码页面。预防手段必须组合使用只换一个 User-Agent 基本无效。一个可靠的请求函数要带重试和退避import random import logging import time def fetch_with_retry(fetch_func, max_retry3): for attempt in range(1, max_retry 1): try: return fetch_func() except requests.exceptions.HTTPError as e: if e.response.status_code 403: wait 30 * attempt random.uniform(0, 10) logging.warning(触发限流第 %s 次重试等待 %.1f 秒, attempt, wait) time.sleep(wait) else: raise return None重试的核心是退避策略第一次等待 30 秒左右第二次 60 秒左右而不是固定间隔。随机抖动 random.uniform 是为了避免多个任务同时醒来形成新的请求峰值。频率控制涉及三个关键参数参数常见取值影响调参方向单页间隔1~3 秒越快越容易被限流被封后优先翻倍重试次数2~4 次临时错误容忍度总任务时长内尽量少退避基数20~60 秒解封前的等待时长403 反复出现时调大定位问题时要看三个信号状态码、返回体长度、JSON 解析是否抛异常。如果某页返回 200 但 body 长度明显短于正常页大概率是接口返回了降级数据如果 JSON 解析报 KeyError往往是字段名变了这比限流更值得警惕。对北京市大数据岗位这个量级一页 20 条、抓 25 页也就 500 条数据单线程完全够用不必一上来就上分布式爬虫。只有当关键词池扩大到几十个、需要全量采集时才考虑多进程或机群分发。注意采集频率要遵守目标网站的 robots 协议和公开接口使用限制仅用于学习与个人研究不要对线上服务造成压力。职业用途的采集先确认平台是否允许并严格控制请求量。3. pandas 数据清洗与薪资特征工程把20-40K·14薪折算成统一口径3.1 薪资区间解析与中位数折算招聘平台的薪资文本大致有三类区间型20-40K·14薪、上限型40K以上、模糊型面议。直接存字符串没法做聚合必须先解析出下限、上限、月均三个数值列。解析逻辑要覆盖三类写法import pandas as pd import re def parse_salary(text: str) - dict: if not text or text.strip() 面议: return {salary_low: None, salary_high: None, salary_month: None} main text.split(·)[0] # 去掉14薪这类年终信息 nums re.findall(r\d, main) if 以上 in main: low int(nums[0]) high None elif len(nums) 2: low, high int(nums[0]), int(nums[1]) else: return {salary_low: None, salary_high: None, salary_month: None} mid (low high) / 2 if high else low return {salary_low: low, salary_high: high, salary_month: mid} df[salary_parsed] df[salary_text].apply(parse_salary) df pd.concat([df, df[salary_parsed].apply(pd.Series)], axis1) df.drop(columns[salary_parsed], inplaceTrue)逻辑说明先按·切分把14薪15薪这类年终月份信息从主薪资中剥离避免正则被干扰再用正则抽出所有数字20-40K得到 20 和 4040K以上只得到一个数字用 high 为 None 区分最后中位数取区间两端均值单值就取本身。单位处理上国内招聘平台常见K表示千元/月也有少数写20000-40000/月遇到后者要在解析前先做单位归一否则会把 20000 当成 20 算进中位数。原始文本下限上限月薪中位数处理规则20-40K·14薪204030按·切分后取主区间40K以上40空40上限缺失中位数取下限面议空空空整条剔除不参与聚合300-500元/天0.651.080.87按 21.75 天折算月薪最后一行说明按天计薪的岗位要单独折算300-500元/天按每月 21.75 个工作日折算成月薪后再入库。这类数据在大数据岗位里占比不大但如果不处理中位数会被极端值拉偏。3.2 经验与学历字段的有序编码经验文本和学历文本都是字符串直接 groupby 也能分组但排序会按拼音而不是业务含义。更好的办法是做有序编码把3-5年映射成数值等级这样既能做分组统计也能画热力图或算相关性。这种字段口径设计在大数据岗位的实际业务里很常见面试时也常被问到分桶依据exp_map { 在校生: 0, 无需经验: 1, 1年以内: 1, 1-3年: 2, 3-5年: 3, 5-10年: 4, 10年以上: 5, } edu_rank {大专: 1, 本科: 2, 硕士: 3, 博士: 4} df[exp_level] df[exp_text].map(exp_map) df[edu_level] df[edu_text].map(edu_rank) # 没有映射上的值会变成 NaN单独归为不限桶 df[exp_level] df[exp_level].fillna(-1) df[edu_level] df[edu_level].fillna(0)这里要说明为什么用有序编码而不是 one-hot经验等级 0 到 5 本身有大小关系one-hot 会丢掉这个顺序信息后续做相关性计算或散点图都吃亏。fillna(-1) 和 fillna(0) 是为了把经验不限学历不限单独保留一个桶而不是直接丢弃——在招聘分析里不限本身就是一个值得统计的岗位特征。3.3 多维聚合分析学历、经验、公司规模对薪资的影响清洗完字段就可以用 pandas 做分组聚合输出后续可视化要用的数据表。这一步的结果直接决定图表长什么样所以要先把口径定清楚薪资中位数用的是月薪字段 salary_month而不是上下限样本量太小的分组要标记出来避免一张图里混入只有两条数据的组。edu_stat ( df.dropna(subset[salary_month, edu_level]) .groupby(edu_text)[salary_month] .agg([count, median, mean]) .round(1) .sort_values(median, ascendingFalse) ) exp_stat ( df[df[exp_level] 0] .groupby(exp_level)[salary_month] .agg([count, median]) ) size_stat ( df.groupby(company_size)[salary_month] .median() .sort_values(ascendingFalse) )聚合结果的解读有两个容易错的点。第一mean 受40K以上这类单值上限的影响很大所以报告里优先用 medianmean 只做参考列第二groupby 之后要顺手看 count如果某个分组只有几条样本中位数就没有统计意义后续可视化时要单独处理或直接不展示。岗位职责文本的热词统计也建议在清洗阶段做掉因为后面可视化大屏要用。用 jieba 分词加停用词过滤是常见做法import jieba from collections import Counter stop_words {岗位职责, 任职要求, 岗位, 负责, 以及, 相关, 以上} words [] for desc in df[job_desc].dropna(): words [w for w in jieba.lcut(desc) if len(w) 1 and w not in stop_words] top_words Counter(words).most_common(40)分词结果的噪音主要来自两类词一类是招聘模板自带的岗位职责任职要求这类页面固定文案必须进停用词表另一类是HadoopSpark这种专有名词jieba 默认词库可能切错可以用 jieba.add_word(Hadoop) 手动补词典。热词统计的完整结果先存成 DataFrame后面做词云或横向条形图直接取 top 30 即可。4. PyECharts 可视化展示从 matplotlib 到可交互的招聘数据大屏4.1 可视化选型为什么招聘数据分析项目常见做法是 PyECharts可视化这一步的选型直接决定交付形态。matplotlib 适合出论文级静态图pandas 内置的 plot 底层就是它学习成本低但图表交互性差BI 工具如 Tableau 或开源的数据分析平台上手快但脱离不了独立软件环境PyECharts 是这几个方案里最适合做可视化大屏的。它生成的是 HTML 页面自带缩放、提示框、图例切换等交互能力图表可以任意组合成 Grid 布局Python 侧只负责传数据和配置项不用碰前端代码。对北京市大数据岗位这个分析场景需要的图表类型很固定薪资分布的条形图、学历与经验交叉的热力图、公司规模的分组条形图、岗位职责热词图。这几类 PyECharts 都有现成组件而且中文字体渲染没有乱码问题。如果你只需要快速看看趋势matplotlib 三行代码就能出图但要做成可交付的大屏PyECharts 是投入产出比最高的选择。4.2 薪资分布条形图与学历-经验交叉热力图代码先做按学历分组的薪资中位数条形图直接用上一章聚合好的 edu_statfrom pyecharts.charts import Bar, HeatMap from pyecharts import options as opts edu_sorted edu_stat.dropna().sort_values(median, ascendingTrue) bar ( Bar(init_optsopts.InitOpts(width800px, height500px)) .add_xaxis(edu_sorted.index.tolist()) .add_yaxis( 薪资中位数K/月, [round(v, 1) for v in edu_sorted[median].tolist()], label_optsopts.LabelOpts(positionright), ) .set_global_opts( title_optsopts.TitleOpts(title北京市大数据岗位薪资中位数按学历), yaxis_optsopts.AxisOpts(type_category), xaxis_optsopts.AxisOpts(nameK/月), ) )参数说明条形图这里把 x 轴和 y 轴做了对调学历这类固定类别放在 y 轴更适合横向展示学历名不会折行LabelOpts positionright 让数值标签显示在条末端方便直接读数。更关键的是 add_yaxis 里传入的数据要和 x 轴顺序一一对应pyecharts 不校验对齐顺序错了图表就完全反了。热力图用来观察学历等级与经验等级交叉后的薪资分布需要先把数据展开成二维矩阵import numpy as np pivot df.pivot_table( indexedu_level, columnsexp_level, valuessalary_month, aggfuncmedian, ) heat_data [ [int(x), int(y), round(float(pivot.loc[x, y]), 1)] for x in pivot.index for y in pivot.columns if not np.isnan(pivot.loc[x, y]) ] heatmap ( HeatMap(init_optsopts.InitOpts(width800px, height500px)) .add_xaxis([不限, 1年以内, 1-3年, 3-5年, 5-10年, 10年以上]) .add_yaxis(学历, [大专, 本科, 硕士, 博士], heat_data) .set_global_opts( title_optsopts.TitleOpts(title学历×经验薪资热力图K/月), visualmap_optsopts.VisualMapOpts(max_np.nanmax(pivot.values)), ) )这里容易出错的是 heat_data 的数据格式PyECharts 的 HeatMap 要求每个元素是 [x索引, y索引, value] 的三元组x 对应 add_xaxis 的列表下标y 对应 add_yaxis 的列表下标。pivot_table 聚合出的蛋形结构里如果有 NaN必须先过滤否则图表渲染会静默失败只显示一个空坐标轴。4.3 用 Grid 组装可视化大屏并配置刷新参数单张图做完后用 Grid 把它们拼到同一个页面里。Grid 的定位方式是按百分比坐标不用关心像素值适配不同分辨率屏幕更省事from pyecharts.charts import Grid grid Grid( init_optsopts.InitOpts( width1400px, height900px, page_title北京市大数据岗位招聘分析面板, ) ) grid.add(bar, grid_optsopts.GridOpts(pos_left5%, pos_right55%, pos_top15%)) grid.add(heatmap, grid_optsopts.GridOpts(pos_left55%, pos_right5%, pos_top15%)) grid.render(beijing_bigdata_jobs.html)Grid 参数取值示例作用调整建议pos_left5% / 55%图表左边界左右分栏时左图 5%右图 55%pos_right55% / 5%图表右边界与 pos_left 配合控制宽度pos_top15%图表上边界顶部留白给 title 和视觉组件pos_bottom10%图表下边界底部有 VisualMap 时调大到 20%Grid 布局的坐标系是相对整个页面的百分比两个图上下或左右拼时要保证一张图的 pos_right 和另一张图的 pos_left 数值有重叠或恰好衔接否则中间会出现空白条或压图。渲染出的 HTML 是静态快照数据刷新后重新 render 覆盖即可。如果要做定时刷新的大屏常见做法是把这个 render 逻辑包进 FastAPI 或 Flask 的一个定时任务里每隔几小时重跑一次采集和分析大屏页面本身不动。提示pyecharts 版本差异会导致部分参数变化本文章节的写法基于 pyecharts 2.x 的链式 API。安装时用 pip install pyecharts 默认装 2.x 即可。5. 数据校验与关键词扩展让采集结果可复现、可增量更新5.1 用总数、分页与 job_id 三重校验采集完整性爬虫跑完不等于数据可靠。第一批清洗前先做三个校验接口返回的总数、实际抓到的条数、去重后的唯一岗位数。这三个数字对不上说明采集过程中有翻页丢失或重复写入。expected_total job_meta[total] # 接口元数据里的总数 actual_total len(df) unique_total df[job_id].nunique() print(f接口声明 {expected_total} 条实际抓取 {actual_total} 条去重后 {unique_total} 条) assert actual_total unique_total, 出现 job_id 为空的数据检查解析字段 dup_ratio 1 - unique_total / actual_total if dup_ratio 0.05: print(f重复率 {dup_ratio:.2%}检查翻页参数或列表排序是否稳定)除此之外还要盯每个字段的空值率。salary_text 空值超过 10% 通常不是数据问题而是解析时某个新格式没覆盖到job_desc 空值率高则说明详情页字段没有取到。定期把这几个指标打印出来比肉眼扫数据更早发现问题。5.2 用 jieba.analyse 提取需求热词并回填关键词池最后一环是把分析结果反哺给采集端让下一轮爬虫覆盖更全的岗位。具体做法是用 jieba 的 TF-IDF 接口从全部岗位职责文本里提取关键词再把高频词加入爬虫的 query 列表from jieba import analyse all_text .join(df[job_desc].dropna()) keywords analyse.extract_tags(all_text, topK30, withWeightTrue) for word, weight in keywords[:15]: print(f{word}: {weight:.4f}) new_keywords [word for word, _ in keywords[:10]] # 下一轮采集时用 new_keywords 拼进 query 列表TF-IDF 提取的热词比单纯词频更合理像岗位职责这类高频但无区分度的词权重会自然被压低。把实时计算数据治理数据仓库这类词回填到关键词池后下一轮采集就能覆盖到以大数据为主关键词搜不到的变体岗位。增量更新的循环就完整了先按初版关键词采集清洗分析后产出热词再扩充关键词池重跑采集。每一轮都要把 job_id 去重放在最前面保证增量采集不会把同一个岗位重复计入薪资统计。本文还有配套的精品资源点击获取