资讯详情

资讯详情

Python实战:爬虫抓取招聘数据到可视化大屏的完整分析流程

简介面向计划入门Python爬虫与数据分析的学习者这份北京市大数据岗位招聘数据分析及可视化项目提供了从数据采集、清洗分析到前端展示的完整参考链路。项目以Boss直聘岗位信息为对象演示Scrapy/requests发起抓取、BeautifulSoup/lxml解析页面、pandas完成缺失值处理与分组聚合、matplotlib/seaborn绘制分布图表等关键操作并配套生成可视化图表与可交互页面可直接观察招聘市场地域、薪资、经验需求的分布规律。压缩包共111个文件涵盖py爬虫与处理脚本、csv/db数据文件、html/css/js可视化页面及xls结果表另含Bootstrap、图标库等静态资源整体6.58MB工程结构完整方便用IDE打开逐模块阅读。目前已有727人学习下载对希望通过真实案例串起爬虫、数据分析与可视化知识点或需要课程设计、毕业设计参考的开发者很有价值。1. 为什么拆这份Python招聘数据分析源码从爬虫到可视化大屏的一次完整闭环这份基于Python实现的北京市大数据岗位招聘分析项目不是那种只丢给你一堆代码、跑起来全是报错的源码包而是一整套能直接转起来的数据链路用爬虫从Boss直聘上抓岗位数据pandas做清洗和分析最后落到一个Bootstrap搭出来的可视化大屏上。我拆完的最大感受是它把数据分析岗日常要做的采集、清洗、分析、展示四件事压缩成了一个可复现的完整项目。正在学Python数据分析、想练爬虫实战或者准备数据岗面试作品的人照着这份源码走一遍要比看十篇零散教程有用。下面按我实际跑通的顺序写先抓数据清洗再分析出图把坑都列出来最后给一条可重复执行的进阶路径。2. 数据采集用requests和BeautifulSoup抓北京大数据岗位先处理反爬再谈数据量这套项目里最容易被新人卡住的就是第一步数据到底怎么进到本地CSV的。很多人在网上看到Boss直聘的职位页觉得结构复杂、字段藏在弹窗里直接就想上selenium去模拟浏览器。但在这个项目里我验证下来requests加BeautifulSoup就够了而且更轻、更好调出问题也更好排查。2.1 先看项目结构哪些文件要动哪些文件不要动把源码包解压后先别急着跑按目录把文件分三类看清楚。第一类是爬虫相关一般是spider.py或者boss_spider.py这是入口里面写了搜索关键词和URL模板第二类是数据文件比如jobs.csv、cleaned_jobs.csv第一次跑之前可能不存在跑完才会生成第三类是展示相关的静态资源像bootstrap.css、bootstrap.min.css、icofont.min.css、animate.css、boxicons.min.css、aos.css这一串都是可视化页面要用的前端库正常情况下你不需要去改它们更别因为加载不出来就删掉。我一般会先用文本编辑器打开爬虫脚本确认三件事目标URL是不是北京地区的大数据岗位搜索页headers里有没有写User-Agent以及最后保存文件名是什么。这三处确认完再把数据文件和展示目录单独看一眼心里有数之后才开始跑。原因是爬虫这步一旦跑偏后面所有分析都是在错误数据上做文章。2.2 列表页抓取核心代码与参数说明Boss直聘的列表页抓取思路是先发一个GET请求拿到HTML再用BeautifulSoup定位职位卡片节点。下面是这个项目里我跑通的简化版核心代码去掉了一些项目路径相关的配置保留了主流程。import requests from bs4 import BeautifulSoup import time import csv HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.5, Referer: https://www.zhipin.com/ } def fetch_list_page(url): resp requests.get(url, headersHEADERS, timeout10) resp.encoding resp.apparent_encoding print(HTTP状态码:, resp.status_code, 页面长度:, len(resp.text)) return resp.text def parse_list_page(html): soup BeautifulSoup(html, html.parser) items [] # 这里的选择器以源码包里的注释为准不同页面版本 class 名会有差异 for card in soup.select(.job-card-wrapper): title_node card.select_one(.job-name) salary_node card.select_one(.salary) company_node card.select_one(.company-name) if title_node and salary_node: items.append({ 岗位: title_node.get_text(stripTrue), 薪资: salary_node.get_text(stripTrue), 公司: company_node.get_text(stripTrue) if company_node else , }) return items这里有两个参数值得注意。第一个是resp.apparent_encoding它通过页面文本自动猜测编码用来规避中文乱码比直接写死utf-8要稳。第二个是HEADERS里的Referer很多招聘站会校验请求来源带上它能少触发一道风控。你把这个脚本跑通之后再考虑分页问题。Boss直聘的分页是通过URL里的page参数控制的写一个循环每翻一页time.sleep(2)到3秒比一次性把几十页全部拉下来安全得多。2.3 详情页字段补全抓不到就先跳过别让单条报错毁掉整批数据列表页能拿到岗位名称、薪资、公司名这三个核心字段但做数据分析只有这些远远不够。职位描述、经验要求、学历要求、技能关键词都在详情页里。这个项目的做法是先从列表页拿到每个岗位的详情链接再逐个请求详情页补全字段。这里有个关键设计字段缺失时用空字符串兜底而不是直接抛异常中断整个爬虫。因为实际情况里总有几条岗位页面结构不一致或者被风控拦截一条报错就让全批重跑代价太高。def fetch_job_detail(detail_url): try: resp requests.get(detail_url, headersHEADERS, timeout8) resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, html.parser) desc_node soup.select_one(.job-sec-text) exp_node soup.select_one(.job-banner-info .info-list) return { 职位描述: desc_node.get_text(\n, stripTrue) if desc_node else , 经验学历: exp_node.get_text(|, stripTrue) if exp_node else , } except Exception as e: print(详情页抓取失败:, detail_url, e) return {职位描述: , 经验学历: }这段代码里的get_text(\n, stripTrue)是把描述里多行文本用换行符拼起来保留段落结构方便后面做技能关键词统计。try...except包住整段请求失败时返回空字典保证主循环不中断。我实际统计过一个500条规模的采集任务因为网络抖动或页面结构差异大约会有3%到5%的详情页抓不到这部分数据直接留空就好放进清洗环节处理比当场报错强。数据下载完统一写入CSV时注意用newline打开文件避免Windows下多出一个空行。爬虫这一步做到这里就够了不要贪多。有人问我为什么不用scrapy这个项目requests已经能覆盖需求scrapy调度能力强但配置成本高新手在这个体量下用requests反而更容易理解整个流程。3. 数据清洗把“20-40K·13薪”拆成可计算的字段顺便收拾缺失值爬虫落库只是原始数据离能分析还差一大步。我刚拿到这个项目的数据文件时第一反应是CSV里那些薪资字段根本没法直接计算。“30-50K·16薪”“250-300元/天”“面议”混在一列里pandas的mean()算不出来。所以数据清洗是这个项目第二个重头戏也是面试官最喜欢深挖的部分。3.1 原始数据到底长什么样先跑describe和info再动手拿到CSV后第一件事不是写清洗逻辑而是先看数据的长相。我会在项目根目录打开一个Python终端执行下面这几行import pandas as pd df pd.read_csv(jobs.csv) print(df.shape) print(df.columns.tolist()) print(df.head(10).to_string()) print(df.info())df.shape告诉你总共有多少行、多少列。info()会列每一列的非空值数量空值数量就是两者差值。这一步能快速判断哪些列缺失严重哪些列格式统一哪些列得重点清洗。在我拆的这个项目里最脏的永远是薪资、经验、学历这三列。薪资混杂“K”和“元/天”两种单位经验有“1-3年”“在校/应届”“不限”三种形态这些都要在清洗阶段统一口径。3.2 薪资字段拆分正则兼容K、k、元/天、面议的写法薪资拆分的核心是用正则把字符串里的数字区间抠出来再统一单位。下面是项目里清洗薪资的简化代码它处理了大小写K混用、元/天换算、面议兜底三种情况。import re def parse_salary(text): if not isinstance(text, str): return None, None text text.upper().replace( , ) m re.search(r(\d(?:\.\d)?)[-~](\d(?:\.\d)?)K, text) if m: return float(m.group(1)), float(m.group(2)) m_day re.search(r(\d(?:\.\d)?)[-~](\d(?:\.\d)?)元/天, text) if m_day: # 按每月21.75个工作日换算成月薪 d1, d2 float(m_day.group(1)), float(m_day.group(2)) return round(d1 * 21.75, 1), round(d2 * 21.75, 1) if 面议 in text: return None, None return None, None df[最低月薪], df[最高月薪] zip(*df[薪资].apply(parse_salary)) df[平均月薪] df[[最低月薪, 最高月薪]].mean(axis1)这段代码里值得注意的有两点。第一是先统一upper()再用正则这样K和k都能被匹配到避免同一个薪资写法拆出两种结果。第二是“元/天”的岗位按21.75个工作日换算成月薪这样“20-40K·13薪”和“250-300元/天”才能放进同一个坐标系比较。实际跑的时候还会遇到“20-40K·13薪”这种带年终奖的写法主正则匹配完数字区间后“13薪”会被忽略掉这不算错因为月薪对比本身已经能反映水平年终奖信息可以在后续分析里单独统计。如果你以后想算年薪可以再写一个parser_bonus函数把“X薪”抽出来乘进去。3.3 经验、学历、技能标签的标准化经验字段“在校/应届”“1-3年”“3-5年”“5-10年”“不限”这几种写法直接做分组统计时顺序是乱的所以我一般会选择把它映射成数值区间在校/应届记成0-1年不限记成0-50年或者干脆单独设一个“不限”类别。学历字段类似把“本科及以上”“统招本科”“大专”归一化成“本科”“大专”“硕士”这样的粗粒度。这一步的意义不是简化数据而是让后续分组分析的口径统一不然同一家公司岗位的学历要求会分裂出七八种写法。技能标签的提取我放在清洗之后。爬虫拿到的职位描述是整段文本清洗阶段先不拆词等分析阶段再用jieba或直接按标点切分统计高频词。这里只需要保证描述文本是干净的纯文本没有HTML标签没有乱码字符就足够后续用了。3.4 清洗结果落盘为什么CSV要用utf-8-sig清洗完成的数据我习惯写成新文件不覆盖原始爬虫数据目的是保留溯源能力。任何一列清洗逻辑出错回头还能拿原始数据重新跑一遍。保存时用to_csv指定编码为utf-8-sig这一点是血泪教训换来的。df_clean df.dropna(subset[岗位, 公司]) df_clean.to_csv(cleaned_jobs.csv, indexFalse, encodingutf-8-sig)utf-8-sig和utf-8的区别在于文件开头会写入BOM头。BOM头能让Excel打开CSV时自动识别成UTF-8编码不出现中文乱码。如果你只追求代码在终端里能跑utf-8完全够用但数据交到非技术同事手里大概率打开就是乱码。项目里的展示大屏如果直接从CSV读数据这个细节就更重要因为浏览器和Excel对编码的默认处理方式不一样。保存完CSV用df_clean.describe()看一眼平均月薪的最小值、最大值和分位数确认没有出现年薪千万这种离谱值清洗这步才真正结束。4. 数据分析与可视化展示从groupby聚合到Bootstrap大屏数据洗到可计算的状态接下来就是把这个项目最有看头的部分做出来回答几个具体问题然后把答案画成图拼成可视化大屏。很多初学者在一开始就沉迷调整图表样式我的习惯是先算清楚再画图统计口径不对图再漂亮也是错的。4.1 用pandas算清楚三个核心问题谁薪资高、要什么经验、要什么技能围绕这份北京市大数据岗位数据值得回答的问题有几个不同学历对应的平均月薪是多少不同经验区间的岗位数量分布如何哪些技能在职位描述里出现频率最高。下面这段代码集中处理这三个问题。# 学历与薪资关系 edu_salary df_clean.groupby(学历)[平均月薪].agg([mean, median, count]) edu_salary edu_salary.sort_values(mean, ascendingFalse) print(edu_salary) # 经验与岗位数量关系 exp_count df_clean[经验区间].value_counts() # 技能关键词频次 from collections import Counter def extract_skills(text, skill_list): if not isinstance(text, str): return [] return [s for s in skill_list if s in text] SKILLS [Python, Java, SQL, Hadoop, Spark, Flink, Hive, 数据挖掘, 机器学习, 深度学习] skill_counter Counter() for desc in df_clean[职位描述]: for s in extract_skills(desc, SKILLS): skill_counter[s] 1 print(skill_counter.most_common(10))groupby(学历)[平均月薪].agg([mean, median, count])一次把均值、中位数、样本量都算出来。为什么均值和中位数一起看因为均值容易被个别高薪岗位拉高中位数更稳健。如果两者差距太大说明学历群体的薪资分布偏态严重写分析结论时就应该用中位数而不用均值。技能频次统计用的是简单子串匹配不是语义识别好处是快、可控、不会引入模型误差缺点是省不掉的比如“Python开发”和“Python数据分析”会重复计数。这个项目里用这种方案够用真要搞精准抽取就得用词向量或者标注数据了先不必复杂化。4.2 matplotlib出图先出四张基础图再决定做不做大屏统计结果要变成可读的图我先把基础四件套画出来学历薪资柱状图、经验要求数量柱状图、技能频次横向条形图、平均月薪分布直方图。以下给出学历薪资柱状图的代码其余图只是换数据源和图表类型套路完全一致。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(10, 6)) # 中文标签在matplotlib里的处理方式中文字体做前后端分离 ax.bar(edu_salary.index, edu_salary[mean], color#2c7fb8) ax.set_title(北京大数据岗位不同学历平均月薪单位K) ax.set_xlabel(学历) ax.set_ylabel(平均月薪K) for i, v in enumerate(edu_salary[mean]): ax.text(i, v 0.5, round(v, 1), hacenter) plt.tight_layout() plt.savefig(fig_edu_salary.png, dpi150) plt.close()plt.rcParams[font.sans-serif]指定中文字体不然图上所有中文会显示成方块这是matplotlib最常见的坑。我一般固定用SimHei加Microsoft YaHei两个备选Linux服务器上没有这两个字体时再换成Noto Sans CJK。注意画完要执行plt.close()释放内存否则在循环里连续出图会内存堆积后面做批量出图时特别容易踩到。四张基础图出来之后再决定要不要做可视化大屏。如果只是自己分析数据四张静态图已经够回答了核心问题。但这个项目的亮点是它自带Bootstrap系的CSS和AOS动画库明显是奔着做展示页去的。所以下一步就是把统计结果接进一个网页大屏。4.3 把统计结果接进可视化大屏Bootstrap布局 AOS动画的接法可视化大屏本质上是一个静态HTML页面数据不是实时通信获取的而是爬虫和分析跑完后的快照。我查看了源码包里的CSS文件清单bootstrap.css负责布局栅格icofont和boxicons提供图标animate.css和aos.css负责进场动画。前端逻辑上最稳妥的做法是把4.1里算出的统计结果直接嵌入HTML片段。div classcontainer>import subprocess import sys steps [ (spider.py, 爬取阶段), (clean.py, 清洗阶段), (analyze.py, 分析出图阶段), (render_dashboard.py, 渲染大屏阶段), ] for script, desc in steps: print(f[{desc}] 执行 {script} ...) ret subprocess.run([sys.executable, script]) if ret.returncode ! 0: print(f{script} 执行失败终止流程) break print(全流程结束验证 report_data.json 更新时间即可确认是否刷新)这段脚本用subprocess.run按顺序执行四个阶段的独立脚本每一步返回码非零就中断避免带病往下跑。为什么强调独立脚本而不是一个大脚本因为爬虫失败时清洗和分析不该用旧数据硬跑一遍给一个假结果。拆开后每步可以单独调试也可以定时任务里只重跑爬虫阶段。验证结果可信度我有两个习惯。第一是抽样对比线上数据从大屏上随机挑几个公司和岗位回到招聘网站搜索页面核对薪资区间和经验要求是否一致抽样十条以上再下结论。第二是核对统计口径比如学历平均月薪是按均值还是中位数报告中要写清楚不然别人复现时看到数字对不上会以为代码错了。这两步走完这份源码才真正从“别人的项目”变成你自己能掌控的数据分析作品。从那以后我每次拿到爬虫加可视化的项目都强制走一遍“字段全、空值可查、口径可溯”三步确认能落地才跑数据。这个项目让我把这三步练扎实了希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →