news 2026/9/17 6:09:03

Python爬取Boss直聘大数据岗位并进行数据清洗与可视化分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬取Boss直聘大数据岗位并进行数据清洗与可视化分析

简介:这是一个面向爬虫与数据分析入门学习者的综合性实战项目,以北京市大数据岗位招聘数据为具体对象,串联起从网络数据采集、网页解析、数据清洗、预处理、统计聚合到可视化图表展示的完整分析链路。项目中的爬虫部分演示了如何请求网页并处理动态内容,数据分析部分从平均薪资、学历要求、经验年限、职位供需关系等维度展开统计与解读,前端部分则通过可视化页面让数据结论更直观。资源包共111个文件,核心包括4个Python源程序、原始数据表格文件、数据库文件,以及20余个网页版可视化页面和统计图表图片,并配有页面样式与交互脚本等前端资源,整体大小仅6.58兆,目录结构适合按阶段参考学习。此项目已有726人学习下载,对于希望构建数据分析项目经验、完成课程作业或毕业设计的读者,是一份从代码到数据再到展示形态都很完整的参考资源。

1. 北京大数据岗位招聘数据:为什么值得用Python爬一遍

也许你在Boss直聘上搜“大数据”岗位时,会看到一个几十页的列表,每页都有不同的薪资、经验、技能要求。想弄清“北京哪个区职位最多”“3年经验能拿多少钱”这类问题,手动翻页根本做不到。这个项目把整条链路打通:用Scrapy爬取Boss直聘北京站的大数据相关职位,requests和BeautifulSoup做辅助解析,pandas清洗出结构化表,再用matplotlib与seaborn把结果画成图,最终打包成一套可部署的HTML可视化页面。

无论你是正在准备大数据方向毕业设计的学生,还是想跳槽前摸清市场行情的开发者,都能直接复用这份代码。它给出的不是一张静态报表,而是一份随时可以重跑、调整口径的岗位数据分析工具。整个项目涉及爬虫、数据清洗、特征提取、可视化和数据库存储,正是目前大数据岗位最常要求的技能组合。

2. Scrapy + requests 双通道采集:从HTML到结构化的第一公里

招聘网站不像开放API一样提供干净数据,爬虫的职责是把网页变成结构化字段。这个项目里其实有两条取数路径:一条是Scrapy框架,适合跑批量任务;另一条是requests+BeautifulSoup,用于快速分析单个页面的DOM结构。我的建议是主爬虫走Scrapy,因为它的调度器自带URL去重和并发控制,而requests适合在调试阶段用。

2.1 为什么用Scrapy而不是自己写requests循环

自己写requests循环并不难,但要处理的问题很多:重复请求怎么去重,请求失败重试几次,并发量怎么控制,日志怎么记录。Scrapy把这些基础设施都做好了。它的Request对象会进入调度器,相同指纹的URL默认丢弃;DownloaderMiddleware统一给每个请求附加UA和Cookie;Item Pipeline把解析结果按顺序清洗、去重、入库。对于Boss直聘这种有反爬策略的站点,Scrapy的RetryMiddleware还能在遇到503或验证码时自动延迟重试,省去自己写异常处理的代码。

从实际效果看,用Scrapy写一个Spider,代码量大约是自己写requests循环的一半。而且项目里需要抓多个搜索词(“数据挖掘”“数据分析”“大数据开发”),Scrapy可以通过start_urlsstart_requests轻松生成多任务。这里有一个最小Spider示例:

# spider.py import scrapy from jobs.items import JobItem class JobSpider(scrapy.Spider): name = 'jobs' start_urls = [ 'https://www.zhipin.com/web/geek/job?query=大数据&city=101010100', 'https://www.zhipin.com/web/geek/job?query=数据挖掘&city=101010100', ] def parse(self, response): for card in response.css('.job-card-wrapper'): item = JobItem() item['job_title'] = card.css('.job-name::text').get() item['salary'] = card.css('.salary::text').get() item['company'] = card.css('.company-name::text').get() item['info'] = card.css('.job-info span::text').getall() yield item

说明:job-card-wrapper是职位卡片容器的class,不同时期Boss直聘的页面结构会变,需要先用浏览器开发者工具确认。info字段用getall()返回的是学历、经验、标签的列表,后面在Pipeline里用','.join()合并。如果想单独调试某个页面,可以用requests获取HTML后用BeautifulSoup跑选择器,调试通过再写回Scrapy。

字段CSS选择器处理方式
job_title.job-name::text直接提取文本
salary.salary::text原始字符串,后处理
company.company-name::text去空格
experience.job-info span::textgetall()后拼接
district.job-area::text可能为空,需回退

2.2 Item Pipeline与字段预处理

Scrapy把“解析”和“存储”分开。Spider只负责提取原始文本,Pipeline负责清洗。比如info字段可能是['3-5年','本科','北京朝阳区'],在Pipeline中按位置拆开。示例:

# pipelines.py class CleanPipeline: def process_item(self, item, spider): info = item.get('info', []) if len(info) >= 3: item['experience'] = info[0] item['education'] = info[1] item['district'] = info[2] else: item['experience'] = '' item['education'] = '' item['district'] = '' # 技能标签需要从职位描述中提取,这里先留空 item['skills'] = '' return item

这个Pipeline把列表字段拆分成了结构化字段,同时处理了解析不到数据的异常情况。为什么不直接在Spider里拆?因为Pipeline更便于做数据校验和日志记录,而且如果未来改用爬虫API,逻辑可以复用。

2.3 反爬参数:请求头、下载延迟与Cookie策略

Boss直聘对无头爬虫的识别比较敏感。我一般会在settings.py里做三件事:设置DOWNLOAD_DELAY = 1.5,让请求间隔至少1.5秒;启用Scrapy自带的RandomUserAgentMiddleware,并准备一个几十组UA的列表;在请求头中携带从浏览器复制的Cookie,避免触发登录跳转。代码:

# settings.py DOWNLOAD_DELAY = 1.5 RANDOM_UA_TYPES = ['chrome', 'firefox'] COOKIES_ENABLED = False # 改为False,手动在headers放Cookie DEFAULT_REQUEST_HEADERS = { 'User-Agent': 'Mozilla/5.0 ...', 'Cookie': 'your_cookie_here', 'Referer': 'https://www.zhipin.com/', }

注意:COOKIES_ENABLED = False可以避免Scrapy的Cookie处理器干扰手动Cookie。如果还遇到验证码,就要检查是不是DOWNLOAD_DELAY太小,或者需要代理IP。项目里附带的数据是已经爬完的结果,所以实际运行时可以先拿少量页面测试,确认稳定后再全量抓取。

3. pandas 数据清洗与薪资解析:把“15-30K·14薪”变成数字

爬下来的是原始文本,要做分析必须先清洗。这一章用pandas完成三个核心任务:去重、缺失值处理、把“15-30K·14薪”拆成数字字段。这三个操作占整个数据预处理工作量的70%。

3.1 重复数据与缺失值:先看再决定

读入CSV后,不要急着删行,先用df.info()df.describe()了解数据概况。重复行往往来自分页爬取时的重复记录,例如同一职位出现在“大数据”和“数据分析”两个搜索词下。可以用subset指定精确去重:

import pandas as pd df = pd.read_csv('jobs_raw.csv') print(df.shape) df = df.drop_duplicates(subset=['job_title', 'company', 'salary']) print(f'去掉重复后剩余 {df.shape[0]} 行')

为什么用这三个字段做subset?因为岗位名称、公司、薪资组合起来几乎能唯一标识一条招聘广告;比单用job_title更严格,又比全行列去重更宽容,能保留不同职位的相似文本。

缺失值看df.isnull().sum()。字段district如果缺失,可能是Boss直聘的职位卡片没有展示区域信息,可以暂时填“未知”;salary缺失必须处理,因为后续分析依赖薪资。

3.2 薪资区间正则解析:从字符串到数值

薪资文本有多种变体:“15-30K”“20-40K·14薪”“面议”。下面这段代码提取区间下限和上限,并计算中位数;对“面议”直接记为None:

import re def split_salary(text): if not isinstance(text, str): return pd.Series([None, None, None]) m = re.search(r'(\d+)-(\d+)K', text) if m: low = int(m.group(1)) high = int(m.group(2)) mid = (low + high) / 2 return pd.Series([low, high, mid]) return pd.Series([None, None, None]) df[['salary_low', 'salary_high', 'salary_mid']] = df['salary'].apply(split_salary)

apply(split_salary)在这里逐行处理,返回的Series会自动对齐到新列。也可以改用zip(*df['salary'].apply(split_salary)),但可读性不如直接赋值。注意salary_mid用于画箱线图,因为它比“最低薪资”“最高薪资”更稳定,能代表招聘方的平均报价。

如果还想计算“14薪”的影响,可以再加一个函数:re.search(r'(\d+)薪', text)提取薪期,然后salary_mid * months / 12折算成月均。不过多数岗位描述里的薪资已经是月薪,所以这一步可选。

3.3 学历与经验编码:把文本变成可排序的数值

学历文本一般是“大专”“本科”“硕士”“博士”,用map做有序编码。经验文本有“1-3年”“3-5年”“5-10年”,提取最低年限作为特征。

edu_map = {'大专': 1, '本科': 2, '硕士': 3, '博士': 4} df['edu_code'] = df['education'].map(edu_map) def extract_exp(text): m = re.search(r'(\d+)', text) if isinstance(text, str) else None return int(m.group(1)) if m else None df['exp_year'] = df['experience'].apply(extract_exp)

这里把学历从定类变量转成定序变量,便于后面做相关性分析;经验年限直接用数字表示,能跟薪资做散点图。需要留意:map遇到未识别值会返回NaN,比如“学历不限”,这时可以根据分析目标填0或删除。

清洗后的数据表格如下:

原始salarysalary_lowsalary_highsalary_midedu_codeexp_year
15-30K·14薪153022.523
20-40K·15薪204030.035
面议NaNNaNNaNNaNNaN

经过这步,原始数据已经从“能看”变成“能算”。下一步所有图表都基于salary_midedu_codeexp_year这几个数字列。

4. matplotlib + seaborn 可视化:用图表回答“北京需要什么样的大数据人才”

数据清洗完,下一步是可视化。项目里用matplotlib和seaborn生成静态图,再嵌入到HTML模板中展示。可视化不是随便画几个柱状图,而是要回答具体问题:哪个区岗位最多?薪资和年限怎么变?技能要求集中在哪些词上?

图表数据字段回答的问题
区域分布柱状图district + job_title岗位集中在哪个区
经验薪资箱线图exp_year + salary_mid经验对薪资的影响
技能词频图skills 计数哪些技能最热门

4.1 区域岗位数量分布

直接对districtvalue_counts,画水平条形图,便于区名阅读。注意要先设置中文字体,否则标题和坐标轴会变成方块。代码:

import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False district_counts = df['district'].value_counts().head(10) district_counts.sort_values().plot(kind='barh', figsize=(10, 6)) plt.xlabel('岗位数量') plt.ylabel('区域') plt.title('北京大数据岗位区域分布 Top10') plt.tight_layout() plt.savefig('district.png')

head(10)只取前10,避免海淀区单个大柱子盖住其他区。sort_values()让条形图从低到高排列,长条在下方,视觉上更顺。保存成PNG而不是直接show,是为了后续嵌入到HTML报告中。

4.2 经验要求与薪资的箱线图

箱线图能同时展示分布、中位数和异常值。这里用seaborn,按exp_year分组(0年、1年、3年、5年、10年)画salary_mid的分布:

import seaborn as sns sns.boxplot(data=df, x='exp_year', y='salary_mid') plt.xlabel('工作经验(年)') plt.ylabel('月薪中位数(K)') plt.title('不同经验要求下的薪资分布') plt.xticks(rotation=45) plt.savefig('exp_salary_box.png')

注意exp_year如果是连续的,最好先离散化:df['exp_group'] = pd.cut(df['exp_year'], bins=[0,1,3,5,10], labels=['0-1','1-3','3-5','5-10']),否则箱线图会把每个唯一的年数值都画一箱。离散化后能看到明显的阶梯效应:1-3年到3-5年有一个大的薪资跃升,这是分析报告中值得强调的发现。

4.3 技能关键词词频提取

职位描述里会写“熟悉Python、Spark、Hadoop”,技能字段可能是从标签里抓的。简单的做法是用字符串计数,先定义一个技能字典,再对skills列做str.contains统计:

skills = ['Python', 'Spark', 'Hadoop', 'SQL', 'Java', 'Scala', 'Flink', 'Hive', 'Kafka'] counts = {s: df['skills'].str.contains(s, na=False).sum() for s in skills} counts = pd.Series(counts).sort_values()

这里na=False让缺失值不参与匹配,.sum()统计包含该技能的行数。把结果画成柱状图,就能直观看到Python是绝对第一,Spark与Hive紧随其后。如果想更精细,可以用jieba分词后统计词频,但对于这种预定义的技能清单,直接匹配既快又不易出噪声。

可视化部分的输出直接对接项目里的HTML页面,bootstrap.cssaos.css只是给页面加样式和滚动动效,真正承载数据的是这些PNG图和后面要讲的MySQL表。

5. 数据导出与入库:从CSV到MySQL的工程化细节

分析结果要给别人用,CSV文件不够友好。项目里把清洗后的数据导入MySQL,这样可以用SQL做临时查询,也可以供可视化后端调用。这一章记录三个关键点:连接方式、类型映射、增量更新。

5.1 用SQLAlchemy建立连接

pandas的to_sql不直接接受原生连接,需要通过SQLAlchemy的create_engine。示例:

from sqlalchemy import create_engine engine = create_engine( 'mysql+pymysql://root:password@localhost:3306/jobs_db?charset=utf8mb4' ) df.to_sql('bigdata_jobs', engine, if_exists='replace', index=False)

charset=utf8mb4是关键,否则职位描述里的中文表情或特殊字符会报错。if_exists='replace'适合全量重建表;如果只是追加,改成'append',但前提是表结构一致。

5.2 字段类型与索引设计

pandas自动推断类型可能不准,to_sql可以用dtype参数指定SQL类型,尤其是字符串字段要显式设成VARCHARTEXT,避免默认的VARCHAR(255)截断长文本。

字段SQL类型说明
job_titleVARCHAR(128)职位名
salary_midFLOAT月薪中位数
educationVARCHAR(16)学历原文
exp_yearINT最低经验年限
skillsTEXT技能标签

同时在建表SQL里给job_title+company建立联合唯一索引,配合INSERT ... ON DUPLICATE KEY UPDATE实现更新。

5.3 增量更新:用jobid做分批抓取

爬虫每天跑一次,不能每次都全量重抓。常见做法是:每次抓取时带上当天日期或职位ID,SQL中先SELECT已有的ID集合,再对新增的插入。具体到Scrapy,可以在Pipeline里维护一个已见ID的set,如果item.get('jobid')已存在,直接drop_item

这里有一个简单的去重Pipeline:

from scrapy.exceptions import DropItem class DuplicatePipeline: def __init__(self): self.seen = set() def process_item(self, item, spider): jobid = item.get('jobid') if jobid in self.seen: raise DropItem(f'重复职位: {jobid}') self.seen.add(jobid) return item

但这个seen只在单次爬取内有效,重启爬虫后会丢失。更可靠的做法是先查MySQL里已有的jobid集合,初始化到self.seen中,实现“断点续爬”。在下一章我会专门讲怎么校验这套去重逻辑不会漏数。

6. 让爬虫和数据管线可维护:去重、断点续爬与质量校验

项目交付不能只跑一次,要能重跑、能检查数据有没有问题。这一章给三个具体技巧,都是我在类似项目中会直接落地的。

6.1 用jobid实现断点续爬与增量合并

Boss直聘每一条职位详情页的URL里有唯一ID,例如/job_detail/123456789.html。在Spider里把这个ID提取出来放到item中,然后在Pipeline里用它做全局去重:

# pipelines.py import pymysql from scrapy.exceptions import DropItem class UniquePipeline: def __init__(self): self.conn = pymysql.connect(host='localhost', user='root', password='123456', db='jobs_db') self.seen = self._load_seen() def _load_seen(self): with self.conn.cursor() as cur: cur.execute('SELECT jobid FROM bigdata_jobs') return {row[0] for row in cur.fetchall()} def process_item(self, item, spider): if item['jobid'] in self.seen: raise DropItem(f'已存在: {item["jobid"]}') self.seen.add(item['jobid']) # 继续保存到MySQL... return item

_load_seen在初始化时把已有ID装进集合,内存占用不大;每次新jobid插入后,内存集合和数据库保持同步。这样即使爬到一半中断,下次启动也会自动跳过已完成的部分。

6.2 数据质量校验:断言与阈值

清洗完数据后,我习惯写一段校验脚本,不达标直接抛异常,防止脏数据流入报表。这里用一个简单表格列出校验项和阈值:

检查项阈值说明
salary_mid 缺失数< 5%缺失过多则爬取或解析有问题
重复比例0去重不彻底需重跑
district 覆盖率> 90%区域缺失影响地图可视化
exp_year 最小值>= 0负值来自异常解析

校验代码:

def validate(df): assert df['salary_mid'].isna().mean() < 0.05, '薪资缺失过多' assert df.duplicated(subset=['jobid']).sum() == 0, '存在重复jobid' assert df['district'].notna().mean() > 0.9, '区域覆盖不足' assert (df['exp_year'] >= 0).all(), '经验年限有负值' return True validate(df) print(f'数据校验通过,共 {df.shape[0]} 条记录')

这些断言跑完之后,再生成图表和HTML报告。项目里的bootstrap.cssaos.css这些文件都是报告页面的皮肤,你完全可以用静态站点把这些图表和表单一页展示出来,交给业务方或面试官当作作品集。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 6:05:52

用python-pptx解析PPTX:SMART目标校验与任务排期巡检

简介&#xff1a;这是一份面向团队管理者、项目负责人及培训人员的《团队目标管理》PPT课件&#xff0c;围绕目标设定与落地执行展开&#xff0c;适合内部培训、管理入门或团队复盘参考。课件从彼得杜拉克的目标观切入&#xff0c;梳理团队目标的三大作用&#xff0c;剖析目标模…

作者头像 李华
网站建设 2026/9/17 6:05:51

Linux 安装 VS Code 与项目运行配置全指南

Linux 安装 VS Code 这件事&#xff0c;看起来是一条命令的事&#xff0c;但真正在团队里带新人时&#xff0c;我发现十个人里有八个会在同一个地方卡住——要么是装了个版本落后的发行版仓库包&#xff0c;要么是装完不知道code命令为什么敲不出来&#xff0c;要么是环境跑通了…

作者头像 李华
网站建设 2026/9/17 6:05:38

LabVIEW UDS刷写Main.vi:状态机编排与图莫斯协议栈协同设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 6:05:22

字符串组成问题解析:哈希表与排序法实战

1. 字符串组成问题解析&#xff1a;从入门到精通字符串组成问题在技术面试中出现的频率高达78%&#xff08;根据2023年算法面试题库统计&#xff09;&#xff0c;这类问题看似简单却暗藏玄机。作为面试官最爱的考察点之一&#xff0c;它不仅能检验候选人对基础数据结构的掌握程…

作者头像 李华
网站建设 2026/9/17 6:04:37

Python京东抢购脚本技术解析:虚拟环境、Cookie管理与异步高并发

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华